From 98c88f3db87833dec12aa6617d9b0ae19dae5913 Mon Sep 17 00:00:00 2001
From: Tim Peters <tim.one@comcast.net>
Date: Wed, 31 Aug 2005 21:27:28 +0000
Subject: [PATCH] Merge rev 38051 from 3.4 branch.

Code and new test to ensure that making a savepoint triggers cache gc.
---
 NEWS.txt                                  | 31 ++++++++----
 src/ZODB/Connection.py                    |  8 +++-
 src/ZODB/tests/testConnectionSavepoint.py | 57 ++++++++++++++++++++++-
 3 files changed, 86 insertions(+), 10 deletions(-)

diff --git a/NEWS.txt b/NEWS.txt
index 30bce689..6825dcf2 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -16,6 +16,13 @@ Zope3 development).  These are the dates of the internal releases:
 Savepoints
 ----------
 
+- (3.5.0) As for deprecated subtransaction commits, the intent was
+  that making a savepoint would invoke incremental garbage collection on
+  Connection memory caches, to try to reduce the number of objects in
+  cache to the configured cache size.  Due to an oversight, this didn't
+  happen, and stopped happening for subtransaction commits too.  Making a
+  savepoint (or doing a subtransaction commit) does invoke cache gc now.
+
 - (3.5a3) When a savepoint is made, the states of objects modified so far
   are saved to a temporary storage (an instance of class ``TmpStore``,
   although that's an internal implementation detail).  That storage needs
@@ -52,11 +59,15 @@ Subtransactions are deprecated
 
   to::
 
-      transaction.savepoint()
+      transaction.savepoint(True)
+
+  That is, make a savepoint, and forget it.  As shown, it's best to pass
+  ``True`` for the optional ``optimistic`` argument in this case:  because
+  there's no possibility of asking for a rollback later, there's no need
+  to insist that all data managers support rollback.
 
-  That is, make a savepoint, and forget it.  In rarer cases, a
-  subtransaction commit is followed later by a subtransaction abort.  In
-  that case, change the initial::
+  In rarer cases, a subtransaction commit is followed later by a
+  subtransaction abort.  In that case, change the initial::
 
       transaction.commit(1)
 
@@ -229,11 +240,15 @@ Subtransactions
 
   to::
 
-      transaction.savepoint()
+      transaction.savepoint(True)
+
+  That is, make a savepoint, and forget it.  As shown, it's best to pass
+  ``True`` for the optional ``optimistic`` argument in this case:  because
+  there's no possibility of asking for a rollback later, there's no need
+  to insist that all data managers support rollback.
 
-  That is, make a savepoint, and forget it.  In rarer cases, a
-  subtransaction commit is followed later by a subtransaction abort.  In
-  that case, change the initial::
+  In rarer cases, a subtransaction commit is followed later by a
+  subtransaction abort.  In that case, change the initial::
 
       transaction.commit(1)
 
diff --git a/src/ZODB/Connection.py b/src/ZODB/Connection.py
index 29e5de24..38a100e1 100644
--- a/src/ZODB/Connection.py
+++ b/src/ZODB/Connection.py
@@ -1063,7 +1063,13 @@ class Connection(ExportImport, object):
         self._registered_objects = []
 
         state = self._storage.position, self._storage.index.copy()
-        return Savepoint(self, state)
+        result = Savepoint(self, state)
+        # While the interface doesn't guarantee this, savepoints are
+        # sometimes used just to "break up" very long transactions, and as
+        # a pragmatic matter this is a good time to reduce the cache
+        # memory burden.
+        self.cacheGC()
+        return result
 
     def _rollback(self, state):
         self._abort()
diff --git a/src/ZODB/tests/testConnectionSavepoint.py b/src/ZODB/tests/testConnectionSavepoint.py
index 2592721d..7366238c 100644
--- a/src/ZODB/tests/testConnectionSavepoint.py
+++ b/src/ZODB/tests/testConnectionSavepoint.py
@@ -85,7 +85,62 @@ def testCantCloseConnectionWithActiveSavepoint():
 
     >>> db.close()
     """
-    
+
+def testSavepointDoesCacheGC():
+    """\
+Although the interface doesn't guarantee this internal detail, making a
+savepoint should do incremental gc on connection memory caches.  Indeed,
+one traditional use for savepoints (started by the older, related
+"subtransaction commit" idea) is simply to free memory space midstream
+during a long transaction.  Before ZODB 3.4.2, making a savepoint failed
+to trigger cache gc, and this test verifies that it now does.
+
+    >>> import ZODB
+    >>> from ZODB.tests.MinPO import MinPO
+    >>> from ZODB.MappingStorage import MappingStorage
+    >>> import transaction
+    >>> CACHESIZE = 5  # something tiny
+    >>> LOOPCOUNT = CACHESIZE * 10
+    >>> st = MappingStorage("Test")
+    >>> db = ZODB.DB(st, cache_size=CACHESIZE)
+    >>> cn = db.open()
+    >>> rt = cn.root()
+
+Now attach substantially more than CACHESIZE persistent objects to the root:
+
+    >>> for i in range(LOOPCOUNT):
+    ...     rt[i] = MinPO(i)
+    >>> transaction.commit()
+
+Now modify all of them; the cache should contain LOOPCOUNT MinPO objects
+then, + 1 for the root object:
+
+    >>> for i in range(LOOPCOUNT):
+    ...     obj = rt[i]
+    ...     obj.value = -i
+    >>> len(cn._cache) == LOOPCOUNT + 1
+    True
+
+Making a savepoint at this time used to leave the cache holding the same
+number of objects.  Make sure the cache shrinks now instead.
+
+    >>> dummy = transaction.savepoint()
+    >>> len(cn._cache) <= CACHESIZE + 1
+    True
+
+Verify all the values are as expected:
+
+    >>> failures = []
+    >>> for i in range(LOOPCOUNT):
+    ...     obj = rt[i]
+    ...     if obj.value != -i:
+    ...         failures.append(obj)
+    >>> failures
+    []
+
+    >>> transaction.abort()
+    >>> db.close()
+"""
 
 def test_suite():
     return unittest.TestSuite((
-- 
2.30.9