[Checkins] SVN: zope.locking/trunk/s Make tokens orderable to fix an issue where the token utility

Patrick Strawderman patrick at zope.com
Mon Nov 23 16:06:26 EST 2009


Log message for revision 105969:
  Make tokens orderable to fix an issue where the token utility
  wasn't able to clean up expired tokens. (See CHANGES.txt).
  

Changed:
  U   zope.locking/trunk/setup.py
  U   zope.locking/trunk/src/zope/locking/CHANGES.txt
  U   zope.locking/trunk/src/zope/locking/README.txt
  U   zope.locking/trunk/src/zope/locking/adapters.py
  U   zope.locking/trunk/src/zope/locking/annoying.txt
  U   zope.locking/trunk/src/zope/locking/browser/tokenview.py
  U   zope.locking/trunk/src/zope/locking/cleanup.txt
  U   zope.locking/trunk/src/zope/locking/configure.zcml
  A   zope.locking/trunk/src/zope/locking/generations.py
  U   zope.locking/trunk/src/zope/locking/interfaces.py
  A   zope.locking/trunk/src/zope/locking/testing.py
  U   zope.locking/trunk/src/zope/locking/tests.py
  U   zope.locking/trunk/src/zope/locking/tokens.py
  U   zope.locking/trunk/src/zope/locking/utility.py

-=-
Modified: zope.locking/trunk/setup.py
===================================================================
--- zope.locking/trunk/setup.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/setup.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -9,22 +9,23 @@
     include_package_data=True,
     install_requires = [
         'setuptools',
-        'zope.security',
-        'zope.interface',
-        'zope.i18nmessageid',
+        'ZODB3',
+        'pytz',
+        'zc.i18n',
+        'zope.app.generations',
+        'zope.app.keyreference',
+        'zope.app.publisher',
+        'zope.app.testing',
         'zope.component',
-        'zope.schema',
-        'zope.app.testing',
-        'zope.testing',
         'zope.event',
-        'ZODB3',
-        'zope.app.keyreference',
+        'zope.formlib',
+        'zope.i18nmessageid',
+        'zope.interface',
         'zope.location',
         'zope.publisher',
-        'zope.formlib',
-        'zope.app.publisher',
-        'zc.i18n',
-        'pytz',
+        'zope.schema',
+        'zope.security',
+        'zope.testing',
         ],
     zip_safe = False,
     description=open("README.txt").read(),

Modified: zope.locking/trunk/src/zope/locking/CHANGES.txt
===================================================================
--- zope.locking/trunk/src/zope/locking/CHANGES.txt	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/CHANGES.txt	2009-11-23 21:06:25 UTC (rev 105969)
@@ -2,6 +2,28 @@
 Changes
 =======
 
+
+----------------
+1.2 (unreleased)
+----------------
+
+- Bug fix: tokens were stored in a manner that prevented them from
+  being cleaned up properly in the utility's _principal_ids mapping.
+  Make zope.locking.tokens.Token orderable to fix this, as tokens
+  are stored as keys in BTrees.
+
+- Add a zope.app.generations Schema Manager to clean up any lingering
+  tokens due to this bug.  Token utilities not accessible through the
+  component registry can be cleaned up manually with
+  zope.locking.generations.fix_token_utility.
+
+- TokenUtility's register method will now add the token to the utility's
+  database connection if the token provides IPersistent.
+
+- Clean up the tests and docs and move some common code to testing.py.
+
+- Fix some missing imports.
+
 ---
 1.1
 ---

Modified: zope.locking/trunk/src/zope/locking/README.txt
===================================================================
--- zope.locking/trunk/src/zope/locking/README.txt	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/README.txt	2009-11-23 21:06:25 UTC (rev 105969)
@@ -8,7 +8,7 @@
 
 - advisory shared locks for individual objects; and
 
-- frozen objects (locked to noone).
+- frozen objects (locked to no one).
 
 Locks and freezes by themselves are advisory tokens and inherently
 meaningless.  They must be given meaning by other software, such as a security
@@ -32,7 +32,8 @@
 The first object we'll introduce, then, is the TokenUtility: the utility that
 is responsible for the registration and the retrieving of tokens.
 
-    >>> from zope.locking import utility, interfaces
+    >>> from zope import component, interface
+    >>> from zope.locking import interfaces, utility, tokens
     >>> util = utility.TokenUtility()
     >>> from zope.interface.verify import verifyObject
     >>> verifyObject(interfaces.ITokenUtility, util)
@@ -41,45 +42,33 @@
 The utility only has a few methods--`get`, `iterForPrincipalId`,
 `__iter__`, and `register`--which we will look at below.  It is expected to be
 persistent, and the included implementation is in fact persistent.Persistent,
-and expects to be installed as a local utility.
+and expects to be installed as a local utility.  The utility needs a
+connection to the database before it can register persistent tokens.
 
-The standard token utility can accept tokens for any object that is adaptable
-to IKeyReference.  Here are some stubs to enable working with our token
-utility.
-
-    >>> from zope import interface, component
-    >>> import zope.app.keyreference.interfaces
-    >>> class IDemo(interface.Interface):
-    ...     """a demonstration interface for a demonstration class"""
+    >>> lock = tokens.ExclusiveLock(Demo(), 'Fantomas')
+    >>> util.register(lock)
+    Traceback (most recent call last):
     ...
-    >>> class Demo(object):
-    ...     interface.implements(IDemo)
-    ...
-    >>> class DemoKeyReference(object):
-    ...     component.adapts(IDemo)
-    ...     _class_counter = 0
-    ...     interface.implements(
-    ...         zope.app.keyreference.interfaces.IKeyReference)
-    ...     def __init__(self, context):
-    ...         self.context = context
-    ...         class_ = type(self)
-    ...         self._id = getattr(context, '__demo_key_reference__', None)
-    ...         if self._id is None:
-    ...             self._id = class_._class_counter
-    ...             context.__demo_key_reference__ = self._id
-    ...             class_._class_counter += 1
-    ...     key_type_id = 'zope.locking.README.DemoKeyReference'
-    ...     def __call__(self):
-    ...         return self.context
-    ...     def __hash__(self):
-    ...         return (self.key_type_id, self._id)
-    ...     def __cmp__(self, other):
-    ...         if self.key_type_id == other.key_type_id:
-    ...             return cmp(self._id, other._id)
-    ...         return cmp(self.key_type_id, other.key_type_id) 
-    ...
-    >>> component.provideAdapter(DemoKeyReference)
+    AttributeError: 'NoneType' object has no attribute 'add'
 
+    >>> conn.add(util)
+
+If the token provides IPersistent, the utility will add it to its connection.
+
+    >>> lock._p_jar is None
+    True
+
+    >>> lock = util.register(lock)
+    >>> lock._p_jar is util._p_jar
+    True
+
+    >>> lock.end()
+    >>> lock = util.register(lock)
+
+
+The standard token utility can accept tokens for any object that is adaptable
+to IKeyReference.
+
     >>> import datetime
     >>> import pytz
     >>> before_creation = datetime.datetime.now(pytz.utc)
@@ -110,7 +99,6 @@
 Here's an example of creating and registering an exclusive lock: the principal
 with an id of 'john' locks the demo object.
 
-    >>> from zope.locking import tokens 
     >>> lock = tokens.ExclusiveLock(demo, 'john')
     >>> res = util.register(lock)
     >>> res is lock
@@ -352,7 +340,7 @@
     >>> zope.locking.utils.now = hackNow # make code think it's 2 hours later
     >>> lock.duration
     datetime.timedelta(0, 14400)
-    >>> two >= lock.remaining_duration >= one 
+    >>> two >= lock.remaining_duration >= one
     True
     >>> lock.remaining_duration -= one
     >>> one >= lock.remaining_duration >= datetime.timedelta()
@@ -434,7 +422,7 @@
     >>> ev.object is lock
     True
 
-Here, principals with ids of 'john' and 'mary' have locked the demo object. 
+Here, principals with ids of 'john' and 'mary' have locked the demo object.
 The returned token implements ISharedLock and provides a superset of the
 IExclusiveLock capabilities.  These next operations should all look familiar
 from the discussion of the ExclusiveLock tokens above.
@@ -463,7 +451,7 @@
     >>> lock.ended >= lock.started
     True
 
-As mentioned, though, the SharedLock capabilities are a superset of the 
+As mentioned, though, the SharedLock capabilities are a superset of the
 ExclusiveLock ones.  There are two extra methods: `add` and `remove`.  These
 are able to add and remove principal ids as shared owners of the lock token.
 
@@ -576,7 +564,7 @@
 --------------
 
 An endable freeze token is similar to a lock token except that it grants the
-'lock' to noone.
+'lock' to no one.
 
     >>> token = util.register(tokens.EndableFreeze(demo))
     >>> verifyObject(interfaces.IEndableFreeze, token)
@@ -656,7 +644,7 @@
 In the context of the Zope 3 security model, the first two needs are intended
 to be addressed by the ITokenBroker interface, and associated adapter; the last
 need is intended to be addressed by the ITokenHandler, and associated
-adpaters.
+adapters.
 
 ------------
 TokenBrokers
@@ -676,7 +664,7 @@
 This won't work without an interaction, of course.  Notice that we start the
 example by registering the utility.  We would normally be required to put the
 utility in a site package, so that it would be persistent, but for this
-demonstration we are simplifying the regsitration.
+demonstration we are simplifying the registration.
 
     >>> component.provideUtility(util, provides=interfaces.ITokenUtility)
 
@@ -721,7 +709,7 @@
     ...
     >>> zope.security.management.endInteraction()
     >>> zope.security.management.newInteraction(DemoParticipation(joe))
-    
+
     >>> token = broker.lock()
     >>> interfaces.IExclusiveLock.providedBy(token)
     True
@@ -800,7 +788,7 @@
 With an interaction, the principals get the lock by default.
 
     >>> zope.security.management.newInteraction(DemoParticipation(joe))
-    
+
     >>> token = broker.lockShared()
     >>> interfaces.ISharedLock.providedBy(token)
     True
@@ -857,7 +845,7 @@
 ------
 
 The `freeze` method allows users to create an endable freeze.  It has no
-requirments on the interaction.  It should be protected carefully, from a 
+requirements on the interaction.  It should be protected carefully, from a
 security perspective.
 
     >>> token = broker.freeze()
@@ -889,7 +877,7 @@
 it returns the current active token for the object, or None.  It is useful
 for protected code, since utilities typically do not get security assertions,
 and this method can get its security assertions from the object, which is
