[Zodb-checkins] SVN: ZODB/branches/3.3/ Zope3-dev Collector #139:
Memory leak involving buckets and connections
Tim Peters
tim.one at comcast.net
Wed Jul 7 22:14:43 EDT 2004
Log message for revision 26199:
Zope3-dev Collector #139: Memory leak involving buckets and connections
Connection objects were typically immortal because the threaded
transaction manager kept them in an ever-growing lists. Reworked the
transaction manager internals to use a simple implementation of weak sets
instead. This plugs all leaks in the test program attached to the
collector report (which was leaking about 100KB/sec on my box).
-=-
Modified: ZODB/branches/3.3/NEWS.txt
===================================================================
--- ZODB/branches/3.3/NEWS.txt 2004-07-08 00:00:21 UTC (rev 26198)
+++ ZODB/branches/3.3/NEWS.txt 2004-07-08 02:14:43 UTC (rev 26199)
@@ -2,6 +2,21 @@
=======================
Release date: DD-MMM-2004
+Transaction Managers
+--------------------
+
+Zope3-dev Collector #139: Memory leak involving buckets and connections
+
+The transaction manager internals effectively made every Connection
+object immortal, except for those explicitly closed. Since typical
+practice is not to close connections explicitly (and closing a DB
+happens not to close the connections to it -- although that may
+change), this caused massive memory leaks when many connections were
+opened. The transaction manager internals were reworked to use weak
+references instead, so that connection memory (and other registered
+synch objects) now get cleaned up when nothing other than the
+transaction manager knows about them.
+
Storages
--------
Modified: ZODB/branches/3.3/src/transaction/_manager.py
===================================================================
--- ZODB/branches/3.3/src/transaction/_manager.py 2004-07-08 00:00:21 UTC (rev 26198)
+++ ZODB/branches/3.3/src/transaction/_manager.py 2004-07-08 02:14:43 UTC (rev 26199)
@@ -18,24 +18,70 @@
"""
import thread
+import weakref
from transaction._transaction import Transaction
+# We have to remember sets of synch objects, especially Connections.
+# But we don't want mere registration with a transaction manager to
+# keep a synch object alive forever; in particular, it's common
+# practice not to explicitly close Connection objects, and keeping
+# a Connection alive keeps a potentially huge number of other objects
+# alive (e.g., the cache, and everything reachable from it too).
+#
+# Therefore we use "weak sets" internally. The implementation here
+# implements just enough of Python's sets.Set interface for our needs.
+
+class WeakSet(object):
+ """A set of objects that doesn't keep its elements alive.
+
+ The objects in the set must be weakly referencable.
+ The objects need not be hashable, and need not support comparison.
+ Two objects are considered to be the same iff their id()s are equal.
+
+ When the only references to an object are weak references (including
+ those from WeakSets), the object can be garbage-collected, and
+ will vanish from any WeakSets it may be a member of at that time.
+ """
+
+ def __init__(self):
+ # Map id(obj) to obj. By using ids as keys, we avoid requiring
+ # that the elements be hashable or comparable.
+ self.data = weakref.WeakValueDictionary()
+
+ # Same as a Set, add obj to the collection.
+ def add(self, obj):
+ self.data[id(obj)] = obj
+
+ # Same as a Set, remove obj from the collection, and raise
+ # KeyError if obj not in the collection.
+ def remove(self, obj):
+ del self.data[id(obj)]
+
+ # Return a list of all the objects in the collection.
+ # Because a weak dict is used internally, iteration
+ # is dicey (the underlying dict may change size during
+ # iteration, due to gc or activity from other threads).
+ # as_list() attempts to be safe.
+ def as_list(self):
+ return self.data.values()
+
+
class TransactionManager(object):
def __init__(self):
self._txn = None
- self._synchs = []
+ self._synchs = WeakSet()
def begin(self):
if self._txn is not None:
self._txn.abort()
- self._txn = Transaction(self._synchs, self)
+ self._txn = Transaction(self._synchs.as_list(), self)
return self._txn
def get(self):
if self._txn is None:
- self._txn = Transaction(self._synchs, self)
+ self._txn = Transaction(self._synchs.as_list(), self)
return self._txn
def free(self, txn):
@@ -43,7 +89,7 @@
self._txn = None
def registerSynch(self, synch):
- self._synchs.append(synch)
+ self._synchs.add(synch)
def unregisterSynch(self, synch):
self._synchs.remove(synch)
@@ -57,9 +103,9 @@
def __init__(self):
# _threads maps thread ids to transactions
self._txns = {}
- # _synchs maps a thread id to a list of registered synchronizers.
- # The list is passed to the Transaction constructor, because
- # it needs to call the synchronizers when it commits.
+ # _synchs maps a thread id to a WeakSet of registered synchronizers.
+ # The set elements are passed to the Transaction constructor,
+ # because the latter needs to call the synchronizers when it commits.
self._synchs = {}
def begin(self):
@@ -67,14 +113,20 @@
txn = self._txns.get(tid)
if txn is not None:
txn.abort()
- txn = self._txns[tid] = Transaction(self._synchs.get(tid), self)
+ synchs = self._synchs.get(tid)
+ if synchs is not None:
+ synchs = synchs.as_list()
+ txn = self._txns[tid] = Transaction(synchs, self)
return txn
def get(self):
tid = thread.get_ident()
txn = self._txns.get(tid)
if txn is None:
- txn = self._txns[tid] = Transaction(self._synchs.get(tid), self)
+ synchs = self._synchs.get(tid)
+ if synchs is not None:
+ synchs = synchs.as_list()
+ txn = self._txns[tid] = Transaction(synchs, self)
return txn
def free(self, txn):
@@ -84,10 +136,12 @@
def registerSynch(self, synch):
tid = thread.get_ident()
- L = self._synchs.setdefault(tid, [])
- L.append(synch)
+ ws = self._synchs.get(tid)
+ if ws is None:
+ ws = self._synchs[tid] = WeakSet()
+ ws.add(synch)
def unregisterSynch(self, synch):
tid = thread.get_ident()
- L = self._synchs.get(tid)
- L.remove(synch)
+ ws = self._synchs[tid]
+ ws.remove(synch)
More information about the Zodb-checkins
mailing list