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 @@
#
##############################################################################
"""
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
from Globals import HTMLFile, MessageDialog
from TransienceInterfaces import Transient, DictionaryLike, ItemWithId,\
TTWDictionary, ImmutablyValuedMappingOfPickleableObjects,\
from TransienceInterfaces import ItemWithId,\
StringKeyedHomogeneousItemContainer, TransientItemContainer
from TransientObject import TransientObject
from OFS.SimpleItem import SimpleItem
from Persistence import Persistent, PersistentMapping
from Acquisition import Implicit, aq_base
from Persistence import Persistent
from AccessControl import ClassSecurityInfo, getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager
import AccessControl.SpecialUsers
from AccessControl.User import nobody
from BTrees import OOBTree
from BTrees.Length import Length
from zLOG import LOG, WARNING, BLATHER
import os
import os.path
import math
import time
import sys
import random
from types import InstanceType
import os, os.path, math, time, sys, random
DEBUG = os.environ.get('Z_TOC_DEBUG', '')
def TLOG(*args):
def DLOG(*args):
tmp = []
for arg in args:
tmp.append(str(arg))
LOG('Transience DEBUG', BLATHER, ' '.join(tmp))
class MaxTransientObjectsExceeded(Exception): pass
_notfound = []
_marker = []
WRITEGRANULARITY=30 # Timing granularity for write clustering, in seconds
time = time.time
# permissions
ADD_CONTAINER_PERM = 'Add Transient Object Container'
MGMT_SCREEN_PERM = 'View management screens'
......@@ -137,82 +130,61 @@ MANAGE_CONTAINER_PERM = 'Manage Transient Object Container'
constructTransientObjectContainerForm = HTMLFile(
'dtml/addTransientObjectContainer', globals())
def constructTransientObjectContainer(self, id, title='', timeout_mins=20,
addNotification=None, delNotification=None,
REQUEST=None):
addNotification=None, delNotification=None, limit=0, REQUEST=None):
""" """
ob = TransientObjectContainer(id, title, timeout_mins,
addNotification, delNotification)
ob = TransientObjectContainer(
id, title, timeout_mins, addNotification, delNotification, limit=limit
)
self._setObject(id, ob)
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1)
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"
icon = "misc_/Transience/datacontainer.gif"
__implements__ = (ItemWithId,
StringKeyedHomogeneousItemContainer,
TransientItemContainer
)
manage_options = (
{ 'label': 'Manage',
'action': 'manage_container',
'help': ('Transience', 'Transience.stx')
{ 'label': 'Manage',
'action': 'manage_container',
'help': ('Transience', 'Transience.stx')
},
{ 'label': 'Security',
'action': 'manage_access'
{ 'label': 'Security',
'action': 'manage_access'
},
)
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,
['Manager','Anonymous','Sessions'])
security.setPermissionDefault(CREATE_TRANSIENTS_PERM,
['Manager',])
['Manager','Anonymous'])
security.setPermissionDefault(MANAGE_CONTAINER_PERM,['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')
manage_container = HTMLFile('dtml/manageTransientObjectContainer',
globals())
security.setDefaultAccess('deny')
#
# Initializer
#
_limit = 0
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.title=title
self._ctype = ctype
self._addCallback = None
self._delCallback = None
self._err_margin = err_margin
self._setTimeout(timeout_mins)
self._setLimit(limit)
self._reset()
self.setDelNotificationTarget(delNotification)
self.setAddNotificationTarget(addNotification)
......@@ -228,35 +200,46 @@ class TransientObjectContainer(SimpleItem):
# StringKeyedHomogenousItemContainer
#
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
def new(self, key, wrap_with=None):
if type(key) is not type(''):
raise TypeError, (key, "key is not a string type")
if self.get(key,None) is not None:
if self[key].isValid():
raise KeyError, key # Not allowed to dup keys
del self[key]
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
item = TransientObject(key)
self[key] = item
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
def new(self, k):
if type(k) is not type(''):
raise TypeError, (k, "key is not a string type")
if self.get(k, None) is not None:
raise KeyError, "duplicate key %s" % k # Not allowed to dup keys
item = TransientObject(k)
self[k] = item
self.notifyAdd(item)
if not wrap_with:
return item.__of__(self)
else:
return item.__of__(wrap_with)
return item.__of__(self)
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing')
def new_or_existing(self, key, wrap_with=None):
item = self.get(key,_notfound)
if item is _notfound:
return self.new(key, wrap_with)
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)
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key')
def has_key(self, k):
v = self.get(k, _notfound)
if v is _notfound: return 0
return 1
# -----------------------------------------------------------------
# TransientItemContainer
......@@ -274,15 +257,23 @@ class TransientObjectContainer(SimpleItem):
""" """
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')
def getAddNotificationTarget(self):
return self._addCallback or ''
security.declareProtected(MANAGE_CONTAINER_PERM,
'setAddNotificationTarget')
security.declareProtected(MANAGE_CONTAINER_PERM,'setAddNotificationTarget')
def setAddNotificationTarget(self, f):
# We should assert that the callback function 'f' implements
# the TransientNotification interface
self._addCallback = f
security.declareProtected(MGMT_SCREEN_PERM, 'getDelNotificationTarget')
......@@ -292,12 +283,9 @@ class TransientObjectContainer(SimpleItem):
security.declareProtected(MANAGE_CONTAINER_PERM,
'setDelNotificationTarget')
def setDelNotificationTarget(self, f):
# We should assert that the callback function 'f' implements
# the TransientNotification interface
self._delCallback = f
#
# ----------------------------------------------
# Supporting methods (not part of the interface)
#
......@@ -309,14 +297,16 @@ class TransientObjectContainer(SimpleItem):
if self._delCallback:
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':
name = 'notifyAdd'
callback = self._addCallback
else:
name = 'notifyDestruct'
callback = self._delCallback
if type(callback) is type(''):
try:
method = self.unrestrictedTraverse(callback)
......@@ -332,33 +322,31 @@ class TransientObjectContainer(SimpleItem):
else:
method = callback
if callable(method):
if DEBUG:
TLOG('calling %s at object %s' % (callback, kind))
try:
user = getSecurityManager().getUser()
for item in items:
if callable(method):
try:
newSecurityManager(None, nobody)
method(item, self)
except:
# dont raise, just log
path = self.getPhysicalPath()
LOG('Transience',
WARNING,
'%s failed when calling %s in %s' % (name, callback,
'/'.join(path)),
error=sys.exc_info()
)
finally:
newSecurityManager(None, user)
else:
err = '%s in %s attempted to call non-callable %s'
path = self.getPhysicalPath()
LOG('Transience',
WARNING,
err % (name, '/'.join(path), callback),
error=sys.exc_info()
)
user = getSecurityManager().getUser()
try:
newSecurityManager(None, nobody)
method(item, self)
except:
# dont raise, just log
path = self.getPhysicalPath()
LOG('Transience', WARNING,
'%s failed when calling %s in %s' % (name,callback,
'/'.join(path)),
error=sys.exc_info()
)
finally:
newSecurityManager(None, user)
else:
err = '%s in %s attempted to call non-callable %s'
path = self.getPhysicalPath()
LOG('Transience',
WARNING,
err % (name, '/'.join(path), callback),
error=sys.exc_info()
)
# -----------------------------------------------------------------
# Management item support (non API)
......@@ -368,14 +356,13 @@ class TransientObjectContainer(SimpleItem):
'manage_changeTransientObjectContainer')
def manage_changeTransientObjectContainer(self, title='',
timeout_mins=20, addNotification=None, delNotification=None,
REQUEST=None):
limit=0, REQUEST=None):
"""
Change an existing transient object container.
"""
self.title = title
self.setTimeoutMinutes(timeout_mins)
self.setSubobjectLimit(limit)
if not addNotification:
addNotification = None
if not delNotification:
......@@ -391,76 +378,101 @@ class TransientObjectContainer(SimpleItem):
raise TypeError, (timeout_mins, "Must be integer")
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'):
for k in self.keys():
self.notifyDestruct(self[k])
del self[k]
try: self.notifyDestruct(self[k])
except KeyError: pass
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())
now = int(time.time())
for x in range(numbuckets):
dump_after = now + i
c = self._ctype()
c = OOBTree.OOBTree()
l.insert(0, [c, dump_after])
i = i + r_secs
index = self._ctype()
index = OOBTree.OOBTree()
self._ring = Ring(l, index)
try: self.__len__.set(0)
except AttributeError: self.__len__ = self.getLen = Length()
def _getCurrentBucket(self, get_dump=0):
# no timeout always returns last bucket
if not self._timeout_secs:
b, dump_after = self._ring._data[0]
if DEBUG:
TLOG('no timeout, returning first bucket')
DLOG('no timeout, returning first bucket')
return b
index = self._ring._index
now = int(time())
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.
to_clean = []
while 1:
l = b, dump_after = self._ring._data[-1]
if now > dump_after:
if DEBUG:
TLOG('now is %s' % now)
TLOG('dump_after for %s was %s, dumping'%(b, dump_after))
DLOG('now is %s' % now)
DLOG('dump_after for %s was %s, dumping'%(b, 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)
if b: to_clean.append(b)# only clean non-empty buckets
i = i + self._resolution_secs
else:
break
if to_clean: self._clean(to_clean, index)
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):
if DEBUG:
TLOG('building list of index items')
l = list(index.items())
if DEBUG:
TLOG('done building list of index items, now iterating over them')
tmp = []
for k, v in l:
if v is b:
tmp.append(k)
self.notifyDestruct(index[k][k])
def _clean(self, bucket_set, index):
# Build a reverse index. Eventually, I'll keep this in another
# persistent struct but I'm afraid of creating more conflicts right
# now. The reverse index is a mapping from bucketref -> OOSet of string
# keys.
rindex = {}
for k, v in list(index.items()):
# listifying above is dumb, but I think there's a btrees bug
# that causes plain old index.items to always return a sequence
# of even numbers
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]
if DEBUG:
TLOG('deleted %s' % tmp)
TLOG('clearing %s' % b)
b.clear()
try: self.__len__.change(-1)
except AttributeError: pass
bucket_to_expire.clear()
# finalize em
self.notifyDestruct(trans_obs)
def _show(self):
""" debug method """
......@@ -476,6 +488,12 @@ class TransientObjectContainer(SimpleItem):
for x in t:
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):
current = self._getCurrentBucket()
index = self._ring._index
......@@ -483,6 +501,19 @@ class TransientObjectContainer(SimpleItem):
if b is None:
# this is a new key
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:
# this is an old key that isn't in the current bucket.
del b[k] # delete it from the old bucket
......@@ -506,31 +537,6 @@ class TransientObjectContainer(SimpleItem):
del b[k] # delete the item from the old bucket.
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):
self._getCurrentBucket()
index = self._ring._index
......@@ -540,17 +546,13 @@ class TransientObjectContainer(SimpleItem):
security.declareProtected(ACCESS_TRANSIENTS_PERM, '__len__')
def __len__(self):
""" this won't be called unless we havent run __init__ """
if DEBUG: DLOG('Class __len__ called!')
self._getCurrentBucket()
return len(self._ring._index)
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key')
def has_key(self, k):
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
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'getLen')
getLen = __len__
def values(self):
return map(lambda k, self=self: self[k], self.keys())
......@@ -560,24 +562,10 @@ class TransientObjectContainer(SimpleItem):
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
return list(self._ring._index.keys())
def copy(self):
raise NotImplementedError
security.declareProtected(ACCESS_TRANSIENTS_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 """
""" ring of buckets """
def __init__(self, l, index):
if not len(l):
raise "ring must have at least one element"
......@@ -601,152 +589,9 @@ class Ring(Persistent):
def _p_independent(self):
return 1
class TransientObject(Persistent, Implicit):
""" akin to Session Data Object """
__implements__ = (ItemWithId, # randomly generate an id
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()`
)
# this should really have a _p_resolveConflict, but
# I've not had time to come up with a reasonable one that
# works in every circumstance.
Globals.InitializeClass(TransientObjectContainer)
Globals.InitializeClass(TransientObject)
......@@ -275,6 +275,9 @@ class HomogeneousItemContainer(Interface.Base):
"""
Return value associated with key k via __getitem__. If value
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):
......@@ -283,13 +286,8 @@ class HomogeneousItemContainer(Interface.Base):
return false.
"""
def delete(self, k):
"""
Delete value associated with key k, raise a KeyError if nonexistent.
"""
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
with key "k" and returns it.
......@@ -297,14 +295,15 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
If an object already exists in the container with key "k", a
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.
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
is returned.
......@@ -312,11 +311,12 @@ class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
Otherwise, create a new subobject of the type supported by this
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.
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):
......
##############################################################################
#
# 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 @@
"""
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 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):
context.registerClass(
......@@ -100,3 +105,4 @@ def initialize(context):
)
context.registerHelp()
context.registerHelpTitle('Zope Help')
......@@ -73,6 +73,20 @@ the Zope physical path to the method to be invoked to receive the notification
</TD>
</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>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
......
......@@ -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
(the "data object timeout") after which it will be flushed.
</p>
<dtml-call nudge><!-- turn the buckets if necessary -->
<p class="form-label">
<font color="green">
<dtml-let l=getLen>
......@@ -55,6 +55,21 @@ Transient data will persist, but only for a user-specified period of time
</td>
</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>
<td align="left" valign="top">
<div class="form-label">
......
......@@ -30,6 +30,19 @@ TransientObjectContainer - Add
they may not be deleted exactly after this number of minutes elapses.
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**
*Optional*
......
......@@ -34,6 +34,19 @@ TransientObjectContainer - Manage
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**
*Optional*
......
......@@ -112,13 +112,6 @@ class TransientObjectContainer:
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):
"""
Creates a new subobject of the type supported by this container
......@@ -129,6 +122,9 @@ class TransientObjectContainer:
"k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded will
be raised.
Permission -- 'Create Transient Objects'
"""
......@@ -142,6 +138,9 @@ class TransientObjectContainer:
"k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded exception
be raised.
Permission -- 'Create Transient Objects'
"""
......@@ -345,6 +344,16 @@ class TransientObject:
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
from Acquisition import aq_base
from Products.Transience.Transience import TransientObjectContainer
import Products.Transience.Transience
import Products.Transience.TransientObject
from Products.PythonScripts.PythonScript import PythonScript
from ZODB.POSException import InvalidObjectReference
from DateTime import DateTime
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
from ZODB.DemoStorage import DemoStorage
from OFS.Application import Application
import time, threading, whrandom
import threading, whrandom
import fauxtime
import time as oldtime
WRITEGRANULARITY = 30
epoch = time.time()
stuff = {}
def _getApp():
......@@ -54,6 +57,7 @@ def _delApp():
class TestBase(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.app = makerequest.makerequest(_getApp())
timeout = self.timeout = 1
sm=TransientObjectContainer(
......@@ -66,12 +70,14 @@ class TestBase(TestCase):
get_transaction().abort()
_delApp()
del self.app
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
class TestLastAccessed(TestBase):
def testLastAccessed(self):
sdo = self.app.sm.new_or_existing('TempObject')
la1 = sdo.getLastAccessed()
fauxsleep(WRITEGRANULARITY + 1)
fauxtime.sleep(WRITEGRANULARITY + 1)
sdo = self.app.sm['TempObject']
assert sdo.getLastAccessed() > la1, (sdo.getLastAccessed(), la1)
......@@ -79,7 +85,7 @@ class TestNotifications(TestBase):
def testAddNotification(self):
self.app.sm.setAddNotificationTarget(addNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject')
now = fauxtime()
now = fauxtime.time()
k = sdo.get('starttime')
assert type(k) == type(now)
assert k <= now
......@@ -88,35 +94,26 @@ class TestNotifications(TestBase):
self.app.sm.setDelNotificationTarget(delNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject')
timeout = self.timeout * 60
fauxsleep(timeout + (timeout * .33))
fauxtime.sleep(timeout + (timeout * .33))
try: sdo1 = self.app.sm['TempObject']
except KeyError: pass
now = fauxtime()
now = fauxtime.time()
k = sdo.get('endtime')
assert type(k) == type(now)
assert k <= now
def addNotificationTarget(item, context):
item['starttime'] = fauxtime()
item['starttime'] = fauxtime.time()
def delNotificationTarget(item, context):
item['endtime'] = fauxtime()
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)
item['endtime'] = fauxtime.time()
def test_suite():
last_accessed = makeSuite(TestLastAccessed, 'test')
start_end = makeSuite(TestNotifications, 'test')
runner = TextTestRunner()
suite = TestSuite((start_end, last_accessed))
return suite
if __name__ == '__main__':
runner = TextTestRunner(sys.stdout)
runner = TextTestRunner(verbosity=9)
runner.run(test_suite())
......@@ -82,34 +82,38 @@
# attributions are listed in the accompanying credits file.
#
##############################################################################
import sys, os, time, whrandom, unittest
import sys, os, whrandom, unittest
if __name__ == "__main__":
sys.path.insert(0, '../../..')
#os.chdir('../../..')
import ZODB
from Products.Transience.Transience import \
TransientObjectContainer, TransientObject
from Products.Transience.Transience import TransientObjectContainer
from Products.Transience.TransientObject import TransientObject
import Products.Transience.TransientObject
import Products.Transience.Transience
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
epoch = time.time()
import time as oldtime
import fauxtime
class TestTransientObject(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.errmargin = .20
self.timeout = 60
Products.Transience.Transience.time = fauxtime
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60)
def tearDown(self):
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
self.t = None
del self.t
def test_id(self):
t = self.t.new('xyzzy')
assert t.getId() != 'xyzzy'
assert t.getContainerKey() == 'xyzzy'
def test_validate(self):
t = self.t.new('xyzzy')
......@@ -119,22 +123,22 @@ class TestTransientObject(TestCase):
def test_getLastAccessed(self):
t = self.t.new('xyzzy')
ft = fauxtime()
ft = fauxtime.time()
assert t.getLastAccessed() <= ft
def test_getCreated(self):
t = self.t.new('xyzzy')
ft = fauxtime()
ft = fauxtime.time()
assert t.getCreated() <= ft
def test_setLastAccessed(self):
t = self.t.new('xyzzy')
ft = fauxtime()
ft = fauxtime.time()
assert t.getLastAccessed() <= ft
fauxsleep(self.timeout) # go to sleep past the granuarity
ft2 = fauxtime()
fauxtime.sleep(self.timeout) # go to sleep past the granuarity
ft2 = fauxtime.time()
t.setLastAccessed()
ft3 = fauxtime()
ft3 = fauxtime.time()
assert t.getLastAccessed() <= ft3
assert t.getLastAccessed() >= ft2
......@@ -175,19 +179,11 @@ def test_suite():
alltests = TestSuite((testsuite,))
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 = {
'a': 'a',
1: 1,
'Mary': 'no little lamb for you today!',
'epoch': epoch,
'epoch': 999999999,
'fauxtime': fauxtime
}
......
......@@ -86,29 +86,31 @@ import sys, os, time, whrandom, unittest
if __name__ == "__main__":
sys.path.insert(0, '../../..')
#os.chdir('../../..')
import ZODB
from Products.Transience.Transience import \
TransientObjectContainer, TransientObject
from Products.Transience.Transience import TransientObjectContainer,\
MaxTransientObjectsExceeded
from Products.Transience.TransientObject import TransientObject
import Products.Transience.Transience
import Products.Transience.TransientObject
from ExtensionClass import Base
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
epoch = time.time()
stash = {}
import time as oldtime
import fauxtime
class TestTransientObjectContainer(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
self.errmargin = .20
self.timeout = 60
Products.Transience.Transience.time = fauxtime
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60)
def tearDown(self):
self.t = None
del self.t
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
def testGetItemFails(self):
self.assertRaises(KeyError, self._getitemfail)
......@@ -357,7 +359,7 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110):
self.t[x] = x
# 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):
self.t[x] = x
assert len(self.t.keys()) == 100, len(self.t.keys())
......@@ -374,7 +376,7 @@ class TestTransientObjectContainer(TestCase):
# 1 minute
for x in range(10, 110):
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())
# 2 minutes
......@@ -382,9 +384,9 @@ class TestTransientObjectContainer(TestCase):
self.t._reset()
for x in range(10, 110):
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())
fauxsleep(self.timeout * (self.errmargin+1))
fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys())
# 3 minutes
......@@ -392,22 +394,22 @@ class TestTransientObjectContainer(TestCase):
self.t._reset()
for x in range(10, 110):
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())
fauxsleep(self.timeout * (self.errmargin+1))
fauxtime.sleep(self.timeout * (self.errmargin+1))
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())
def testGetItemDelaysTimeout(self):
for x in range(10, 110):
self.t[x] = x
# 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
for x in range(10, 110):
self.t[x]
fauxsleep(self.timeout/2)
fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110):
assert self.t[x] == x
......@@ -416,11 +418,11 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110):
self.t[x] = x
# 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
for x in range(10, 110):
self.t[x] = x + 1
fauxsleep(self.timeout/2)
fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110):
assert self.t[x] == x + 1
......@@ -429,11 +431,11 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110):
self.t[x] = x
# 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
for x in range(10, 110):
self.t.get(x)
fauxsleep(self.timeout/2)
fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110):
assert self.t[x] == x
......@@ -479,9 +481,18 @@ class TestTransientObjectContainer(TestCase):
def test_getId(self):
assert self.t.getId() == 'sdc'
def test_getContainerKey(self):
t = self.t.new('foobieblech')
assert t.getContainerKey() == 'foobieblech'
def testSubobjectLimitWorks(self):
self.t = TransientObjectContainer('a', timeout_mins=self.timeout/60,
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):
l1=list(l1)
......@@ -491,19 +502,10 @@ def lsubtract(l1, l2):
return l
def test_suite():
#print "TransientObjectContainer tests take just about forever (10+ mins)"
testsuite = makeSuite(TestTransientObjectContainer, 'test')
alltests = TestSuite((testsuite,))
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__':
runner = TextTestRunner(verbosity=9)
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