-often the righ place.
+often the right place.
 
 Again, the TokenBroker does embody some policy; if it is not good policy for
 your application, build your own interfaces and adapters that do.
@@ -905,7 +893,7 @@
 very much policy, and other approaches may be useful. They are intended to be
 registered as trusted adapters.
 
-For exclusive locks and shared locks, then, we have token handlers. 
+For exclusive locks and shared locks, then, we have token handlers.
 Generally, token handlers give access to all of the same capabilities as their
 corresponding tokens, with the following additional constraints and
 capabilities:
@@ -926,7 +914,7 @@
 ExclusiveLockHandlers
 ---------------------
 
-Given the general constraints described above, exclusive lock handers will
+Given the general constraints described above, exclusive lock handlers will
 generally only allow access to their special capabilities if the operation
 is in an interaction with only the lock owner.
 
@@ -1035,7 +1023,7 @@
     ...
     ParticipationError
 
-The `add` method lets any principal ids be aded to the lock, but all
+The `add` method lets any principal ids be added to the lock, but all
 principals in the current interaction must be a part of the lock.
 
     >>> handler.add(('susan',))
@@ -1053,11 +1041,14 @@
 Warnings
 --------
 
-The token utility will register a token for an object if it can.  It does not
-check to see if it is actually the local token utility for the given object. 
-This should be arranged by clients of the token utility, and verified
-externally if desired.
+* The token utility will register a token for an object if it can.  It does not
+  check to see if it is actually the local token utility for the given object.
+  This should be arranged by clients of the token utility, and verified
+  externally if desired.
 
+* Tokens are stored as keys in BTrees, and therefore must be orderable
+  (i.e., they must implement __cmp__).
+
 -------------------------------
 Intended Security Configuration
 -------------------------------

Modified: zope.locking/trunk/src/zope/locking/adapters.py
===================================================================
--- zope.locking/trunk/src/zope/locking/adapters.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/adapters.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -1,6 +1,5 @@
 from zope import interface, component
 
-import zope.security.interfaces
 import zope.security.management
 
 from zope.locking import interfaces, tokens

Modified: zope.locking/trunk/src/zope/locking/annoying.txt
===================================================================
--- zope.locking/trunk/src/zope/locking/annoying.txt	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/annoying.txt	2009-11-23 21:06:25 UTC (rev 105969)
@@ -10,38 +10,14 @@
     True
 
     >>> from zope import interface, component
-    >>> import zope.app.keyreference.interfaces
-    >>> class IDemo(interface.Interface):
-    ...     """a demonstration interface for a demonstration class"""
-    ...
-    >>> class Demo(object):
-    ...     interface.implements(IDemo)
-    ...
-    >>> class DemoKeyReference(object):
-    ...     component.adapts(IDemo)
-    ...     _class_counter = 0
-    ...     interface.implements(
-    ...         zope.app.keyreference.interfaces.IKeyReference)
-    ...     def __init__(self, context):
-    ...         self.context = context
-    ...         class_ = type(self)
-    ...         self._id = getattr(context, '__demo_key_reference__', None)
-    ...         if self._id is None:
-    ...             self._id = class_._class_counter
-    ...             context.__demo_key_reference__ = self._id
-    ...             class_._class_counter += 1
-    ...     key_type_id = 'zope.locking.README.DemoKeyReference'
-    ...     def __call__(self):
-    ...         return self.context
-    ...     def __hash__(self):
-    ...         return (self.key_type_id, self._id)
-    ...     def __cmp__(self, other):
-    ...         if self.key_type_id == other.key_type_id:
-    ...             return cmp(self._id, other._id)
-    ...         return cmp(self.key_type_id, other.key_type_id) 
-    ...
-    >>> component.provideAdapter(DemoKeyReference)
 
+    >>> class Connection(object):
+    ...     def add(self, obj):
+    ...         pass
+    ...     def db(self):
+    ...         raise Exception()
+    >>> util._p_jar = Connection()
+
     >>> import datetime
     >>> import pytz
     >>> before_creation = datetime.datetime.now(pytz.utc)
@@ -125,7 +101,7 @@
     >>> zope.locking.utils.now = hackNow # make code think it's 2 hours later
     >>> lock.duration
     datetime.timedelta(0, 14400)
-    >>> two >= lock.remaining_duration >= one 
+    >>> two >= lock.remaining_duration >= one
     True
     >>> lock.remaining_duration -= datetime.timedelta(hours=1)
     >>> one >= lock.remaining_duration >= datetime.timedelta()
@@ -184,7 +160,7 @@
 --------------
 
 An endable freeze token is similar to a lock token except that it grants the
-'lock' to noone.
+'lock' to no one.
 
     >>> token = util.register(tokens.EndableFreeze(demo))
     >>> ev = events[-1]
@@ -330,7 +306,7 @@
     >>> zope.locking.utils.now = hackNow # make code think it's 2 hours later
     >>> token.duration
     datetime.timedelta(0, 14400)
-    >>> two >= token.remaining_duration >= one 
+    >>> two >= token.remaining_duration >= one
     True
     >>> token.remaining_duration -= one
     >>> one >= token.remaining_duration >= datetime.timedelta()

Modified: zope.locking/trunk/src/zope/locking/browser/tokenview.py
===================================================================
--- zope.locking/trunk/src/zope/locking/browser/tokenview.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/browser/tokenview.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -4,10 +4,11 @@
 from zope.interface.common.idatetime import ITZInfo
 from zope.security.checker import canAccess, canWrite
 from zope.security.interfaces import IGroup
+import zope.app.form.interfaces
+import zope.app.pagetemplate
 import zope.publisher.browser
 import zope.formlib.namedtemplate
 
-from zope.app.publisher.interfaces.browser import IBrowserMenuItem
 from zope.app.security.interfaces import IAuthentication
 
 from zope.locking import interfaces

Modified: zope.locking/trunk/src/zope/locking/cleanup.txt
===================================================================
--- zope.locking/trunk/src/zope/locking/cleanup.txt	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/cleanup.txt	2009-11-23 21:06:25 UTC (rev 105969)
@@ -4,7 +4,7 @@
 users.
 
 The token utility keeps three indexes of the tokens.  The primary index,
-`_locks`, is a mapping of 
+`_locks`, is a mapping of
 
   <key reference to content object>: (
       <token>,
@@ -13,7 +13,7 @@
 
 The utility's `get` method uses this data structure, for instance.
 
-Another index, `_principal_ids`, maps <principal id> to <set of <tokens>>. 
+Another index, `_principal_ids`, maps <principal id> to <set of <tokens>>.
 Its use is the `iterForPrincipalId` methods.
 
 The last index, `_expirations`, maps <token expiration datetimes> to <set of
@@ -31,49 +31,16 @@
 - a token changes and needs to be reindexed.
 
 Let's run through some examples and check the data structures as we go.  We'll
-need to start with some setup.  This is identical to the setups in the other
-tex files in this directory, so if you've worked throught this before, skip
-past this code.
+need to start with some setup.
 
     >>> from zope.locking import utility, interfaces, tokens
+    >>> from zope.app.keyreference.interfaces import IKeyReference
     >>> util = utility.TokenUtility()
+    >>> conn.add(util)
     >>> from zope.interface.verify import verifyObject
     >>> verifyObject(interfaces.ITokenUtility, util)
     True
 
-    >>> from zope import interface, component
-    >>> import zope.app.keyreference.interfaces
-    >>> class IDemo(interface.Interface):
-    ...     """a demonstration interface for a demonstration class"""
-    ...
-    >>> class Demo(object):
-    ...     interface.implements(IDemo)
-    ...
-    >>> class DemoKeyReference(object):
-    ...     component.adapts(IDemo)
-    ...     _class_counter = 0
-    ...     interface.implements(
-    ...         zope.app.keyreference.interfaces.IKeyReference)
-    ...     def __init__(self, context):
-    ...         self.context = context
-    ...         class_ = type(self)
-    ...         self._id = getattr(context, '__demo_key_reference__', None)
-    ...         if self._id is None:
-    ...             self._id = class_._class_counter
-    ...             context.__demo_key_reference__ = self._id
-    ...             class_._class_counter += 1
-    ...     key_type_id = 'zope.locking.README.DemoKeyReference'
-    ...     def __call__(self):
-    ...         return self.context
-    ...     def __hash__(self):
-    ...         return (self.key_type_id, self._id)
-    ...     def __cmp__(self, other):
-    ...         if self.key_type_id == other.key_type_id:
-    ...             return cmp(self._id, other._id)
-    ...         return cmp(self.key_type_id, other.key_type_id) 
-    ...
-    >>> component.provideAdapter(DemoKeyReference)
-
     >>> import datetime
     >>> import pytz
     >>> before_creation = datetime.datetime.now(pytz.utc)
@@ -199,8 +166,7 @@
 
     >>> len(util._locks)
     2
-    >>> token, principals, expiration = util._locks[
-    ...     zope.app.keyreference.interfaces.IKeyReference(frozen)]
+    >>> token, principals, expiration = util._locks[IKeyReference(frozen)]
     >>> token is freeze
     True
     >>> len(principals)
@@ -240,8 +206,7 @@
 
     >>> len(util._locks)
     2
-    >>> token, principals, expiration = util._locks[
-    ...     zope.app.keyreference.interfaces.IKeyReference(demo)]
+    >>> token, principals, expiration = util._locks[IKeyReference(demo)]
     >>> token is lock
     True
     >>> sorted(principals)
@@ -264,9 +229,8 @@
 
 Now all the indexes should have removed the references to the old lock.
 
-    >>> sorted(util._locks) == sorted((
-    ...         zope.app.keyreference.interfaces.IKeyReference(frozen),
-    ...         zope.app.keyreference.interfaces.IKeyReference(another_demo)))
+    >>> sorted(util._locks) == sorted((IKeyReference(frozen),
+    ...                                IKeyReference(another_demo)))
     True
     >>> sorted(util._principal_ids)
     ['john']
@@ -289,7 +253,7 @@
     >>> len(util._locks)
     2
     >>> token, principals, expiration = util._locks[
-    ...     zope.app.keyreference.interfaces.IKeyReference(another_demo)]
+    ...     IKeyReference(another_demo)]
     >>> token is lock
     True
     >>> sorted(principals)
@@ -311,7 +275,7 @@
     >>> len(util._locks)
     2
     >>> token, principals, expiration = util._locks[
-    ...     zope.app.keyreference.interfaces.IKeyReference(another_demo)]
+    ...     IKeyReference(another_demo)]
     >>> token is new_lock
     True
     >>> sorted(principals)
@@ -366,4 +330,126 @@
     >>> len(util._expirations)
     0
 
+
+Demo
+----
+
+The following is a regression test for a bug which prevented the token
+utility from cleaning up expired tokens correctly; perhaps it is also a
+somewhat more realistic demonstration of some interactions with the utility
+in that it uses multiple connections to the database.
+
+    >>> offset = NO_TIME
+    >>> import persistent
+    >>> import transaction
+
+    >>> def populate(principal, conn, duration=None, n=100):
+    ...   """Add n tokens for principal to the db using conn as the connection
+    ...      to the db.
+    ...   """
+    ...   t = conn.transaction_manager.begin()
+    ...   util = token_util(conn)
+    ...   for i in range(n):
+    ...     obj = persistent.Persistent()
+    ...     conn.add(obj)
+    ...     lock = tokens.ExclusiveLock(obj, principal, duration=duration)
+    ...     ignored = util.register(lock)
+    ...   t.commit()
+    >>> def end(principal, conn, n=None):
+    ...   """End n tokens for the given principal using conn as the connection
+    ...      to the db.
+    ...   """
+    ...   t = conn.transaction_manager.begin()
+    ...   locks = list(token_util(conn).iterForPrincipalId(principal))
+    ...   res = len(map(lambda l: l.end(), locks[:n]))
+    ...   t.commit()
+    ...   return res
+    >>> def get_locks(principal, conn):
+    ...   """Retrieves a list of locks for the principal using conn as the
+    ...      connection to the db.
+    ...   """
+    ...   t = conn.transaction_manager.begin()
+    ...   try:
+    ...     return list(token_util(conn)._principal_ids[principal])
+    ...   except KeyError:
+    ...     return []
+
+    >>> tm1 = transaction.TransactionManager()
+    >>> tm2 = transaction.TransactionManager()
+
+    >>> conn1 = db.open(transaction_manager=tm1)
+    >>> conn2 = db.open(transaction_manager=tm2)
+
+We "install" the token utility.
+
+    >>> conn1.root()['token_util'] = zope.locking.utility.TokenUtility()
+    >>> token_util = lambda conn: conn.root()['token_util']
+    >>> tm1.commit()
+
+First, we fill the token utility with 100 locks through connection 1
+under the principal id of 'Dwight Holly'.
+
+    >>> populate('Dwight Holly', conn1)
+
+Via connection 2, we end 50 of Dwight's locks.
+
+    >>> n = end('Dwight Holly', conn2, 50)
+
+In connection 1, we verify that 50 locks have been removed.
+
+    >>> len(get_locks('Dwight Holly', conn1)) == 100 - n
+    True
+
+Now we end the rest of the locks through connection 2.
+
+    >>> ignored = end('Dwight Holly', conn2)
+
+And verify through connection 1 that Dwight now has no locks in the utility.
+
+    >>> get_locks('Dwight Holly', conn1) == []
+    True
+    >>> 'Dwight Holly' in token_util(conn1)._principal_ids
+    False
+
+Dwight gets 100 more locks through connection 1, however this time they are
+all set to expire in 10 minutes.
+
+    >>> populate('Dwight Holly', conn1, duration=datetime.timedelta(minutes=10))
+
+We sync connection 2 so we can see that the locks are indeed there.
+
+    >>> conn2.sync()
+    >>> util = token_util(conn2)
+    >>> 'Dwight Holly' in util._principal_ids
+    True
+    >>> len(util._expirations) > 0
+    True
+
+Now we time-travel one hour into the future, where Dwight's locks have long
+since expired.
+
+    >>> offset = ONE_HOUR
+
+Adding a new lock through connection 2 will trigger a cleanup...
+
+    >>> populate('Pete Bondurant', conn2)
+
+...at which point we can see via connection 1 that all of Dwight's locks
+are gone.
+
+    >>> conn1.sync()
+    >>> util = token_util(conn1)
+    >>> len(util._expirations)
+    0
+    >>> 'Dwight Holly' in util._principal_ids
+    False
+
+    >>> conn1.close()
+    >>> conn2.close()
+
+
+
+Clean Up
+--------
+
     >>> zope.locking.utils.now = oldNow # undo the time hack

Modified: zope.locking/trunk/src/zope/locking/configure.zcml
===================================================================
--- zope.locking/trunk/src/zope/locking/configure.zcml	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/configure.zcml	2009-11-23 21:06:25 UTC (rev 105969)
@@ -11,7 +11,7 @@
   <!-- the annotations mapping -->
   <class class=".tokens.AnnotationsMapping">
     <require permission="zope.View"
-      interface="zope.interface.common.mapping.IEnumerableMapping" 
+      interface="zope.interface.common.mapping.IEnumerableMapping"
       />
   </class>
 
@@ -20,7 +20,7 @@
     <require permission="zope.View"
       attributes="context utility principal_ids started annotations" />
   </class>
-  
+
   <class class=".tokens.ExclusiveLock">
     <require like_class=".tokens.Freeze" />
     <require permission="zope.View"
@@ -29,17 +29,17 @@
       attributes="end"
       set_attributes="expiration duration remaining_duration" />
   </class>
-  
+
   <class class=".tokens.SharedLock">
     <require like_class=".tokens.ExclusiveLock" />
     <require permission="zope.Security"
       attributes="add remove" />
   </class>
-  
+
   <class class=".tokens.EndableFreeze">
     <require like_class=".tokens.ExclusiveLock" />
   </class>
-  
+
   <!-- token broker -->
   <class class=".adapters.TokenBroker">
     <require permission="zope.ManageContent"
@@ -49,9 +49,9 @@
     <require permission="zope.Security"
       attributes="freeze" />
   </class>
-  
+
   <adapter factory=".adapters.TokenBroker" trusted="1" />
-  
+
   <!-- token handlers -->
   <class class=".adapters.ExclusiveLockHandler">
     <require permission="zope.View"
@@ -63,20 +63,26 @@
     <require permission="zope.Public" attributes="release"
       set_attributes="expiration duration remaining_duration" />
   </class>
-  
+
   <adapter factory=".adapters.ExclusiveLockHandler" trusted="1" />
-  
+
   <class class=".adapters.SharedLockHandler">
     <require like_class=".adapters.ExclusiveLockHandler" />
     <require permission="zope.Security" attributes="join" />
     <!-- add has its own security policy -->
     <require permission="zope.Public" attributes="add" />
   </class>
-  
+
   <adapter factory=".adapters.SharedLockHandler" trusted="1" />
 
   <include package=".browser" />
 
+  <utility
+     name="zope.locking"
+     provides="zope.app.generations.interfaces.ISchemaManager"
+     component=".generations.schemaManager"
+    />
+
   <configure
     xmlns:zcml="http://namespaces.zope.org/zcml"
     zcml:condition="have apidoc"

Added: zope.locking/trunk/src/zope/locking/generations.py
===================================================================
--- zope.locking/trunk/src/zope/locking/generations.py	                        (rev 0)
+++ zope.locking/trunk/src/zope/locking/generations.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -0,0 +1,62 @@
+import BTrees.OOBTree
+import zope.interface
+import zope.app.generations.interfaces
+
+import zope.locking.interfaces
+
+class SchemaManager(object):
+
+    zope.interface.implements(
+        zope.app.generations.interfaces.IInstallableSchemaManager)
+
+    minimum_generation = 1
+    generation = 1
+
+    def install(self, context):
+        # Clean up cruft in any existing token utilities.
+        # This is done here because zope.locking didn't have a
+        # schema manager prior to 1.2.
+        app = context.connection.root().get('Application')
+        if app is not None:
+            for util in find_token_utilities(app):
+                fix_token_utility(util)
+
+    def evolve(self, context, generation):
+        pass
+
+
+schemaManager = SchemaManager()
+
+def get_site_managers(app_root):
+    def _get_site_managers(sm):
+        yield sm
+        for sm in sm.subs:
+            for _sm in _get_site_managers(sm):
+                yield _sm
+    return _get_site_managers(app_root.getSiteManager())
+
+
+def find_token_utilities(app_root):
+    for sm in get_site_managers(app_root):
+        for registration in sm.registeredUtilities():
+            if registration.provided is zope.locking.interfaces.ITokenUtility:
+                yield registration.component
+
+
+def fix_token_utility(util):
+    """ A bug in versions of zope.locking prior to 1.2 could cause
+        token utilities to keep references to expired/ended locks.
+
+        This function cleans up any old locks lingering in a token
+        utility due to this issue.
+    """
+    for pid in list(util._principal_ids):
+        # iterForPrincipalId only returns non-ended locks, so we know
+        # they're still good.
+        new_tree = BTrees.OOBTree.OOTreeSet(util.iterForPrincipalId(pid))
+        if new_tree:
+            util._principal_ids[pid] = new_tree
+        else:
+            del util._principal_ids[pid]
+    for dt, tree in list(util._expirations.items()):
+        util._expirations[dt] = BTrees.OOBTree.OOTreeSet(tree)


Property changes on: zope.locking/trunk/src/zope/locking/generations.py
___________________________________________________________________
Added: svn:eol-style
   + native

Modified: zope.locking/trunk/src/zope/locking/interfaces.py
===================================================================
--- zope.locking/trunk/src/zope/locking/interfaces.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/interfaces.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -18,9 +18,7 @@
 """
 from zope import interface, schema
 
-from zope.component.interfaces import IObjectEvent
-from zope.interface.common.mapping import IMapping
-from zope.component.interfaces import ObjectEvent
+from zope.component.interfaces import IObjectEvent, ObjectEvent
 from zope.locking.i18n import _
 
 ##############################################################################
@@ -33,7 +31,7 @@
 
     def get(obj, default=None):
         """For obj, return active IToken or default.
-        
+
         Token must be active (not ended), or else return default.
         """
 
@@ -47,11 +45,11 @@
 
     def register(token):
         """register an IToken, or a change to a previously-registered token.
-        
+
         If the token has not yet been assigned a `utility` value, sets the
         `utility` attribute of the token to self, to mark registration.
         Raises ValueError if token has been registered to another utility.
-        
+
         If lock has never been registered before, fires TokenStartedEvent.
         """
 
@@ -61,7 +59,7 @@
 
 class IAbstractToken(interface.Interface):
     """A token.  Must be registered with token utility to start.
