[Zope-CVS] CVS: Products/Transience - Transience.py:1.3

Matthew T. Kromer matt@zope.com
Mon, 22 Oct 2001 14:48:18 -0400


Update of /cvs-repository/Products/Transience
In directory cvs.zope.org:/tmp/cvs-serv29147

Modified Files:
	Transience.py 
Log Message:
Updated with more code from the original CoreSessionTracking SessionDataManager
for object expiry.


=== Products/Transience/Transience.py 1.2 => 1.3 ===
 from Acquisition import Implicit, aq_base
 from AccessControl import ClassSecurityInfo
+from BTrees import OOBTree
 import os.path
+import math
 import time
 
 _notfound = []
+_marker = []
 
 # permissions
 ADD_DATAMGR_PERM = 'Add Transient Object Container'
 CHANGE_DATAMGR_PERM = 'Change Transient Object Containers'
 MGMT_SCREEN_PERM = 'View management screens'
 ACCESS_CONTENTS_PERM = 'Access contents information'
+CREATE_TRANSIENTS_PERM = 'Create Transient Objects'
 ACCESS_SESSIONDATA_PERM = 'Access Transient Objects'
 MANAGE_CONTAINER_PERM = 'Manage Transient Object Container'
 
@@ -165,6 +169,8 @@
                                 ['Manager','Anonymous'])
     security.setPermissionDefault(ACCESS_SESSIONDATA_PERM,
                                 ['Manager','Anonymous'])
+    security.setPermissionDefault(CREATE_TRANSIENTS_PERM,
+                                ['Manager',])
 
     security.declareProtected(MGMT_SCREEN_PERM, 'manage_container')
     manage_container = HTMLFile('dtml/manageTransientObjectContainer',
@@ -180,17 +186,23 @@
     #
 
     def __init__(self, id, title='', timeout_mins=20, addNotification=None,
-        delNotification=None):
+        delNotification=None, err_margin=.20, ctype=OOBTree.OOBTree):
+
 
         self.id = id
         self.title=title
-        self._container = {}
+
+        self._ctype = ctype
+
         self._addCallback = None
         self._delCallback = None
-        self.setTimeoutMinutes(timeout_mins)
+        self._err_margin = err_margin
+
+        self._setTimeout(timeout_mins)
+        self._reset()
 
-        self.setAddNotificationTarget(addNotification)
         self.setDelNotificationTarget(delNotification)
+        self.setAddNotificationTarget(addNotification)
 
     # -----------------------------------------------------------------
     # ItemWithID
@@ -204,24 +216,28 @@
     # StringKeyedHomogenousItemContainer
     #
 
+    security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
     def new(self, key):
 
         if type(key) is not type(''):
             raise TypeError, (key, "key is not a string type")
     
-        if self._container.has_key(key):
+        if self.has_key(key):
             raise KeyError, key         # Not allowed to dup keys
         
         item = TransientObject(key, parent=self)
 
-        self._container[key] = item
+        self[key] = item
+
+        self.notifyAdd(item)
 
         return item
         
 
+    security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing')
     def new_or_existing(self, key):
 
-        item  = self._container.get(key,_notfound)
+        item  = self.get(key,_notfound)
         if item is not _notfound: return item
 
         return self.new(key)
@@ -232,11 +248,15 @@
 
     security.declareProtected(MANAGE_CONTAINER_PERM, 'setTimeoutMinutes')
     def setTimeoutMinutes(self, timeout_mins):
-        self._timeout = timeout_mins
+        """ """
+        if timeout_mins != self.getTimeoutMinutes():
+            self._setTimeout(timeout_mins)
+            self._reset()
 
     security.declareProtected(MGMT_SCREEN_PERM, 'getTimeoutMinutes')
     def getTimeoutMinutes(self):
-        return self._timeout
+        """ """
+        return self._timeout_secs / 60
 
     security.declareProtected(MGMT_SCREEN_PERM, 'getAddNotificationTarget')
     def getAddNotificationTarget(self):
@@ -279,16 +299,6 @@
     #
 
 
