=====================
Connection Management
=====================


Here we exercise the connection management done by the DB class.

    >>> from ZODB import DB
    >>> from ZODB.MappingStorage import MappingStorage as Storage

Capturing log messages from DB is important for some of the examples:

    >>> from zope.testing.loggingsupport import InstalledHandler
    >>> handler = InstalledHandler('ZODB.DB')

Create a storage, and wrap it in a DB wrapper:

    >>> st = Storage()
    >>> db = DB(st)

By default, we can open 7 connections without any log messages:

    >>> conns = [db.open() for dummy in range(7)]
    >>> handler.records
    []

Open one more, and we get a warning:

    >>> conns.append(db.open())
    >>> len(handler.records)
    1
    >>> msg = handler.records[0]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB WARNING DB.open() has 8 open connections with a pool_size of 7

Open 6 more, and we get 6 more warnings:

    >>> conns.extend([db.open() for dummy in range(6)])
    >>> len(conns)
    14
    >>> len(handler.records)
    7
    >>> msg = handler.records[-1]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB WARNING DB.open() has 14 open connections with a pool_size of 7

Add another, so that it's more than twice the default, and the level
rises to critical:

    >>> conns.append(db.open())
    >>> len(conns)
    15
    >>> len(handler.records)
    8
    >>> msg = handler.records[-1]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB CRITICAL DB.open() has 15 open connections with a pool_size of 7

While it's boring, it's important to verify that the same relationships
hold if the default pool size is overridden.

    >>> handler.clear()
    >>> st.close()
    >>> st = Storage()
    >>> PS = 2 # smaller pool size
    >>> db = DB(st, pool_size=PS)
    >>> conns = [db.open() for dummy in range(PS)]
    >>> handler.records
    []

A warning for opening one more:

    >>> conns.append(db.open())
    >>> len(handler.records)
    1
    >>> msg = handler.records[0]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB WARNING DB.open() has 3 open connections with a pool_size of 2

More warnings through 4 connections:

    >>> conns.extend([db.open() for dummy in range(PS-1)])
    >>> len(conns)
    4
    >>> len(handler.records)
    2
    >>> msg = handler.records[-1]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB WARNING DB.open() has 4 open connections with a pool_size of 2

And critical for going beyond that:

    >>> conns.append(db.open())
    >>> len(conns)
    5
    >>> len(handler.records)
    3
    >>> msg = handler.records[-1]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB CRITICAL DB.open() has 5 open connections with a pool_size of 2

We can change the pool size on the fly:

    >>> handler.clear()
    >>> db.setPoolSize(6)
    >>> conns.append(db.open())
    >>> handler.records  # no log msg -- the pool is bigger now
    []
    >>> conns.append(db.open()) # but one more and there's a warning again
    >>> len(handler.records)
    1
    >>> msg = handler.records[0]
    >>> print msg.name, msg.levelname, msg.getMessage()
    ZODB.DB WARNING DB.open() has 7 open connections with a pool_size of 6

Enough of that.

    >>> handler.clear()
    >>> st.close()

More interesting is the stack-like nature of connection reuse.  So long as
we keep opening new connections, and keep them alive, all connections
returned are distinct:

    >>> st = Storage()
    >>> db = DB(st)
    >>> c1 = db.open()
    >>> c2 = db.open()
    >>> c3 = db.open()
    >>> c1 is c2 or c1 is c3 or c2 is c3
    False

Let's put some markers on the connections, so we can identify these
specific objects later:

    >>> c1.MARKER = 'c1'
    >>> c2.MARKER = 'c2'
    >>> c3.MARKER = 'c3'

Now explicitly close c1 and c2:

    >>> c1.close()
    >>> c2.close()

Reaching into the internals, we can see that db's connection pool now has
two connections available for reuse, and knows about three connections in
all:

    >>> pool = db._pools['']
    >>> len(pool.available)
    2
    >>> len(pool.all)
    3

Since we closed c2 last, it's at the top of the available stack, so will
be reused by the next open():

    >>> c1 = db.open()
    >>> c1.MARKER
    'c2'
    >>> len(pool.available), len(pool.all)
    (1, 3)

    >>> c3.close()  # now the stack has c3 on top, then c1
    >>> c2 = db.open()
    >>> c2.MARKER
    'c3'
    >>> len(pool.available), len(pool.all)
    (1, 3)
    >>> c3 = db.open()
    >>> c3.MARKER
    'c1'
    >>> len(pool.available), len(pool.all)
    (0, 3)

What about the 3 in pool.all?  We've seen that closing connections doesn't
reduce pool.all, and it would be bad if DB kept connections alive forever.

In fact pool.all is a "weak set" of connections -- it holds weak references
to connections.  That alone doesn't keep connection objects alive.  The
weak set allows DB's statistics methods to return info about connections
that are still alive.


    >>> len(db.cacheDetailSize())  # one result for each connection's cache
    3

If a connection object is abandoned (it becomes unreachable), then it
will vanish from pool.all automatically.  However, connections are
involved in cycles, so exactly when a connection vanishes from pool.all
isn't predictable.  It can be forced by running gc.collect():

    >>> import gc
    >>> dummy = gc.collect()
    >>> len(pool.all)
    3
    >>> c3 = None
    >>> dummy = gc.collect()  # removes c3 from pool.all
    >>> len(pool.all)
    2

Note that c3 is really gone; in particular it didn't get added back to
the stack of available connections by magic:

    >>> len(pool.available)
    0

Nothing in that last block should have logged any msgs:

    >>> handler.records
    []

If "too many" connections are open, then closing one may kick an older
closed one out of the available connection stack.

    >>> st.close()
    >>> st = Storage()
    >>> db = DB(st, pool_size=3)
    >>> conns = [db.open() for dummy in range(6)]
    >>> len(handler.records)  # 3 warnings for the "excess" connections
    3
    >>> pool = db._pools['']
    >>> len(pool.available), len(pool.all)
    (0, 6)

Let's mark them:

    >>> for i, c in enumerate(conns):
    ...     c.MARKER = i

Closing connections adds them to the stack:

    >>> for i in range(3):
    ...     conns[i].close()
    >>> len(pool.available), len(pool.all)
    (3, 6)
    >>> del conns[:3]  # leave the ones with MARKERs 3, 4 and 5

Closing another one will purge the one with MARKER 0 from the stack
(since it was the first added to the stack):

    >>> [c.MARKER for c in pool.available]
    [0, 1, 2]
    >>> conns[0].close()  # MARKER 3
    >>> len(pool.available), len(pool.all)
    (3, 5)
    >>> [c.MARKER for c in pool.available]
    [1, 2, 3]

Similarly for the other two:

    >>> conns[1].close(); conns[2].close()
    >>> len(pool.available), len(pool.all)
    (3, 3)
    >>> [c.MARKER for c in pool.available]
    [3, 4, 5]

Reducing the pool size may also purge the oldest closed connections:

    >>> db.setPoolSize(2)  # gets rid of MARKER 3
    >>> len(pool.available), len(pool.all)
    (2, 2)
    >>> [c.MARKER for c in pool.available]
    [4, 5]

Since MARKER 5 is still the last one added to the stack, it will be the
first popped:

    >>> c1 = db.open(); c2 = db.open()
    >>> c1.MARKER, c2.MARKER
    (5, 4)
    >>> len(pool.available), len(pool.all)
    (0, 2)

Clean up.

    >>> st.close()
    >>> handler.uninstall()