-    
+
     This is the core token interface.  This core interface is mostly readonly.
     It is used as a base by both tokens and token handlers.
     """
@@ -74,7 +72,7 @@
 
     utility = interface.Attribute(
         """The lock utility in charge of this lock.
-        
+
         Should *only* ever be set once by ILockUtility.register method.
         When the utility sets this attribute, the `start` attribute should
         be set and the token should be considered active (potentially; see
@@ -93,7 +91,7 @@
 
 class IEndable(interface.Interface):
     """A mixin for tokens that may be ended explicitly or timed out.
-    
+
     Some tokens are endable; locks, for instance, are endable.  Freezes may be
     permanent, so some are not IEndable.
     """
@@ -130,7 +128,7 @@
 
     def end():
         """explicitly expire the token.
-        
+
         fires TokenEndedEvent if successful, or raises EndedError
         if the token has already ended."""
 
@@ -142,7 +140,7 @@
 
 class IToken(IAbstractToken):
     """a token that actually stores data.
-    
+
     This is the sort of token that should be used in the token utility."""
 
     __parent__ = interface.Attribute(
@@ -193,7 +191,7 @@
 
 class IExclusiveLock(IEndableToken):
     """a lock held to one and only one principal.
-    
+
     principal_ids must always have one and only one member."""
 
 class ISharedLock(IEndableToken):
@@ -201,25 +199,25 @@
 
     def add(principal_ids):
         """Share this lock with principal_ids.
-        
+
         Adding principals that already are part of the lock can be ignored.
-        
+
         If ended, raise EndedError.
         """
 
     def remove(principal_ids):
         """Remove principal_ids from lock.
-        
+
         Removing all principals removes the lock: there may not be an effective
-        shared lock shared to noone.
-        
+        shared lock shared to no one.
+
         Removing principals that are not part of the lock can be ignored.
-        
+
         If ended, raise EndedError."""
 
 class IFreeze(IToken):
     """principal_ids must always be empty.
-    
+
     May not be ended."""
 
 class IEndableFreeze(IFreeze, IEndableToken):
@@ -231,7 +229,7 @@
 
 class ITokenBroker(interface.Interface):
     """for one object, create standard endable tokens and get active ITokens.
-    
+
     Convenient adapter model for security: broker is in context of affected
     object, so security settings for the object can be obtained automatically.
     """
@@ -244,25 +242,25 @@
 
     def lock(principal_id=None, duration=None):
         """lock context, and return token.
-        
+
         if principal_id is None, use interaction's principal; if interaction
         does not have one and only one principal, raise ValueError.
-        
+
         if principal_id is not None, principal_id must be in interaction,
         or else raise ParticipationError.
-        
+
         Same constraints as token utility's register method.
         """
 
     def lockShared(principal_ids=None, duration=None):
         """lock context with a shared lock, and return token.
-        
+
         if principal_ids is None, use interaction's principals; if interaction
         does not have any principals, raise ValueError.
-        
+
         if principal_ids is not None, principal_ids must be in interaction,
         or else raise ParticipationError.  Must be at least one id.
-        
+
         Same constraints as token utility's register method.
         """
 
@@ -282,9 +280,9 @@
 
 class ITokenHandler(IAbstractToken, IEndable):
     """give appropriate increased access in a security system.
-    
+
     Appropriate for endable tokens with one or more principals (for instance,
-    niether freezes nor endable freezes."""
+    neither freezes nor endable freezes."""
 
     __parent__ = interface.Attribute(
         """the actual token.  readonly.  Important for security.""")
@@ -332,9 +330,9 @@
         All explicitly given principal_ids must be in interaction.  Silently
         ignores requests to remove principals who are not currently part of
         token.
-        
+
         Ends the lock if the removed principals were the only principals.
-        
+
         Raises EndedError if lock has already ended.
         """
 
@@ -351,7 +349,7 @@
         All explicitly given principal_ids must be in interaction.  Silently
         ignores requests to add principal_ids that are already part of the
         token.
-        
+
         Raises EndedError if lock has already ended.
         """
 
@@ -371,10 +369,10 @@
 
 class ITokenStartedEvent(ITokenEvent):
     """An token has started"""
-    
+
 class ITokenEndedEvent(ITokenEvent):
     """A token has been explicitly ended.
-    
+
     Note that this is not fired when a lock expires."""
 
 class IPrincipalsChangedEvent(ITokenEvent):
@@ -384,7 +382,7 @@
 
 class IExpirationChangedEvent(ITokenEvent):
     """Expiration value changed for a token"""
-    
+
     old = interface.Attribute('the old expiration value')
 
 # events

Added: zope.locking/trunk/src/zope/locking/testing.py
===================================================================
--- zope.locking/trunk/src/zope/locking/testing.py	                        (rev 0)
+++ zope.locking/trunk/src/zope/locking/testing.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -0,0 +1,37 @@
+import zope.component
+import zope.interface
+import zope.app.keyreference.interfaces
+
+class IDemo(zope.interface.Interface):
+    """a demonstration interface for a demonstration class"""
+
+class Demo(object):
+    zope.interface.implements(IDemo)
+
+class DemoKeyReference(object):
+    zope.interface.implements(zope.app.keyreference.interfaces.IKeyReference)
+    zope.component.adapts(IDemo)
+
+    _class_counter = 0
+    key_type_id = 'zope.locking.testing.DemoKeyReference'
+
+    def __init__(self, context):
+        self.context = context
+        class_ = type(self)
+        self._id = getattr(context, '__demo_key_reference__', None)
+        if self._id is None:
+            self._id = class_._class_counter
+            context.__demo_key_reference__ = self._id
+            class_._class_counter += 1
+
+    def __call__(self):
+        return self.context
+
+    def __hash__(self):
+        return (self.key_type_id, self._id)
+
+    def __cmp__(self, other):
+        if self.key_type_id == other.key_type_id:
+            return cmp(self._id, other._id)
+        return cmp(self.key_type_id, other.key_type_id)
+


Property changes on: zope.locking/trunk/src/zope/locking/testing.py
___________________________________________________________________
Added: svn:eol-style
   + native

Modified: zope.locking/trunk/src/zope/locking/tests.py
===================================================================
--- zope.locking/trunk/src/zope/locking/tests.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/tests.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -1,17 +1,39 @@
 import unittest
-from zope.app.testing import placelesssetup
-from zope.testing import doctest 
 
+import persistent.interfaces
+import ZODB.DB
+import ZODB.MappingStorage
+import transaction
+import zope.app.keyreference.interfaces
+import zope.app.keyreference.persistent
+import zope.app.testing.placelesssetup
+import zope.component
+import zope.event
+
+import zope.locking.testing
+
+from zope.testing import doctest
+
+
 def setUp(test):
-    placelesssetup.setUp(test)
+    zope.app.testing.placelesssetup.setUp(test)
+    db = test.globs['db'] = ZODB.DB(ZODB.MappingStorage.MappingStorage())
+    test.globs['conn'] = db.open()
+    test.globs['Demo'] = zope.locking.testing.Demo
+    zope.component.provideAdapter(zope.locking.testing.DemoKeyReference)
+    zope.component.provideAdapter(
+        zope.app.keyreference.persistent.KeyReferenceToPersistent,
+        [persistent.interfaces.IPersistent],
+        zope.app.keyreference.interfaces.IKeyReference)
     events = test.globs['events'] = []
-    import zope.event
     zope.event.subscribers.append(events.append)
 
 def tearDown(test):
-    placelesssetup.tearDown(test)
+    zope.app.testing.placelesssetup.tearDown(test)
+    transaction.abort()
+    test.globs['conn'].close()
+    test.globs['db'].close()
     events = test.globs.pop('events')
-    import zope.event
     assert zope.event.subscribers.pop().__self__ is events
     del events[:] # being paranoid
 
@@ -29,4 +51,4 @@
         ))
 
 if __name__ == '__main__':
-    unittest.main(defaultTest='test_suite') 
+    unittest.main(defaultTest='test_suite')

Modified: zope.locking/trunk/src/zope/locking/tokens.py
===================================================================
--- zope.locking/trunk/src/zope/locking/tokens.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/tokens.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -47,6 +47,10 @@
                 self._started = utils.now()
         return property(get, set)
 
+    def __cmp__(self, other):
+        return cmp((self._p_jar.db().database_name, self._p_oid),
+            (other._p_jar.db().database_name, other._p_oid))
+
 class EndableToken(Token):
 
     def __init__(self, target, duration=None):
@@ -172,14 +176,14 @@
 
 class ExclusiveLock(EndableToken):
     interface.implements(interfaces.IExclusiveLock)
-    
+
     def __init__(self, target, principal_id, duration=None):
         self._principal_ids = frozenset((principal_id,))
         super(ExclusiveLock, self).__init__(target, duration)
 
 class SharedLock(EndableToken):
     interface.implements(interfaces.ISharedLock)
-    
+
     def __init__(self, target, principal_ids, duration=None):
         self._principal_ids = frozenset(principal_ids)
         super(SharedLock, self).__init__(target, duration)

Modified: zope.locking/trunk/src/zope/locking/utility.py
===================================================================
--- zope.locking/trunk/src/zope/locking/utility.py	2009-11-23 15:26:14 UTC (rev 105968)
+++ zope.locking/trunk/src/zope/locking/utility.py	2009-11-23 21:06:25 UTC (rev 105969)
@@ -1,4 +1,5 @@
 import persistent
+import persistent.interfaces
 
 from BTrees.OOBTree import OOBTree, OOTreeSet
 from zope import interface, component, event
@@ -20,15 +21,10 @@
 
     def _del(self, tree, token, value):
         """remove a token for a value within either of the two index trees"""
-        reg = tree.get(value)
-        if reg is not None:
-            try:
-                reg.remove(token)
-            except KeyError:
-                pass
-            else:
-                if not len(reg):
-                    del tree[value]
+        reg = tree[value]
+        reg.remove(token)
+        if not reg:
+            del tree[value]
 
     def _add(self, tree, token, value):
         """add a token for a value within either of the two index trees"""
@@ -57,6 +53,8 @@
             token.utility = self
         elif token.utility is not self:
             raise ValueError('Lock is already registered with another utility')
+        if persistent.interfaces.IPersistent.providedBy(token):
+            self._p_jar.add(token)
         key_ref = IKeyReference(token.context)
         current = self._locks.get(key_ref)
         if current is not None:



More information about the checkins mailing list