-    security.declareProtected(MGMT_SCREEN_PERM, 'getLen')
-    def getLen(self):
-
-        """
-        Potentially expensive helper function to figure out how
-        many items are contained.
-        """
-        return len(self._container)
-
-
     security.declareProtected(MANAGE_CONTAINER_PERM,
         'manage_changeTransientObjectContainer')
     def manage_changeTransientObjectContainer(self, title='',
@@ -318,7 +328,7 @@
 
         f = os.path.join(Globals.data_dir, "transientobjects.zexp")
         self.c = PersistentMapping()
-        for k, v in self._container.items():
+        for k, v in self.items():
             self.c[k] = v
 
         get_transaction().commit()
@@ -340,12 +350,217 @@
         conn = self._p_jar
         ob = conn.importFile(f)
         for k,v in ob.items():
-            self._container[k] = v
+            self[k] = v
         if REQUEST is not None:
             return MessageDialog(
                 title="Transient objects imported",
                 message="Transient objects imported from %s" % f,
                 action="manage_container")
+
+    def _setTimeout(self, timeout_mins):
+        if type(timeout_mins) is not type(1):
+            raise TypeError, (timeout_mins, "Must be integer")
+        self._timeout_secs = timeout_mins * 60
+
+    def _reset(self):
+        t_secs = self._timeout_secs
+        r_secs = self._resolution_secs = int(t_secs * self._err_margin) or 1
+        numbuckets = int(math.floor(t_secs/r_secs)) or 1
+        l = []
+        i = 0
+        now = int(time.time())
+        for x in range(numbuckets):
+            dump_after = now + i
+            c = self._ctype()
+            l.insert(0, [c, dump_after])
+            i = i + r_secs
+        index = self._ctype()
+        self._ring = Ring(l, index)
+
+
+    def _getCurrentBucket(self, get_dump=0):
+        # no timeout always returns last bucket
+        if not self._timeout_secs:
+            b, dump_after = self._ring._data[0]
+            return b
+        index = self._ring._index
+        now = int(time.time())
+        i = self._timeout_secs
+        # expire all buckets in the ring which have a dump_after time that
+        # is before now, turning the ring as many turns as necessary to
+        # get to a non-expirable bucket.
+        while 1:
+            l = b, dump_after = self._ring._data[-1]
+            if now > dump_after:
+                self._ring.turn()
+                # mutate elements in-place in the ring
+                new_dump_after = now + i
+                l[1] = new_dump_after
+                self._clean(b, index)
+                i = i + self._resolution_secs
+            else:
+                break
+        if get_dump:
+            return self._ring._data[0], dump_after, now
+        else:
+            b, dump_after = self._ring._data[0]
+            return b
+
+    def _clean(self, b, index):
+
+
+        # What is all this?
+        #for ob in b.values():
+        #    d = last = None
+        #    f = getattr(ob, self._onend, None)
+        #    #
+        #    # HUH?
+        #    #
+        #    getDataMgr = getattr(ob, 'getDataMgr', None)
+        #    if getDataMgr is not None:
+        #        if callable(getDataMgr):
+        #            d = getDataMgr()
+        #        if d != last:
+        #            mgr = self.aq_parent.unrestrictedTraverse(d)
+        #            last = d
+        #    if callable(f): f(mgr)
+
+        for k, v in list(index.items()):
+            if v is b:
+                self.notifyDestruct(index[k])
+                del index[k]
+        b.clear()
+
+
+    def _show(self):
+        """ debug method """
+        b,dump,now = self._getCurrentBucket(1)
+        ringdumps = map(lambda x: `x[1]`[-4], self._ring)
+        t = (
+            "now: "+`now`[-4:],
+            "dump_after: "+`dump`[-4:],
+            "ring_dumps: "+`ringdumps`,
+            "ring: " + `self._ring`
+             )
+
+        for x in t:
+            print x
+
+
+    def __setitem__(self, k, v):
+        current = self._getCurrentBucket()
+        index = self._ring._index
+        b = index.get(k)
+        if b is None:
+            # this is a new key
+            index[k] = current
+        elif b is not current:
+            # this is an old key that isn't in the current bucket.
+            del b[k] # delete it from the old bucket
+            index[k] = current
+        # change the value
+        current[k] = v
+        
+    def __getitem__(self, k):
+        current = self._getCurrentBucket()
+        index = self._ring._index
+        # the next line will raise the proper error if the item has expired
+        b = index[k]
+        v = b[k] # grab the value before we potentially time it out.
+        if b is not current:
+            # it's not optimal to do writes in getitem, but there's no choice.
+            # we accessed the object, so it should become current.
+            index[k] = current # change the index to the current bucket.
+            current[k] = v # add the value to the current bucket.
+            del b[k] # delete the item from the old bucket.
+        return v
+
+    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'get')
+    def set(self, k, v):
+        """ """
+        if type(k) is not type(''):
+            raise TypeError, "Transient Object Container keys must be strings"
+        self[k] = v
+
+    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'get')
+    # Uses a different marker than _notfound
+    def get(self, k, default=_marker):
+        try: v = self[k]
+        except KeyError: v = _marker
+        if v is _marker:
+            if default is _marker:
+                return None
+            else:
+                return default
+        return v
+        
+    def __delitem__(self, k):
+        self._getCurrentBucket()
+        index = self._ring._index
+        b = index[k]
+        del index[k]
+        del b[k]
+
+    security.declareProtected(ACCESS_SESSIONDATA_PERM, '__len__')
+    def __len__(self):
+        self._getCurrentBucket()
+        return len(self._ring._index)
+
+    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'has_key')
+    def has_key(self, k):
+        self._getCurrentBucket()
+        index = self._ring._index
+        return index.get(k, _notfound) is not _notfound
+
+    def values(self):
+        return map(lambda k, self=self: self[k], self.keys())
+
+    def items(self):
+        return map(lambda k, self=self: (k, self[k]), self.keys())
+
+    def keys(self):
+        self._getCurrentBucket()
+        index = self._ring._index
+        return map(lambda x: x, index.keys())
+
+    def update(self):
+        raise NotImplementedError
+
+    def clear(self):
+        raise NotImplementedError
+
+    def copy(self):
+        raise NotImplementedError
+
+    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'getLen')
+    getLen = __len__
+    
+class Ring(Persistent):
+    """ Instances of this class will be frequently written to the ZODB,
+    so it's optimized as best possible for write-friendliness """
+    def __init__(self, l, index):
+        if not len(l):
+            raise "ring must have at least one element"
+        self._data = l
+        self._index = index
+
+    def __repr__(self):
+        return repr(self._data)
+
+    def __len__(self):
+        return len(self._data)
+
+    def __getitem__(self, i):
+        return self._data[i]
+
+    def turn(self):
+        last = self._data.pop(-1)
+        self._data.insert(0, last)
+        self._p_changed = 1
+
+    def _p_independent(self):
+        return 1
+
 
 
 class TransientObject(Persistent, Implicit):