Commit dec36cac authored by Chris McDonough's avatar Chris McDonough

Moved TransientObjects into their own module.

Removed wrap_with argument from new and new_or_existing methods
of Transient Data Containers.

Removed delete method of Transient Data Containers.

Added out-of-memory protection to Transient Data Containers.  A
  new __init__ value ('limit') is used to specify the max number
  of objects that can be contained within a transient data container.
  A new envvar ZSESSION_OBJECT_LIMIT can be used to control the
  limit of the default session_data TDC.  Also updated help and
  API docs with this change.

Added a new exception, MaxTransientObjectsExceeded, which is raised
  when the OOM protection kicks in.

Various implementation changes including the use of a BTrees Length
  object to store Transient Data Container length info as well
  as improvements to how buckets are expired.

Addition of tests for OOM protection fatures.
parent 4ba39f69
...@@ -83,49 +83,42 @@ ...@@ -83,49 +83,42 @@
# #
############################################################################## ##############################################################################
""" """
Core session tracking SessionData class. Transient Object Container class.
$Id: Transience.py,v 1.20 2001/11/20 15:29:23 chrism Exp $ $Id: Transience.py,v 1.21 2001/11/21 22:46:36 chrism Exp $
""" """
__version__='$Revision: 1.20 $'[11:-2] __version__='$Revision: 1.21 $'[11:-2]
import Globals import Globals
from Globals import HTMLFile, MessageDialog from Globals import HTMLFile, MessageDialog
from TransienceInterfaces import Transient, DictionaryLike, ItemWithId,\ from TransienceInterfaces import ItemWithId,\
TTWDictionary, ImmutablyValuedMappingOfPickleableObjects,\
StringKeyedHomogeneousItemContainer, TransientItemContainer StringKeyedHomogeneousItemContainer, TransientItemContainer
from TransientObject import TransientObject
from OFS.SimpleItem import SimpleItem from OFS.SimpleItem import SimpleItem
from Persistence import Persistent, PersistentMapping from Persistence import Persistent
from Acquisition import Implicit, aq_base
from AccessControl import ClassSecurityInfo, getSecurityManager from AccessControl import ClassSecurityInfo, getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
import AccessControl.SpecialUsers import AccessControl.SpecialUsers
from AccessControl.User import nobody from AccessControl.User import nobody
from BTrees import OOBTree from BTrees import OOBTree
from BTrees.Length import Length
from zLOG import LOG, WARNING, BLATHER from zLOG import LOG, WARNING, BLATHER
import os import os, os.path, math, time, sys, random
import os.path
import math
import time
import sys
import random
from types import InstanceType
DEBUG = os.environ.get('Z_TOC_DEBUG', '') DEBUG = os.environ.get('Z_TOC_DEBUG', '')
def TLOG(*args): def DLOG(*args):
tmp = [] tmp = []
for arg in args: for arg in args:
tmp.append(str(arg)) tmp.append(str(arg))
LOG('Transience DEBUG', BLATHER, ' '.join(tmp)) LOG('Transience DEBUG', BLATHER, ' '.join(tmp))
class MaxTransientObjectsExceeded(Exception): pass
_notfound = [] _notfound = []
_marker = [] _marker = []
WRITEGRANULARITY=30 # Timing granularity for write clustering, in seconds
time = time.time
# permissions # permissions
ADD_CONTAINER_PERM = 'Add Transient Object Container' ADD_CONTAINER_PERM = 'Add Transient Object Container'
MGMT_SCREEN_PERM = 'View management screens' MGMT_SCREEN_PERM = 'View management screens'
...@@ -137,82 +130,61 @@ MANAGE_CONTAINER_PERM = 'Manage Transient Object Container' ...@@ -137,82 +130,61 @@ MANAGE_CONTAINER_PERM = 'Manage Transient Object Container'
constructTransientObjectContainerForm = HTMLFile( constructTransientObjectContainerForm = HTMLFile(
'dtml/addTransientObjectContainer', globals()) 'dtml/addTransientObjectContainer', globals())
def constructTransientObjectContainer(self, id, title='', timeout_mins=20, def constructTransientObjectContainer(self, id, title='', timeout_mins=20,
addNotification=None, delNotification=None, addNotification=None, delNotification=None, limit=0, REQUEST=None):
REQUEST=None):
""" """ """ """
ob = TransientObjectContainer(id, title, timeout_mins, ob = TransientObjectContainer(
addNotification, delNotification) id, title, timeout_mins, addNotification, delNotification, limit=limit
)
self._setObject(id, ob) self._setObject(id, ob)
if REQUEST is not None: if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1) return self.manage_main(self, REQUEST, update_menu=1)
class TransientObjectContainer(SimpleItem): class TransientObjectContainer(SimpleItem):
""" akin to Session Data Container """ """ Persists objects for a user-settable time period, after which it
expires them """
meta_type = "Transient Object Container" meta_type = "Transient Object Container"
icon = "misc_/Transience/datacontainer.gif" icon = "misc_/Transience/datacontainer.gif"
__implements__ = (ItemWithId, __implements__ = (ItemWithId,
StringKeyedHomogeneousItemContainer, StringKeyedHomogeneousItemContainer,
TransientItemContainer TransientItemContainer
) )
manage_options = ( manage_options = (
{ 'label': 'Manage', { 'label': 'Manage',
'action': 'manage_container', 'action': 'manage_container',
'help': ('Transience', 'Transience.stx') 'help': ('Transience', 'Transience.stx')
}, },
{ 'label': 'Security', { 'label': 'Security',
'action': 'manage_access' 'action': 'manage_access'
}, },
) )
security = ClassSecurityInfo() security = ClassSecurityInfo()
security.setDefaultAccess('deny')
security.setPermissionDefault(MANAGE_CONTAINER_PERM,
['Manager',])
security.setPermissionDefault(MGMT_SCREEN_PERM,
['Manager',])
security.setPermissionDefault(ACCESS_CONTENTS_PERM,
['Manager','Anonymous'])
security.setPermissionDefault(ACCESS_TRANSIENTS_PERM, security.setPermissionDefault(ACCESS_TRANSIENTS_PERM,
['Manager','Anonymous','Sessions']) ['Manager','Anonymous'])
security.setPermissionDefault(CREATE_TRANSIENTS_PERM, security.setPermissionDefault(MANAGE_CONTAINER_PERM,['Manager',])
['Manager',]) security.setPermissionDefault(MGMT_SCREEN_PERM,['Manager',])
security.setPermissionDefault(ACCESS_CONTENTS_PERM,['Manager','Anonymous'])
security.setPermissionDefault(CREATE_TRANSIENTS_PERM,['Manager',])
security.declareProtected(MGMT_SCREEN_PERM, 'manage_container') security.declareProtected(MGMT_SCREEN_PERM, 'manage_container')
manage_container = HTMLFile('dtml/manageTransientObjectContainer', manage_container = HTMLFile('dtml/manageTransientObjectContainer',
globals()) globals())
security.setDefaultAccess('deny') _limit = 0
#
# Initializer
#
def __init__(self, id, title='', timeout_mins=20, addNotification=None, def __init__(self, id, title='', timeout_mins=20, addNotification=None,
delNotification=None, err_margin=.20, ctype=OOBTree.OOBTree): delNotification=None, err_margin=.20, limit=0):
self.id = id self.id = id
self.title=title self.title=title
self._ctype = ctype
self._addCallback = None self._addCallback = None
self._delCallback = None self._delCallback = None
self._err_margin = err_margin self._err_margin = err_margin
self._setTimeout(timeout_mins) self._setTimeout(timeout_mins)
self._setLimit(limit)
self._reset() self._reset()
self.setDelNotificationTarget(delNotification) self.setDelNotificationTarget(delNotification)
self.setAddNotificationTarget(addNotification) self.setAddNotificationTarget(addNotification)
...@@ -228,35 +200,46 @@ class TransientObjectContainer(SimpleItem): ...@@ -228,35 +200,46 @@ class TransientObjectContainer(SimpleItem):
# StringKeyedHomogenousItemContainer # StringKeyedHomogenousItemContainer
# #
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing')
def new_or_existing(self, k):
item = self.get(k, _notfound)
if item is _notfound: return self.new(k)
else: return item
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'get')
def get(self, k, default=_marker):
# Intentionally uses a different marker than _notfound
try:
v = self[k]
except KeyError:
if default is _marker: return None
else: return default
else:
if hasattr(v, 'isValid') and v.isValid():
return v.__of__(self)
elif not hasattr(v, 'isValid'):
return v
else:
del self[k] # item is no longer valid, so we delete it
if default is _marker: return None
else: return default
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new') security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
def new(self, key, wrap_with=None): def new(self, k):
if type(key) is not type(''): if type(k) is not type(''):
raise TypeError, (key, "key is not a string type") raise TypeError, (k, "key is not a string type")
if self.get(key,None) is not None: if self.get(k, None) is not None:
if self[key].isValid(): raise KeyError, "duplicate key %s" % k # Not allowed to dup keys
raise KeyError, key # Not allowed to dup keys item = TransientObject(k)
del self[key] self[k] = item
item = TransientObject(key)
self[key] = item
self.notifyAdd(item) self.notifyAdd(item)
if not wrap_with:
return item.__of__(self) return item.__of__(self)
else:
return item.__of__(wrap_with)
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing') security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key')
def new_or_existing(self, key, wrap_with=None): def has_key(self, k):
item = self.get(key,_notfound) v = self.get(k, _notfound)
if item is _notfound: if v is _notfound: return 0
return self.new(key, wrap_with) return 1
if not item.isValid():
del self[key]
return self.new(key, wrap_with)
if not wrap_with:
return item.__of__(self)
else:
return item.__of__(wrap_with)
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# TransientItemContainer # TransientItemContainer
...@@ -274,15 +257,23 @@ class TransientObjectContainer(SimpleItem): ...@@ -274,15 +257,23 @@ class TransientObjectContainer(SimpleItem):
""" """ """ """
return self._timeout_secs / 60 return self._timeout_secs / 60
security.declareProtected(MANAGE_CONTAINER_PERM, 'setSubobjectLimit')
def setSubobjectLimit(self, limit):
""" """
if limit != self.getSubobjectLimit():
self._setLimit(limit)
security.declareProtected(MGMT_SCREEN_PERM, 'getSubobjectLimit')
def getSubobjectLimit(self):
""" """
return self._limit
security.declareProtected(MGMT_SCREEN_PERM, 'getAddNotificationTarget') security.declareProtected(MGMT_SCREEN_PERM, 'getAddNotificationTarget')
def getAddNotificationTarget(self): def getAddNotificationTarget(self):
return self._addCallback or '' return self._addCallback or ''
security.declareProtected(MANAGE_CONTAINER_PERM, security.declareProtected(MANAGE_CONTAINER_PERM,'setAddNotificationTarget')
'setAddNotificationTarget')
def setAddNotificationTarget(self, f): def setAddNotificationTarget(self, f):
# We should assert that the callback function 'f' implements
# the TransientNotification interface
self._addCallback = f self._addCallback = f
security.declareProtected(MGMT_SCREEN_PERM, 'getDelNotificationTarget') security.declareProtected(MGMT_SCREEN_PERM, 'getDelNotificationTarget')
...@@ -292,12 +283,9 @@ class TransientObjectContainer(SimpleItem): ...@@ -292,12 +283,9 @@ class TransientObjectContainer(SimpleItem):
security.declareProtected(MANAGE_CONTAINER_PERM, security.declareProtected(MANAGE_CONTAINER_PERM,
'setDelNotificationTarget') 'setDelNotificationTarget')
def setDelNotificationTarget(self, f): def setDelNotificationTarget(self, f):
# We should assert that the callback function 'f' implements
# the TransientNotification interface
self._delCallback = f self._delCallback = f
# ----------------------------------------------
#
# Supporting methods (not part of the interface) # Supporting methods (not part of the interface)
# #
...@@ -309,14 +297,16 @@ class TransientObjectContainer(SimpleItem): ...@@ -309,14 +297,16 @@ class TransientObjectContainer(SimpleItem):
if self._delCallback: if self._delCallback:
self._notify(item, 'destruct') self._notify(item, 'destruct')
def _notify(self, item, kind): def _notify(self, items, kind):
if not type(items) in [type([]), type(())]:
items = [items]
if kind =='add': if kind =='add':
name = 'notifyAdd' name = 'notifyAdd'
callback = self._addCallback callback = self._addCallback
else: else:
name = 'notifyDestruct' name = 'notifyDestruct'
callback = self._delCallback callback = self._delCallback
if type(callback) is type(''): if type(callback) is type(''):
try: try:
method = self.unrestrictedTraverse(callback) method = self.unrestrictedTraverse(callback)
...@@ -332,9 +322,8 @@ class TransientObjectContainer(SimpleItem): ...@@ -332,9 +322,8 @@ class TransientObjectContainer(SimpleItem):
else: else:
method = callback method = callback
for item in items:
if callable(method): if callable(method):
if DEBUG:
TLOG('calling %s at object %s' % (callback, kind))
try: try:
user = getSecurityManager().getUser() user = getSecurityManager().getUser()
try: try:
...@@ -343,9 +332,8 @@ class TransientObjectContainer(SimpleItem): ...@@ -343,9 +332,8 @@ class TransientObjectContainer(SimpleItem):
except: except:
# dont raise, just log # dont raise, just log
path = self.getPhysicalPath() path = self.getPhysicalPath()
LOG('Transience', LOG('Transience', WARNING,
WARNING, '%s failed when calling %s in %s' % (name,callback,
'%s failed when calling %s in %s' % (name, callback,
'/'.join(path)), '/'.join(path)),
error=sys.exc_info() error=sys.exc_info()
) )
...@@ -368,14 +356,13 @@ class TransientObjectContainer(SimpleItem): ...@@ -368,14 +356,13 @@ class TransientObjectContainer(SimpleItem):
'manage_changeTransientObjectContainer') 'manage_changeTransientObjectContainer')
def manage_changeTransientObjectContainer(self, title='', def manage_changeTransientObjectContainer(self, title='',
timeout_mins=20, addNotification=None, delNotification=None, timeout_mins=20, addNotification=None, delNotification=None,
REQUEST=None): limit=0, REQUEST=None):
""" """
Change an existing transient object container. Change an existing transient object container.
""" """
self.title = title self.title = title
self.setTimeoutMinutes(timeout_mins) self.setTimeoutMinutes(timeout_mins)
self.setSubobjectLimit(limit)
if not addNotification: if not addNotification:
addNotification = None addNotification = None
if not delNotification: if not delNotification:
...@@ -391,76 +378,101 @@ class TransientObjectContainer(SimpleItem): ...@@ -391,76 +378,101 @@ class TransientObjectContainer(SimpleItem):
raise TypeError, (timeout_mins, "Must be integer") raise TypeError, (timeout_mins, "Must be integer")
self._timeout_secs = timeout_mins * 60 self._timeout_secs = timeout_mins * 60
def _reset(self): def _setLimit(self, limit):
if type(limit) is not type(1):
raise TypeError, (limit, "Must be integer")
self._limit = limit
def _setLastAccessed(self, transientObject):
sla = getattr(transientObject, 'setLastAccessed', None)
if sla is not None: sla()
def _reset(self):
if hasattr(self,'_ring'): if hasattr(self,'_ring'):
for k in self.keys(): for k in self.keys():
self.notifyDestruct(self[k]) try: self.notifyDestruct(self[k])
del self[k] except KeyError: pass
t_secs = self._timeout_secs t_secs = self._timeout_secs
r_secs = self._resolution_secs = int(t_secs * self._err_margin) or 1 r_secs = self._resolution_secs = int(t_secs * self._err_margin) or 1
numbuckets = int(math.floor(t_secs/r_secs)) or 1 numbuckets = int(math.floor(t_secs/r_secs)) or 1
l = [] l = []
i = 0 i = 0
now = int(time()) now = int(time.time())
for x in range(numbuckets): for x in range(numbuckets):
dump_after = now + i dump_after = now + i
c = self._ctype() c = OOBTree.OOBTree()
l.insert(0, [c, dump_after]) l.insert(0, [c, dump_after])
i = i + r_secs i = i + r_secs
index = self._ctype() index = OOBTree.OOBTree()
self._ring = Ring(l, index) self._ring = Ring(l, index)
try: self.__len__.set(0)
except AttributeError: self.__len__ = self.getLen = Length()
def _getCurrentBucket(self, get_dump=0): def _getCurrentBucket(self, get_dump=0):
# no timeout always returns last bucket # no timeout always returns last bucket
if not self._timeout_secs: if not self._timeout_secs:
b, dump_after = self._ring._data[0] b, dump_after = self._ring._data[0]
if DEBUG: if DEBUG:
TLOG('no timeout, returning first bucket') DLOG('no timeout, returning first bucket')
return b return b
index = self._ring._index index = self._ring._index
now = int(time()) now = int(time.time())
i = self._timeout_secs i = self._timeout_secs
# expire all buckets in the ring which have a dump_after time that # 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 # is before now, turning the ring as many turns as necessary to
# get to a non-expirable bucket. # get to a non-expirable bucket.
to_clean = []
while 1: while 1:
l = b, dump_after = self._ring._data[-1] l = b, dump_after = self._ring._data[-1]
if now > dump_after: if now > dump_after:
if DEBUG: if DEBUG:
TLOG('now is %s' % now) DLOG('now is %s' % now)
TLOG('dump_after for %s was %s, dumping'%(b, dump_after)) DLOG('dump_after for %s was %s, dumping'%(b, dump_after))
self._ring.turn() self._ring.turn()
# mutate elements in-place in the ring # mutate elements in-place in the ring
new_dump_after = now + i new_dump_after = now + i
l[1] = new_dump_after l[1] = new_dump_after
self._clean(b, index) if b: to_clean.append(b)# only clean non-empty buckets
i = i + self._resolution_secs i = i + self._resolution_secs
else: else:
break break
if to_clean: self._clean(to_clean, index)
if get_dump: if get_dump:
return self._ring._data[0], dump_after, now return self._ring._data[0], dump_after, now
else: else:
b, dump_after = self._ring._data[0] b, dump_after = self._ring._data[0]
return b return b
def _clean(self, b, index): def _clean(self, bucket_set, index):
if DEBUG: # Build a reverse index. Eventually, I'll keep this in another
TLOG('building list of index items') # persistent struct but I'm afraid of creating more conflicts right
l = list(index.items()) # now. The reverse index is a mapping from bucketref -> OOSet of string
if DEBUG: # keys.
TLOG('done building list of index items, now iterating over them') rindex = {}
tmp = [] for k, v in list(index.items()):
for k, v in l: # listifying above is dumb, but I think there's a btrees bug
if v is b: # that causes plain old index.items to always return a sequence
tmp.append(k) # of even numbers
self.notifyDestruct(index[k][k]) if rindex.get(v, _marker) is _marker: rindex[v]=OOBTree.OOSet([k])
else: rindex[v].insert(k)
if DEBUG: DLOG("rindex", rindex)
trans_obs = [] # sequence of objects that we will eventually finalize
for bucket_to_expire in bucket_set:
keys = rindex.get(bucket_to_expire, [])
if keys and DEBUG: DLOG("deleting")
for k in keys:
if DEBUG: DLOG(k)
trans_obs.append(bucket_to_expire[k])
del index[k] del index[k]
if DEBUG: try: self.__len__.change(-1)
TLOG('deleted %s' % tmp) except AttributeError: pass
TLOG('clearing %s' % b) bucket_to_expire.clear()
b.clear()
# finalize em
self.notifyDestruct(trans_obs)
def _show(self): def _show(self):
""" debug method """ """ debug method """
...@@ -476,6 +488,12 @@ class TransientObjectContainer(SimpleItem): ...@@ -476,6 +488,12 @@ class TransientObjectContainer(SimpleItem):
for x in t: for x in t:
print x print x
security.declareProtected(MGMT_SCREEN_PERM, 'nudge')
def nudge(self):
""" Used by mgmt interface to turn the bucket set each time
a screen is shown """
self._getCurrentBucket()
def __setitem__(self, k, v): def __setitem__(self, k, v):
current = self._getCurrentBucket() current = self._getCurrentBucket()
index = self._ring._index index = self._ring._index
...@@ -483,6 +501,19 @@ class TransientObjectContainer(SimpleItem): ...@@ -483,6 +501,19 @@ class TransientObjectContainer(SimpleItem):
if b is None: if b is None:
# this is a new key # this is a new key
index[k] = current index[k] = current
li = self._limit
# do OOM protection
if li and len(self) >= li:
LOG('Transience', WARNING,
('Transient object container %s max subobjects '
'reached' % self.id)
)
raise MaxTransientObjectsExceeded, (
"%s exceeds maximum number of subobjects %s" % (len(self), li)
)
# do length accounting
try: self.__len__.change(1)
except AttributeError: pass
elif b is not current: elif b is not current:
# this is an old key that isn't in the current bucket. # this is an old key that isn't in the current bucket.
del b[k] # delete it from the old bucket del b[k] # delete it from the old bucket
...@@ -506,31 +537,6 @@ class TransientObjectContainer(SimpleItem): ...@@ -506,31 +537,6 @@ class TransientObjectContainer(SimpleItem):
del b[k] # delete the item from the old bucket. del b[k] # delete the item from the old bucket.
return v return v
def _setLastAccessed(self, transientObject):
# A safety valve; dont try to set the last accessed time if the
# object we were given doesnt support it
sla = getattr(transientObject, 'setLastAccessed', None)
if sla is not None: sla()
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'set')
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_TRANSIENTS_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): def __delitem__(self, k):
self._getCurrentBucket() self._getCurrentBucket()
index = self._ring._index index = self._ring._index
...@@ -540,17 +546,13 @@ class TransientObjectContainer(SimpleItem): ...@@ -540,17 +546,13 @@ class TransientObjectContainer(SimpleItem):
security.declareProtected(ACCESS_TRANSIENTS_PERM, '__len__') security.declareProtected(ACCESS_TRANSIENTS_PERM, '__len__')
def __len__(self): def __len__(self):
""" this won't be called unless we havent run __init__ """
if DEBUG: DLOG('Class __len__ called!')
self._getCurrentBucket() self._getCurrentBucket()
return len(self._ring._index) return len(self._ring._index)
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key') security.declareProtected(ACCESS_TRANSIENTS_PERM, 'getLen')
def has_key(self, k): getLen = __len__
v = self.get(k, _notfound)
if v is _notfound: return 0
# Grr, test suite uses ints all over the place
if (type(v) is InstanceType and issubclass(v.__class__, TransientObject)
and not v.isValid()): return 0
return 1
def values(self): def values(self):
return map(lambda k, self=self: self[k], self.keys()) return map(lambda k, self=self: self[k], self.keys())
...@@ -560,24 +562,10 @@ class TransientObjectContainer(SimpleItem): ...@@ -560,24 +562,10 @@ class TransientObjectContainer(SimpleItem):
def keys(self): def keys(self):
self._getCurrentBucket() self._getCurrentBucket()
index = self._ring._index return list(self._ring._index.keys())
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_TRANSIENTS_PERM, 'getLen')
getLen = __len__
class Ring(Persistent): class Ring(Persistent):
""" Instances of this class will be frequently written to the ZODB, """ ring of buckets """
so it's optimized as best possible for write-friendliness """
def __init__(self, l, index): def __init__(self, l, index):
if not len(l): if not len(l):
raise "ring must have at least one element" raise "ring must have at least one element"
...@@ -601,152 +589,9 @@ class Ring(Persistent): ...@@ -601,152 +589,9 @@ class Ring(Persistent):
def _p_independent(self): def _p_independent(self):
return 1 return 1
class TransientObject(Persistent, Implicit): # this should really have a _p_resolveConflict, but
""" akin to Session Data Object """ # I've not had time to come up with a reasonable one that
__implements__ = (ItemWithId, # randomly generate an id # works in every circumstance.
Transient,
DictionaryLike,
TTWDictionary,
ImmutablyValuedMappingOfPickleableObjects
)
security = ClassSecurityInfo()
security.setDefaultAccess('allow')
security.declareObjectPublic()
#
# Initializer
#
def __init__(self, containerkey):
self.token = containerkey
self.id = self._generateUniqueId()
self._container = {}
self._created = self._last_accessed = time()
self._timergranularity = WRITEGRANULARITY # timer granularity
# -----------------------------------------------------------------
# ItemWithId
#
def getId(self):
return self.id
# -----------------------------------------------------------------
# Transient
#
def invalidate(self):
self._invalid = None
def isValid(self):
return not hasattr(self, '_invalid')
def getLastAccessed(self):
return self._last_accessed
def setLastAccessed(self):
# check to see if the last_accessed time is too recent, and avoid
# setting if so, to cut down on heavy writes
t = time()
if self._last_accessed and (self._last_accessed +
self._timergranularity < t):
self._last_accessed = t
def getCreated(self):
return self._created
# -----------------------------------------------------------------
# DictionaryLike
#
def keys(self):
return self._container.keys()
def values(self):
return self._container.values()
def items(self):
return self._container.items()
def get(self, k, default=_notfound):
v = self._container.get(k, default)
if v is _notfound: return None
return v
def has_key(self, k):
if self._container.get(k, _notfound) is not _notfound: return 1
return 0
def clear(self):
self._container.clear()
self._p_changed = 1
def update(self, d):
for k in d.keys():
self[k] = d[k]
# -----------------------------------------------------------------
# ImmutablyValuedMappingOfPickleableObjects (what a mouthful!)
#
def __setitem__(self, k, v):
# if the key or value is a persistent instance,
# set up its _p_jar immediately
if hasattr(v, '_p_jar') and v._p_jar is None:
v._p_jar = self._p_jar
v._p_changed = 1
if hasattr(k, '_p_jar') and k._p_jar is None:
k._p_jar = self._p_jar
k._p_changed = 1
self._container[k] = v
self._p_changed = 1
def __getitem__(self, k):
return self._container[k]
def __delitem__(self, k):
del self._container[k]
# -----------------------------------------------------------------
# TTWDictionary
#
set = __setitem__
def delete(self, k):
del self._container[k]
self._p_changed = 1
__guarded_setitem__ = __setitem__
# -----------------------------------------------------------------
# Other non interface code
#
def _p_independent(self):
# My state doesn't depend on or materially effect the state of
# other objects (eliminates read conflicts).
return 1
getName = getId # this is for SQLSession compatibility
def getContainerKey(self):
return self.token
def _generateUniqueId(self):
t = str(int(time()))
d = "%010d" % random.randint(0, sys.maxint-1)
return "%s%s" % (t, d)
def __repr__(self):
return "id: %s, token: %s, contents: %s" % (
self.id, self.token, `self.items()`
)
Globals.InitializeClass(TransientObjectContainer) Globals.InitializeClass(TransientObjectContainer)
Globals.InitializeClass(TransientObject)
...@@ -275,6 +275,9 @@ class HomogeneousItemContainer(Interface.Base): ...@@ -275,6 +275,9 @@ class HomogeneousItemContainer(Interface.Base):
""" """
Return value associated with key k via __getitem__. If value Return value associated with key k via __getitem__. If value
associated with k does not exist, return default. associated with k does not exist, return default.
Returned item is acquisition-wrapped in self unless a default
is passed in and returned.
""" """
def has_key(self, k): def has_key(self, k):
...@@ -283,13 +286,8 @@ class HomogeneousItemContainer(Interface.Base): ...@@ -283,13 +286,8 @@ class HomogeneousItemContainer(Interface.Base):
return false. return false.
""" """
def delete(self, k):
"""
Delete value associated with key k, raise a KeyError if nonexistent.
"""
class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer): class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
def new(self, k, wrap_with=None): def new(self, k):
""" """
Creates a new subobject of the type supported by this container Creates a new subobject of the type supported by this container
with key "k" and returns it. with key "k" and returns it.
...@@ -297,14 +295,15 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer): ...@@ -297,14 +295,15 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
If an object already exists in the container with key "k", a If an object already exists in the container with key "k", a
KeyError is raised. KeyError is raised.
If wrap_with is non-None, the subobject is returned in the
acquisition context of wrap_in, else it is returned in
the acquisition context of this transient object container.
"k" must be a string, else a TypeError is raised. "k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded exception
will be raised.
Returned object is acquisition-wrapped in self.
""" """
def new_or_existing(self, k, wrap_with=None): def new_or_existing(self, k):
""" """
If an object already exists in the container with key "k", it If an object already exists in the container with key "k", it
is returned. is returned.
...@@ -312,11 +311,12 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer): ...@@ -312,11 +311,12 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
Otherwise, create a new subobject of the type supported by this Otherwise, create a new subobject of the type supported by this
container with key "k" and return it. container with key "k" and return it.
If wrap_with is non-None, the subobject is returned in the
acquisition context of wrap_in, else it is returned in
the acquisition context of this transient object container.
"k" must be a string, else a TypeError is raised. "k" must be a string, else a TypeError is raised.
If a new object needs to be created and the container is 'full',
a MaxTransientObjectsExceeded exception will be raised.
Returned object is acquisition-wrapped in self.
""" """
class TransientItemContainer(Interface.Base): class TransientItemContainer(Interface.Base):
......
##############################################################################
#
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#
# Copyright (c) Digital Creations. All rights reserved.
#
# This license has been certified as Open Source(tm).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions in source code must retain the above copyright
# notice, this list of conditions, and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions, and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Digital Creations requests that attribution be given to Zope
# in any manner possible. Zope includes a "Powered by Zope"
# button that is installed by default. While it is not a license
# violation to remove this button, it is requested that the
# attribution remain. A significant investment has been put
# into Zope, and this effort will continue if the Zope community
# continues to grow. This is one way to assure that growth.
#
# 4. All advertising materials and documentation mentioning
# features derived from or use of this software must display
# the following acknowledgement:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# In the event that the product being advertised includes an
# intact Zope distribution (with copyright and license included)
# then this clause is waived.
#
# 5. Names associated with Zope or Digital Creations must not be used to
# endorse or promote products derived from this software without
# prior written permission from Digital Creations.
#
# 6. Modified redistributions of any form whatsoever must retain
# the following acknowledgment:
#
# "This product includes software developed by Digital Creations
# for use in the Z Object Publishing Environment
# (http://www.zope.org/)."
#
# Intact (re-)distributions of any official Zope release do not
# require an external acknowledgement.
#
# 7. Modifications are encouraged but must be packaged separately as
# patches to official Zope releases. Distributions that do not
# clearly separate the patches from the original work must be clearly
# labeled as unofficial distributions. Modifications which do not
# carry the name Zope may be packaged in any form, as long as they
# conform to all of the clauses above.
#
#
# Disclaimer
#
# THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
# EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations. Specific
# attributions are listed in the accompanying credits file.
#
##############################################################################
"""
Simple ZODB-based transient object implementation.
$Id: TransientObject.py,v 1.1 2001/11/21 22:46:36 chrism Exp $
"""
__version__='$Revision: 1.1 $'[11:-2]
from Persistence import Persistent
from Acquisition import Implicit, aq_base
import time, random, sys
from TransienceInterfaces import ItemWithId, Transient, DictionaryLike,\
TTWDictionary, ImmutablyValuedMappingOfPickleableObjects
from AccessControl import ClassSecurityInfo
import Globals
from zLOG import LOG, BLATHER
_notfound = []
WRITEGRANULARITY=30 # Timing granularity for write clustering, in seconds
class TransientObject(Persistent, Implicit):
""" Dictionary-like object that supports additional methods
concerning expiration and containment in a transient object container
"""
__implements__ = (ItemWithId, # randomly generate an id
Transient,
DictionaryLike,
TTWDictionary,
ImmutablyValuedMappingOfPickleableObjects
)
security = ClassSecurityInfo()
security.setDefaultAccess('allow')
security.declareObjectPublic()
def __init__(self, containerkey):
self.token = containerkey
self.id = self._generateUniqueId()
self._container = {}
self._created = self._last_accessed = time.time()
# -----------------------------------------------------------------
# ItemWithId
#
def getId(self):
return self.id
# -----------------------------------------------------------------
# Transient
#
def invalidate(self):
self._invalid = None
def isValid(self):
return not hasattr(self, '_invalid')
def getLastAccessed(self):
return self._last_accessed
def setLastAccessed(self, WG=WRITEGRANULARITY):
# check to see if the last_accessed time is too recent, and avoid
# setting if so, to cut down on heavy writes
t = time.time()
if (self._last_accessed + WG) < t:
self._last_accessed = t
def getCreated(self):
return self._created
def getContainerKey(self):
return self.token
# -----------------------------------------------------------------
# DictionaryLike
#
def keys(self):
return self._container.keys()
def values(self):
return self._container.values()
def items(self):
return self._container.items()
def get(self, k, default=_notfound):
v = self._container.get(k, default)
if v is _notfound: return None
return v
def has_key(self, k):
if self._container.get(k, _notfound) is not _notfound: return 1
return 0
def clear(self):
self._container.clear()
self._p_changed = 1
def update(self, d):
for k in d.keys():
self[k] = d[k]
# -----------------------------------------------------------------
# ImmutablyValuedMappingOfPickleableObjects (what a mouthful!)
#
def __setitem__(self, k, v):
# if the key or value is a persistent instance,
# set up its _p_jar immediately
if hasattr(v, '_p_jar') and v._p_jar is None:
v._p_jar = self._p_jar
v._p_changed = 1
if hasattr(k, '_p_jar') and k._p_jar is None:
k._p_jar = self._p_jar
k._p_changed = 1
self._container[k] = v
self._p_changed = 1
def __getitem__(self, k):
return self._container[k]
def __delitem__(self, k):
del self._container[k]
# -----------------------------------------------------------------
# TTWDictionary
#
set = __setitem__
def delete(self, k):
del self._container[k]
self._p_changed = 1
__guarded_setitem__ = __setitem__
# -----------------------------------------------------------------
# Other non interface code
#
def _p_independent(self):
# My state doesn't depend on or materially effect the state of
# other objects (eliminates read conflicts).
return 1
def _p_resolveConflict(self, saved, state1, state2):
attrs = ['token', 'id', '_created', '_invalid']
# note that last_accessed and _container are the only attrs
# missing from this list. The only time we can clearly resolve
# the conflict is if everything but the last_accessed time and
# the contents are the same, so we make sure nothing else has
# changed. We're being slightly sneaky here by accepting
# possibly conflicting data in _container, but it's acceptable
# in this context.
LOG('Transience', BLATHER, 'Resolving conflict in TransientObject')
for attr in attrs:
old = saved.get(attr)
st1 = state1.get(attr)
st2 = state2.get(attr)
if not (old == st1 == st2):
return None
# return the object with the most recent last_accessed value.
if state1['_last_accessed'] > state2['_last_accessed']:
return state1
else:
return state2
getName = getId # this is for SQLSession compatibility
def _generateUniqueId(self):
t = str(int(time.time()))
d = "%010d" % random.randint(0, sys.maxint-1)
return "%s%s" % (t, d)
def __repr__(self):
return "id: %s, token: %s, contents: %s" % (
self.id, self.token, `self.items()`
)
Globals.InitializeClass(TransientObject)
...@@ -85,10 +85,15 @@ ...@@ -85,10 +85,15 @@
""" """
Transience initialization routines Transience initialization routines
$Id: __init__.py,v 1.4 2001/11/07 06:46:36 chrism Exp $ $Id: __init__.py,v 1.5 2001/11/21 22:46:36 chrism Exp $
""" """
import ZODB # this is to help out testrunner, don't remove.
import Transience import Transience
# import of MaxTransientObjectsExceeded for easy import from scripts,
# this is protected by a module security info declaration in the
# Sessions package.
from Transience import MaxTransientObjectsExceeded
def initialize(context): def initialize(context):
context.registerClass( context.registerClass(
...@@ -100,3 +105,4 @@ def initialize(context): ...@@ -100,3 +105,4 @@ def initialize(context):
) )
context.registerHelp() context.registerHelp()
context.registerHelpTitle('Zope Help') context.registerHelpTitle('Zope Help')
...@@ -73,6 +73,20 @@ the Zope physical path to the method to be invoked to receive the notification ...@@ -73,6 +73,20 @@ the Zope physical path to the method to be invoked to receive the notification
</TD> </TD>
</TR> </TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Maximum number of subobjects
</div>
<div class="form-help">
("0" means infinite)
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="limit:int" SIZE="10" value="1000">
</TD>
</TR>
<TR> <TR>
<TD ALIGN="LEFT" VALIGN="TOP"> <TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label"> <div class="form-label">
......
...@@ -13,7 +13,7 @@ Transient Object Containers are used to store transient data. ...@@ -13,7 +13,7 @@ Transient Object Containers are used to store transient data.
Transient data will persist, but only for a user-specified period of time Transient data will persist, but only for a user-specified period of time
(the "data object timeout") after which it will be flushed. (the "data object timeout") after which it will be flushed.
</p> </p>
<dtml-call nudge><!-- turn the buckets if necessary -->
<p class="form-label"> <p class="form-label">
<font color="green"> <font color="green">
<dtml-let l=getLen> <dtml-let l=getLen>
...@@ -55,6 +55,21 @@ Transient data will persist, but only for a user-specified period of time ...@@ -55,6 +55,21 @@ Transient data will persist, but only for a user-specified period of time
</td> </td>
</tr> </tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Maximum number of subobjects
</div>
<div class="form-help">
("0" means infinite)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="limit:int" size=10
value=&dtml-getSubobjectLimit;>
</td>
</tr>
<tr> <tr>
<td align="left" valign="top"> <td align="left" valign="top">
<div class="form-label"> <div class="form-label">
......
...@@ -30,6 +30,19 @@ TransientObjectContainer - Add ...@@ -30,6 +30,19 @@ TransientObjectContainer - Add
they may not be deleted exactly after this number of minutes elapses. they may not be deleted exactly after this number of minutes elapses.
A setting of "0" indicates that objects should not expire. A setting of "0" indicates that objects should not expire.
- **Maximum number of subobjects **
The maximum number of subobjects that this container may
simultaneously hold.
If the value is "0", the number of objects addable to the container
will be not be artificially limited.
Note: This setting is useful to prevent accidental or deliberate denial
of service due to RAM shortage if the transient object container is
instantiated in a storage which is backed solely by RAM, such
as a Temporary Folder.
- **Script to call when objects are added** - **Script to call when objects are added**
*Optional* *Optional*
......
...@@ -34,6 +34,19 @@ TransientObjectContainer - Manage ...@@ -34,6 +34,19 @@ TransientObjectContainer - Manage
If the timeout value is "0", objects will not time out. If the timeout value is "0", objects will not time out.
- **Maximum number of subobjects **
The maximum number of subobjects that this container may
simultaneously hold.
If the value is "0", the number of objects addable to the container
will be not be artificially limited.
This setting is useful to prevent accidental or deliberate denial
of service due to RAM shortage if the transient object container is
instantiated in a storage which is backed solely by RAM, such
as a Temporary Folder.
- **Script to call when objects are added** - **Script to call when objects are added**
*Optional* *Optional*
......
...@@ -112,13 +112,6 @@ class TransientObjectContainer: ...@@ -112,13 +112,6 @@ class TransientObjectContainer:
Permission -- 'Access Transient Objects' Permission -- 'Access Transient Objects'
""" """
def delete(self, k):
"""
Delete value associated with key k, raise a KeyError if nonexistent.
Permission -- 'Access Transient Objects'
"""
def new(self, k): def new(self, k):
""" """
Creates a new subobject of the type supported by this container Creates a new subobject of the type supported by this container
...@@ -129,6 +122,9 @@ class TransientObjectContainer: ...@@ -129,6 +122,9 @@ class TransientObjectContainer:
"k" must be a string, else a TypeError is raised. "k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded will
be raised.
Permission -- 'Create Transient Objects' Permission -- 'Create Transient Objects'
""" """
...@@ -142,6 +138,9 @@ class TransientObjectContainer: ...@@ -142,6 +138,9 @@ class TransientObjectContainer:
"k" must be a string, else a TypeError is raised. "k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded exception
be raised.
Permission -- 'Create Transient Objects' Permission -- 'Create Transient Objects'
""" """
...@@ -345,6 +344,16 @@ class TransientObject: ...@@ -345,6 +344,16 @@ class TransientObject:
Permission -- Always available Permission -- Always available
""" """
class MaxTransientObjectsExceeded:
"""
An exception importable from the Products.Transience.Transience module
which is raised when an attempt is made to add an item to a
TransientObjectContainer that is 'full'.
This exception may be caught in PythonScripts through a normal import.
A successful import of the exception can be achieved via::
from Products.Transience import MaxTransientObjectsExceeded
"""
import time as origtime
epoch = origtime.time()
def time():
""" False timer -- returns time 10 x faster than normal time """
return (origtime.time() - epoch) * 10.0
def sleep(duration):
""" False sleep -- sleep for 1/10 the time specifed """
origtime.sleep(duration / 10.0)
...@@ -10,15 +10,18 @@ import Acquisition ...@@ -10,15 +10,18 @@ import Acquisition
from Acquisition import aq_base from Acquisition import aq_base
from Products.Transience.Transience import TransientObjectContainer from Products.Transience.Transience import TransientObjectContainer
import Products.Transience.Transience import Products.Transience.Transience
import Products.Transience.TransientObject
from Products.PythonScripts.PythonScript import PythonScript from Products.PythonScripts.PythonScript import PythonScript
from ZODB.POSException import InvalidObjectReference from ZODB.POSException import InvalidObjectReference
from DateTime import DateTime from DateTime import DateTime
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
from ZODB.DemoStorage import DemoStorage from ZODB.DemoStorage import DemoStorage
from OFS.Application import Application from OFS.Application import Application
import time, threading, whrandom import threading, whrandom
import fauxtime
import time as oldtime
WRITEGRANULARITY = 30 WRITEGRANULARITY = 30
epoch = time.time()
stuff = {} stuff = {}
def _getApp(): def _getApp():
...@@ -54,6 +57,7 @@ def _delApp(): ...@@ -54,6 +57,7 @@ def _delApp():
class TestBase(TestCase): class TestBase(TestCase):
def setUp(self): def setUp(self):
Products.Transience.Transience.time = fauxtime Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.app = makerequest.makerequest(_getApp()) self.app = makerequest.makerequest(_getApp())
timeout = self.timeout = 1 timeout = self.timeout = 1
sm=TransientObjectContainer( sm=TransientObjectContainer(
...@@ -66,12 +70,14 @@ class TestBase(TestCase): ...@@ -66,12 +70,14 @@ class TestBase(TestCase):
get_transaction().abort() get_transaction().abort()
_delApp() _delApp()
del self.app del self.app
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
class TestLastAccessed(TestBase): class TestLastAccessed(TestBase):
def testLastAccessed(self): def testLastAccessed(self):
sdo = self.app.sm.new_or_existing('TempObject') sdo = self.app.sm.new_or_existing('TempObject')
la1 = sdo.getLastAccessed() la1 = sdo.getLastAccessed()
fauxsleep(WRITEGRANULARITY + 1) fauxtime.sleep(WRITEGRANULARITY + 1)
sdo = self.app.sm['TempObject'] sdo = self.app.sm['TempObject']
assert sdo.getLastAccessed() > la1, (sdo.getLastAccessed(), la1) assert sdo.getLastAccessed() > la1, (sdo.getLastAccessed(), la1)
...@@ -79,7 +85,7 @@ class TestNotifications(TestBase): ...@@ -79,7 +85,7 @@ class TestNotifications(TestBase):
def testAddNotification(self): def testAddNotification(self):
self.app.sm.setAddNotificationTarget(addNotificationTarget) self.app.sm.setAddNotificationTarget(addNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject') sdo = self.app.sm.new_or_existing('TempObject')
now = fauxtime() now = fauxtime.time()
k = sdo.get('starttime') k = sdo.get('starttime')
assert type(k) == type(now) assert type(k) == type(now)
assert k <= now assert k <= now
...@@ -88,35 +94,26 @@ class TestNotifications(TestBase): ...@@ -88,35 +94,26 @@ class TestNotifications(TestBase):
self.app.sm.setDelNotificationTarget(delNotificationTarget) self.app.sm.setDelNotificationTarget(delNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject') sdo = self.app.sm.new_or_existing('TempObject')
timeout = self.timeout * 60 timeout = self.timeout * 60
fauxsleep(timeout + (timeout * .33)) fauxtime.sleep(timeout + (timeout * .33))
try: sdo1 = self.app.sm['TempObject'] try: sdo1 = self.app.sm['TempObject']
except KeyError: pass except KeyError: pass
now = fauxtime() now = fauxtime.time()
k = sdo.get('endtime') k = sdo.get('endtime')
assert type(k) == type(now) assert type(k) == type(now)
assert k <= now assert k <= now
def addNotificationTarget(item, context): def addNotificationTarget(item, context):
item['starttime'] = fauxtime() item['starttime'] = fauxtime.time()
def delNotificationTarget(item, context): def delNotificationTarget(item, context):
item['endtime'] = fauxtime() item['endtime'] = fauxtime.time()
def fauxtime():
""" False timer -- returns time 10 x faster than normal time """
return (time.time() - epoch) * 10.0
def fauxsleep(duration):
""" False sleep -- sleep for 1/10 the time specifed """
time.sleep(duration / 10.0)
def test_suite(): def test_suite():
last_accessed = makeSuite(TestLastAccessed, 'test') last_accessed = makeSuite(TestLastAccessed, 'test')
start_end = makeSuite(TestNotifications, 'test') start_end = makeSuite(TestNotifications, 'test')
runner = TextTestRunner()
suite = TestSuite((start_end, last_accessed)) suite = TestSuite((start_end, last_accessed))
return suite return suite
if __name__ == '__main__': if __name__ == '__main__':
runner = TextTestRunner(sys.stdout) runner = TextTestRunner(verbosity=9)
runner.run(test_suite()) runner.run(test_suite())
...@@ -82,34 +82,38 @@ ...@@ -82,34 +82,38 @@
# attributions are listed in the accompanying credits file. # attributions are listed in the accompanying credits file.
# #
############################################################################## ##############################################################################
import sys, os, time, whrandom, unittest import sys, os, whrandom, unittest
if __name__ == "__main__": if __name__ == "__main__":
sys.path.insert(0, '../../..') sys.path.insert(0, '../../..')
#os.chdir('../../..')
import ZODB import ZODB
from Products.Transience.Transience import \ from Products.Transience.Transience import TransientObjectContainer
TransientObjectContainer, TransientObject from Products.Transience.TransientObject import TransientObject
import Products.Transience.TransientObject
import Products.Transience.Transience import Products.Transience.Transience
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
import time as oldtime
epoch = time.time() import fauxtime
class TestTransientObject(TestCase): class TestTransientObject(TestCase):
def setUp(self): def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.errmargin = .20 self.errmargin = .20
self.timeout = 60 self.timeout = 60
Products.Transience.Transience.time = fauxtime
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60) self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60)
def tearDown(self): def tearDown(self):
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
self.t = None self.t = None
del self.t del self.t
def test_id(self): def test_id(self):
t = self.t.new('xyzzy') t = self.t.new('xyzzy')
assert t.getId() != 'xyzzy' assert t.getId() != 'xyzzy'
assert t.getContainerKey() == 'xyzzy'
def test_validate(self): def test_validate(self):
t = self.t.new('xyzzy') t = self.t.new('xyzzy')
...@@ -119,22 +123,22 @@ class TestTransientObject(TestCase): ...@@ -119,22 +123,22 @@ class TestTransientObject(TestCase):
def test_getLastAccessed(self): def test_getLastAccessed(self):
t = self.t.new('xyzzy') t = self.t.new('xyzzy')
ft = fauxtime() ft = fauxtime.time()
assert t.getLastAccessed() <= ft assert t.getLastAccessed() <= ft
def test_getCreated(self): def test_getCreated(self):
t = self.t.new('xyzzy') t = self.t.new('xyzzy')
ft = fauxtime() ft = fauxtime.time()
assert t.getCreated() <= ft assert t.getCreated() <= ft
def test_setLastAccessed(self): def test_setLastAccessed(self):
t = self.t.new('xyzzy') t = self.t.new('xyzzy')
ft = fauxtime() ft = fauxtime.time()
assert t.getLastAccessed() <= ft assert t.getLastAccessed() <= ft
fauxsleep(self.timeout) # go to sleep past the granuarity fauxtime.sleep(self.timeout) # go to sleep past the granuarity
ft2 = fauxtime() ft2 = fauxtime.time()
t.setLastAccessed() t.setLastAccessed()
ft3 = fauxtime() ft3 = fauxtime.time()
assert t.getLastAccessed() <= ft3 assert t.getLastAccessed() <= ft3
assert t.getLastAccessed() >= ft2 assert t.getLastAccessed() >= ft2
...@@ -175,19 +179,11 @@ def test_suite(): ...@@ -175,19 +179,11 @@ def test_suite():
alltests = TestSuite((testsuite,)) alltests = TestSuite((testsuite,))
return alltests return alltests
def fauxtime():
""" False timer -- returns time 10 x faster than normal time """
return (time.time() - epoch) * 10.0
def fauxsleep(duration):
""" False sleep -- sleep for 1/10 the time specifed """
time.sleep(duration / 10.0)
data = { data = {
'a': 'a', 'a': 'a',
1: 1, 1: 1,
'Mary': 'no little lamb for you today!', 'Mary': 'no little lamb for you today!',
'epoch': epoch, 'epoch': 999999999,
'fauxtime': fauxtime 'fauxtime': fauxtime
} }
......
...@@ -86,28 +86,30 @@ import sys, os, time, whrandom, unittest ...@@ -86,28 +86,30 @@ import sys, os, time, whrandom, unittest
if __name__ == "__main__": if __name__ == "__main__":
sys.path.insert(0, '../../..') sys.path.insert(0, '../../..')
#os.chdir('../../..')
import ZODB import ZODB
from Products.Transience.Transience import \ from Products.Transience.Transience import TransientObjectContainer,\
TransientObjectContainer, TransientObject MaxTransientObjectsExceeded
from Products.Transience.TransientObject import TransientObject
import Products.Transience.Transience import Products.Transience.Transience
import Products.Transience.TransientObject
from ExtensionClass import Base from ExtensionClass import Base
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
import time as oldtime
epoch = time.time() import fauxtime
stash = {}
class TestTransientObjectContainer(TestCase): class TestTransientObjectContainer(TestCase):
def setUp(self): def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.errmargin = .20 self.errmargin = .20
self.timeout = 60 self.timeout = 60
Products.Transience.Transience.time = fauxtime
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60) self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60)
def tearDown(self): def tearDown(self):
self.t = None self.t = None
del self.t Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
def testGetItemFails(self): def testGetItemFails(self):
self.assertRaises(KeyError, self._getitemfail) self.assertRaises(KeyError, self._getitemfail)
...@@ -357,7 +359,7 @@ class TestTransientObjectContainer(TestCase): ...@@ -357,7 +359,7 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
# these items will time out while we sleep # these items will time out while we sleep
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
for x in range(110, 210): for x in range(110, 210):
self.t[x] = x self.t[x] = x
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
...@@ -374,7 +376,7 @@ class TestTransientObjectContainer(TestCase): ...@@ -374,7 +376,7 @@ class TestTransientObjectContainer(TestCase):
# 1 minute # 1 minute
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys()) assert len(self.t.keys()) == 0, len(self.t.keys())
# 2 minutes # 2 minutes
...@@ -382,9 +384,9 @@ class TestTransientObjectContainer(TestCase): ...@@ -382,9 +384,9 @@ class TestTransientObjectContainer(TestCase):
self.t._reset() self.t._reset()
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys()) assert len(self.t.keys()) == 0, len(self.t.keys())
# 3 minutes # 3 minutes
...@@ -392,22 +394,22 @@ class TestTransientObjectContainer(TestCase): ...@@ -392,22 +394,22 @@ class TestTransientObjectContainer(TestCase):
self.t._reset() self.t._reset()
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
fauxsleep(self.timeout * (self.errmargin+1)) fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys()) assert len(self.t.keys()) == 0, len(self.t.keys())
def testGetItemDelaysTimeout(self): def testGetItemDelaysTimeout(self):
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
# current bucket will become old after we sleep for a while. # current bucket will become old after we sleep for a while.
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
# these items will be added to the new current bucket by getitem # these items will be added to the new current bucket by getitem
for x in range(10, 110): for x in range(10, 110):
self.t[x] self.t[x]
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110): for x in range(10, 110):
assert self.t[x] == x assert self.t[x] == x
...@@ -416,11 +418,11 @@ class TestTransientObjectContainer(TestCase): ...@@ -416,11 +418,11 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
# current bucket will become old after we sleep for a while. # current bucket will become old after we sleep for a while.
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
# these items will be added to the new current bucket by getitem # these items will be added to the new current bucket by getitem
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x + 1 self.t[x] = x + 1
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110): for x in range(10, 110):
assert self.t[x] == x + 1 assert self.t[x] == x + 1
...@@ -429,11 +431,11 @@ class TestTransientObjectContainer(TestCase): ...@@ -429,11 +431,11 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110): for x in range(10, 110):
self.t[x] = x self.t[x] = x
# current bucket will become old after we sleep for a while. # current bucket will become old after we sleep for a while.
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
# these items will be added to the new current bucket by getitem # these items will be added to the new current bucket by getitem
for x in range(10, 110): for x in range(10, 110):
self.t.get(x) self.t.get(x)
fauxsleep(self.timeout/2) fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys()) assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110): for x in range(10, 110):
assert self.t[x] == x assert self.t[x] == x
...@@ -479,9 +481,18 @@ class TestTransientObjectContainer(TestCase): ...@@ -479,9 +481,18 @@ class TestTransientObjectContainer(TestCase):
def test_getId(self): def test_getId(self):
assert self.t.getId() == 'sdc' assert self.t.getId() == 'sdc'
def test_getContainerKey(self): def testSubobjectLimitWorks(self):
t = self.t.new('foobieblech') self.t = TransientObjectContainer('a', timeout_mins=self.timeout/60,
assert t.getContainerKey() == 'foobieblech' limit=10)
self.assertRaises(MaxTransientObjectsExceeded, self._maxOut)
def testUnlimitedSubobjectLimitWorks(self):
self._maxOut()
def _maxOut(self):
for x in range(11):
self.t.new(str(x))
def lsubtract(l1, l2): def lsubtract(l1, l2):
l1=list(l1) l1=list(l1)
...@@ -491,19 +502,10 @@ def lsubtract(l1, l2): ...@@ -491,19 +502,10 @@ def lsubtract(l1, l2):
return l return l
def test_suite(): def test_suite():
#print "TransientObjectContainer tests take just about forever (10+ mins)"
testsuite = makeSuite(TestTransientObjectContainer, 'test') testsuite = makeSuite(TestTransientObjectContainer, 'test')
alltests = TestSuite((testsuite,)) alltests = TestSuite((testsuite,))
return alltests return alltests
def fauxtime():
""" False timer -- returns time 10 x faster than normal time """
return (time.time() - epoch) * 10.0
def fauxsleep(duration):
""" False sleep -- sleep for 1/10 the time specifed """
time.sleep(duration / 10.0)
if __name__ == '__main__': if __name__ == '__main__':
runner = TextTestRunner(verbosity=9) runner = TextTestRunner(verbosity=9)
runner.run(test_suite()) runner.run(test_suite())
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment