Index: lib/python/ZODB/Connection.py =================================================================== --- lib/python/ZODB/Connection.py (revision 1300) +++ lib/python/ZODB/Connection.py (working copy) @@ -252,6 +252,7 @@ obj._p_changed = None obj._p_serial = serial self._pre_cache.pop(oid) + self._setSticky(obj) self._cache[oid] = obj return obj @@ -260,9 +261,9 @@ self._cache.minimize() # TODO: we should test what happens when cacheGC is called mid-transaction. - def cacheGC(self): + def cacheGC(self, may_flush_sticky=False): """Reduce cache size to target size.""" - self._cache.incrgc() + self._cache.incrgc(may_flush_sticky) __onCloseCallbacks = None def onCloseCallback(self, f): @@ -631,6 +632,7 @@ s = self._storage.store(oid, serial, p, self._version, transaction) self._store_count += 1 + self._setSticky(obj) # Put the object in the cache before handling the # response, just in case the response contains the # serial number for a newly created object @@ -1036,6 +1038,21 @@ ########################################################################## ########################################################################## + # Volatile attribute lifetime garantee support + def _setSticky(self, obj): + """set 'obj._p_sticky' if the class tells us so. + + We had prefered to do this in 'Persistent.__new__'. + However, in Zope 2 'Persistent.__new__' often interferes + with 'ExtensionClass.__new__' and then Python raises + an almost uncomprehensible 'TypeError' unless + 'Persistent.__new__' is identical to 'ExtensionClass.__new__'. + """ + from persistent import Persistent + if isinstance(obj, Persistent) and obj._p_sticky: + Persistent._p_sticky.__set__(obj, True) + + ########################################################################## # DEPRECATION candidates __getitem__ = get @@ -1058,6 +1075,7 @@ new._p_jar = self new._p_changed = 1 self._register(new) + self._setSticky(new) self._cache[oid] = new # DEPRECATION candidates Index: lib/python/ZODB/tests/test_sticky.py =================================================================== --- lib/python/ZODB/tests/test_sticky.py (revision 0) +++ lib/python/ZODB/tests/test_sticky.py (revision 6813) @@ -0,0 +1,133 @@ +"""Sticky extension (aka "VolatileAttributeLifetimeGarantee").""" + +from unittest import TestCase, TestSuite, makeSuite + +from persistent import Persistent +import transaction +from ZODB.config import databaseFromString + +class PersistentWithoutSticky(Persistent): pass +class PersistentWithFalseSticky(Persistent): _p_sticky = False +class PersistentWithTrueSticky(Persistent): _p_sticky = True + +def setup_db(): + return databaseFromString(''' + + cache-size 0 + + + ''') + +class StickyTests(TestCase): + def test_sticky_inherited_from_class(self): + # Test that '_p_sticky' is inherited from the class. + + owos = PersistentWithoutSticky() + self.assertEqual(owos._p_sticky, False) + self.assertEqual(Persistent._p_sticky.__get__(owos), False) + + owfs = PersistentWithFalseSticky() + self.assertEqual(owfs._p_sticky, False) + self.assertEqual(Persistent._p_sticky.__get__(owfs), False) + + # This no longer works -- as we have been forced to + # abandon a special 'Persistent.__new__' (due to the + # existence of 'ExtensionClass.__new__'). + # Perform equivalent tests later +## owts = PersistentWithTrueSticky() +## self.assertEqual(owts._p_sticky, True) +## self.assertEqual(Persistent._p_sticky.__get__(owts), True) + +## #It can be overridden on the instance level but does not +## # change the persistent state +## owts._p_sticky = False +## self.assertEqual(owts._p_sticky, False) +## self.assertEqual(Persistent._p_sticky.__get__(owts), False) +## self.assertEqual(owts._p_changed, False) + + #Now test that inheritance is resurrected after loading from + #the storage. + + transaction.abort() # to be carefull + db = setup_db() # sets up database with cache size 0 + c = db.open() + r = c.root() + o = PersistentWithTrueSticky() + o.x = True + r['o'] = o + transaction.commit() + # now '_p_sticky' must have been set + self.assertEqual(o._p_sticky, True) + self.assertEqual(Persistent._p_sticky.__get__(o), True) + #It can be overridden on the instance level but does not + # change the persistent state + changed = o._p_changed + o._p_sticky = False + self.assertEqual(o._p_sticky, False) + self.assertEqual(Persistent._p_sticky.__get__(o), False) + self.assertEqual(o._p_changed, changed) + o._p_deactivate() # make it a ghost + r._p_deactivate() # the root as well + del o # now flushed from cache + o = r['o'] + self.assertEqual(Persistent._p_sticky.__get__(o), True) + + def test_sticky_basic_behavior(self): + transaction.abort() # to be careful + db = setup_db() # sets up database with cache size 0 + c = db.open() + r = c.root() + o = PersistentWithoutSticky() + o.x = True + r['o'] = o + transaction.commit() + o._p_sticky = True + o.x # unghostify + self.assertEqual(o._p_changed, False) + + #Check that "o" is not ghostified by a savepoint -- this + #also tests that "Connection.cacheGC()" does not ghostify + #sticky objects. + + r['test'] = 1 # modify something, that 'savepoint' has an effect + transaction.savepoint() + self.assertEqual(o._p_changed, False) + + #Check that a non sticky "o" is ghostified by a savepoint + + o._p_sticky = False + r['test'] = 1 # modify something, that 'savepoint' has an effect + transaction.savepoint() + self.assertEqual(o._p_changed, None) + + #We make it sticky again and verify that a transaction end + #does ghostify it. We use "abort" in our test. + #This tests "commit" as well, as ghostification happens + #in "_flushInvalidations" called by both "abort" and "commit". + + o._p_sticky = True + o.x # unghostify + self.assertEqual(o._p_changed, False) + r['test'] = 1 # modify something, that 'abort' has an effect + transaction.abort() + self.assertEqual(o._p_changed, None) + + #Next we verify ghostification by "cacheGC(True)". + #The "savepoint" test above already demonstrated that "cacheGC()" + #(equivalent to "cacheGC(False)") did not ghostify. + + o.x # unghostify + c.cacheGC(True) + self.assertEqual(o._p_changed, None) + + #Next we check that closing the connection does ghostify + + o.x # unghostify + c.close() + self.assertEqual(o._p_changed, None) + + +def test_suite(): + return TestSuite([makeSuite(StickyTests)]) + + Index: lib/python/ZODB/tests/testConnectionSavepoint.py =================================================================== --- lib/python/ZODB/tests/testConnectionSavepoint.py (revision 1300) +++ lib/python/ZODB/tests/testConnectionSavepoint.py (working copy) @@ -119,8 +119,11 @@ Making a savepoint at this time used to leave the cache holding the same number of objects. Make sure the cache shrinks now instead. +Deactivate the root object as otherwise it keeps the ghosts (and ghosts) +seem to be counted in "len". >>> dummy = transaction.savepoint() + >>> rt._p_deactivate() >>> len(cn._cache) <= CACHESIZE + 1 True Index: lib/python/ZODB/serialize.py =================================================================== --- lib/python/ZODB/serialize.py (revision 1300) +++ lib/python/ZODB/serialize.py (working copy) @@ -510,6 +510,7 @@ # a ghost. obj._p_changed = None + self._conn._setSticky(obj) self._cache[oid] = obj return obj Index: lib/python/persistent/cPickleCache.c =================================================================== --- lib/python/persistent/cPickleCache.c (revision 1300) +++ lib/python/persistent/cPickleCache.c (working copy) @@ -167,7 +167,7 @@ } static int -scan_gc_items(ccobject *self, int target) +scan_gc_items(ccobject *self, int target, int may_flush_sticky) { /* This function must only be called with the ring lock held, because it places non-object placeholders in the ring. @@ -200,7 +200,9 @@ is not the home */ object = OBJECT_FROM_RING(self, here); - if (object->state == cPersistent_UPTODATE_STATE) { + if (object->state == cPersistent_UPTODATE_STATE + && (may_flush_sticky || !object->sticky) + ) { CPersistentRing placeholder; PyObject *method; PyObject *temp; @@ -244,7 +246,7 @@ } static PyObject * -lockgc(ccobject *self, int target_size) +lockgc(ccobject *self, int target_size, int may_flush_sticky) { /* This is thread-safe because of the GIL, and there's nothing * in between checking the ring_lock and acquiring it that calls back @@ -256,7 +258,7 @@ } self->ring_lock = 1; - if (scan_gc_items(self, target_size) < 0) { + if (scan_gc_items(self, target_size, may_flush_sticky) < 0) { self->ring_lock = 0; return NULL; } @@ -269,7 +271,7 @@ static PyObject * cc_incrgc(ccobject *self, PyObject *args) { - int obsolete_arg = -999; + int may_flush_sticky = 1; int starting_size = self->non_ghost_count; int target_size = self->cache_size; @@ -284,17 +286,10 @@ } - if (!PyArg_ParseTuple(args, "|i:incrgc", &obsolete_arg)) + if (!PyArg_ParseTuple(args, "|i:incrgc", &may_flush_sticky)) return NULL; - if (obsolete_arg != -999 - && - (PyErr_Warn(PyExc_DeprecationWarning, - "No argument expected") - < 0)) - return NULL; - - return lockgc(self, target_size); + return lockgc(self, target_size, may_flush_sticky); } static PyObject * @@ -307,7 +302,7 @@ if (!PyArg_ParseTuple(args, "|i:full_sweep", &dt)) return NULL; if (dt == -999) - return lockgc(self, 0); + return lockgc(self, 0, 1); else return cc_incrgc(self, args); } @@ -327,7 +322,8 @@ < 0)) return NULL; - return lockgc(self, 0); + /* Note: flushing sticky objects may be wrong ... */ + return lockgc(self, 0, 1); } static int @@ -643,10 +639,8 @@ "Ghostify all objects that are not modified. Takes an optional\n" "argument, but ignores it."}, {"incrgc", (PyCFunction)cc_incrgc, METH_VARARGS, - "incrgc() -- Perform incremental garbage collection\n\n" - "This method had been depricated!" - "Some other implementations support an optional parameter 'n' which\n" - "indicates a repetition count; this value is ignored."}, + "incrgc([mayFlushSticky=True]) -- Perform incremental garbage collection\n\n" + "Objects with '_p_sticky' true are only collected if 'mayFlushSticky' is true."}, {"invalidate", (PyCFunction)cc_invalidate, METH_O, "invalidate(oids) -- invalidate one, many, or all ids"}, {"get", (PyCFunction)cc_get, METH_VARARGS, Index: lib/python/persistent/cPersistence.c =================================================================== --- lib/python/persistent/cPersistence.c (revision 1300) +++ lib/python/persistent/cPersistence.c (working copy) @@ -32,6 +32,7 @@ static PyObject *py___getattr__, *py___setattr__, *py___delattr__; static PyObject *py___slotnames__, *copy_reg_slotnames, *__newobj__; static PyObject *py___getnewargs__, *py___getstate__; +static PyObject *py__p_sticky; static int @@ -52,6 +53,7 @@ INIT_STRING(__slotnames__); INIT_STRING(__getnewargs__); INIT_STRING(__getstate__); + INIT_STRING(_p_sticky); #undef INIT_STRING return 0; } @@ -672,6 +674,24 @@ return 1; } + +static PyObject * +Per_get_sticky(cPersistentObject *self) +{ + return PyBool_FromLong(self->sticky); +} + +static int +Per_set_sticky(cPersistentObject *self, PyObject *v) +{ + int true = PyObject_IsTrue(v); + + if (true == -1) return -1; + self->sticky = true; + return 0; +} + + static PyObject* Per_getattro(cPersistentObject *self, PyObject *name) { @@ -747,7 +767,11 @@ if (changed(self) < 0) goto Done; } - } + } else + /* ensure that sticky also updates our C level field */ + if (strcmp(s, "_p_sticky") == 0 && Per_set_sticky(self, v) < 0) + goto Done; + result = PyObject_GenericSetAttr((PyObject *)self, name, v); Done: @@ -1018,6 +1042,7 @@ {"_p_oid", (getter)Per_get_oid, (setter)Per_set_oid}, {"_p_serial", (getter)Per_get_serial, (setter)Per_set_serial}, {"_p_state", (getter)Per_get_state}, + {"_p_sticky", (getter)Per_get_sticky, (setter)Per_set_sticky}, {NULL} }; @@ -1128,9 +1153,51 @@ return 0; } +# if 0 +""" +Note: 'ExtensionClass' defines '__new__' (without need, it could live +without problem with the one inherited from 'object'). +For many Zope 2 objects, an 'ExtensionClass' derived class +preceeds 'Persistent' in the mro. In these cases, the 'ExtensionClass' +'__new__' is used (and obviously would not do the right thing). +Python catches the problem (as it insists that the 'tp_new' +of the types '__new__' equals the 'tp_new' of the subtypes most derived +non-head base. Unfortunately, the resulting 'TypeError' is almost +uncomprehensible. + +Whatsoever, when 'Persistent' should work with Zope 2, it must +have the same 'tp_new' as 'Extensionclass' (which means 'PyType_GenericNew'). +""" static PyObject * +Per_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) +{ + PyObject *obj = PyType_GenericNew(subtype, NULL, NULL); + if (obj) + { + PyObject *sticky = PyObject_GetAttr(obj, py__p_sticky); + if (sticky) + { + int isticky = PyObject_IsTrue(sticky); + if (isticky == -1) + { + Py_DECREF(sticky); Py_DECREF(obj); + return NULL; + } + ((cPersistentObject *)obj)->sticky = isticky; + Py_DECREF(sticky); + } + PyErr_Clear(); + } + return obj; +} +# endif /* 0 */ + +static PyObject * simple_new(PyObject *self, PyObject *type_object) { + /* does not work -- see above + return Per_new((PyTypeObject *)type_object, NULL, NULL); + */ return PyType_GenericNew((PyTypeObject *)type_object, NULL, NULL); } @@ -1169,6 +1236,9 @@ cPersistence_doc_string); Pertype.ob_type = &PyType_Type; + /* Does not work -- see above + Pertype.tp_new = Per_new; + */ Pertype.tp_new = PyType_GenericNew; if (PyType_Ready(&Pertype) < 0) return; Index: lib/python/persistent/cPersistence.h =================================================================== --- lib/python/persistent/cPersistence.h (revision 1300) +++ lib/python/persistent/cPersistence.h (working copy) @@ -59,7 +59,8 @@ CPersistentRing ring; \ char serial[8]; \ signed char state; \ - unsigned char reserved[3]; + unsigned char sticky; /* we need only a bit; but byte access is cheaper */\ + unsigned char reserved[2]; #define cPersistent_GHOST_STATE -1 #define cPersistent_UPTODATE_STATE 0