Commit a7d9afc7 authored by Chris McDonough's avatar Chris McDonough

New TransientObjectContainer implementation.

Changes:

 - More stable under high usage, especially in the face of
   situations under which there are many ZODB conflict
   errors. The previous implementation had stability problems
   when many conflict errors were encountered; especially
   conflicts that were generated as a result of a simultaneous
   change to a subobject of the TOC (such as in the case of a Zope
   application which makes heavy use of both frames and
   sessions).

 - More conflict-resistant.  Instead of ignoring the likelihood
   that multiple threads will attempt to perform the same actions
   simultaneously in methods of the TOC (which often causes
   conflicts), the new implementation attempts to avoid conflicts
   by employing a chance-based housekeeping model.  In this model,
   one thread is "elected" by chance to do the kinds of tasks that
   cause the most conflicts.

 - Now uses a "timeslice" based model instead of a "ring" based
   model.  This also helps cut down on conflicts and makes
   the code slighly less obfuscated (not much, though! ;-)

 - Quite a few more comments in the code.

 - Changes to the sessioning stresstest (which exposed the
   bug that made me reimplement the TOC in the first place).

 - Updates to unit tests.

 - A "HowTransienceWorks.stx" document which attempts to
   explain how the code works.  It's not stellar, but
   it's a start.

 - Changes to the TransientObject class that the TOC
   hands out (typically as a "session data object"), in order
   to make invalidation less Rube-Goldberg-ish.

The structure of the TOC object has changed enough that in order to
maintain b/w compatibility, an in-place upgrade of "old" instances
is implied by running them with this code.   "Upgraded" instances
are not backwards-incompatible, however, so folks can hopefully
move back and forth between Zope versions without much hassle.
parent 23ae0c58
......@@ -24,7 +24,9 @@ from Products.Sessions.SessionDataManager import \
from Products.Transience.Transience import \
TransientObjectContainer, TransientObject
from Products.TemporaryFolder.TemporaryFolder import MountedTemporaryFolder
from ZODB.POSException import InvalidObjectReference, ConflictError
from Products.TemporaryFolder.LowConflictConnection import LowConflictConnection
from ZODB.Connection import Connection
from ZODB.POSException import InvalidObjectReference, ConflictError, ReadConflictError, BTreesConflictError
from DateTime import DateTime
from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
import time, threading, random
......@@ -32,6 +34,7 @@ from cPickle import UnpickleableError
from ZODB.DemoStorage import DemoStorage
from OFS.Application import Application
import sys
from zLOG import log_time
sys.setcheckinterval(200)
tf_name = 'temp_folder'
......@@ -65,7 +68,7 @@ def _populate(app):
bidmgr = BrowserIdManager(idmgr_name)
tf = MountedTemporaryFolder(tf_name, title="Temporary Folder")
toc = TransientObjectContainer(toc_name, title='Temporary '
'Transient Object Container', timeout_mins=20)
'Transient Object Container', timeout_mins=1)
session_data_manager=SessionDataManager(id='session_data_manager',
path='/'+tf_name+'/'+toc_name, title='Session Data Manager')
......@@ -89,7 +92,7 @@ def _populate(app):
get_transaction().commit()
class TestMultiThread(TestCase):
def testNonOverlappingBrowserIds(self):
def testOverlappingBrowserIds(self):
readers = []
writers = []
readiters = 20
......@@ -115,10 +118,7 @@ class TestMultiThread(TestCase):
while threading.activeCount() > 1:
time.sleep(1)
for thread in readers:
assert thread.out == [], thread.out
def testOverlappingBrowserIds(self):
def testNonOverlappingBrowserIds(self):
readers = []
writers = []
readiters = 20
......@@ -144,9 +144,6 @@ class TestMultiThread(TestCase):
while threading.activeCount() > 1:
time.sleep(1)
for thread in readers:
assert thread.out == [], thread.out
class BaseReaderWriter(threading.Thread):
def __init__(self, db, iters, sdm_name):
self.conn = db.open()
......@@ -166,13 +163,23 @@ class BaseReaderWriter(threading.Thread):
try:
self.run1()
return
except ReadConflictError:
print "read conflict"
except BTreesConflictError:
print "btrees conflict"
except ConflictError:
i = i + 1
#print "conflict %d" % i
if i > 3: raise
print "general conflict"
except:
get_transaction().abort()
print log_time()
raise
i = i + 1
get_transaction().abort()
time.sleep(random.randrange(5) * .1)
finally:
self.conn.close()
del self.app
print i
class ReaderThread(BaseReaderWriter):
def run1(self):
......
How Transience Works
The object responsible for managing the expiration of "transient"
objects is the TransientObjectContainer, the class definition for
which is located in
Products.Transience.Transience.TransientObjectContainer. An
instance of this class is found in the default Zope installation at
/temp_folder/session_data.
The TransientObjectContainer (TOC) holds Transient Objects (TOs).
A TO is obtained via its container via a call to
TOC.new_or_existing(key), where "key" is usually the "browser id"
associated with a visitor (See Products.Session.BrowserIdManager).
If the TOC has a "current" TO corresponding to "key", it is
returned.
If the TOC does not have a "current" TO corresponding to "key", (due
to the expiration of the TO or because it never existed in the first
place) a "new" TO is manufactured and returned.
Timeslices
Transience defines the notion of a "timeslice". A "timeslice" is an
integer that represents some "slice" of time, defined by a "period".
For example, if a period is 20 seconds long, three ordered time
slices might be expressed as 0, 20, and 40. The next timeslice
would be 60, and so on. For an absolute time to "belong" to a
timeslice, it would need to be equal to or greater than one
timeslice integer, but less than the subsequent timeslice integer.
Data Structures Maintained by a Transient Object Container
The TOC maintains five important kinds of data structures:
- a "_data" structure, which is an IOBTree mapping a "timeslice"
integer to a "bucket" (see next bullet for definition of bucket).
- One or more "buckets", which are OOBTree objects which map a "key"
(usually browser id) to a TransientObject. Buckets are stored
inside of the "_data" structure. There is a concept of a
"current" bucket, which is the bucket that is contained within the
_data structured with a key equal to the "current" timeslice.
- An "index" which is an OOBTree mapping transient object "key" to
"timeslice", letting us quickly figure out which element in the _data
mapping contains the transient object related to the key. It is
stored as the attribute "_index" of the TOC. When calling code
wants to obtain a Transient Object, its key is looked up in the
index, which returns a timeslice. We ask the _data structure for the
bucket it has stored under that timeslice. Then the bucket is asked
for the object stored under the key. This returns the Transient Object.
- A "last timeslice" integer, which is equal to the "last" timeslice
under which TOC housekeeping operations were performed.
- A "next to deindex" integer, which is a timeslice
representing the next bucket which requires "deindexing"
(the removal of all the keys of the bucket from the index).
When a Transient Object is created via new_or_existing, it is added
to the "current" bucket. As time goes by, the bucket to which the
TO was added ceases to be the "current" bucket. If the transient
object is "accessed" (it is called up out of the TOC via the TOC's
'get' method), it is again moved to the "current" bucket defined by
the current time's timeslice.
During the course of normal operations, a TransientObject will move
from an "old" bucket to the "current" bucket many times, as long as
it continues to be accessed. It is possible for a TransientObject
to *never* expire, as long as it is called up out of its TOC often
enough.
If a TransientObject is not accessed in the period of time defined by
the TOC's "timeout", it is deindexed and eventually garbage collected.
How the TransientObjectContainer Determines if a TransientObject is "Current"
A TO is current if it has an entry in the "index". When a TO has an
entry in the index, it implies that the TO resides in a bucket that
is no "older" than the TOC timeout period, based on the bucket's
timeslice.
Housekeeping: Finalization, Notification, Garbage Collection, and
Bucket Replentishing
The TOC performs "deindexing", "notification", "garbage
collection", and "bucket replentishing". It performs these tasks
"in-band". This means that the TOC does not maintain a separate
thread that wakes up every so often to do these housekeeping tasks.
Instead, during the course of normal operations, the TOC
opportunistically performs them.
Deindexing is defined as the act of making an "expired" TO
inaccessible (by deleting it from the "index"). After a TO is
deindexed, it may not be used by application code any longer,
although it may "stick around" in a bucket for a while until the
bucket is eventually garbage collected.
Notification is defined as optionally calling a function at TOC
finalization time. The optional function call is user-defined, but
it is managed by the "notifyDestruct" method of the TOC.
Garbage collection is defined as deleting "expired" buckets in the
_data structure (the _data structure maps a timeslice to a bucket).
Bucket replentishing is defined as the action of (opportunistically)
creating more buckets to insert into the the _data structure,
replacing ones that are deleted during garbage collection. The act
of deleting a bucket does not necessarily imply that a new bucket
will be immediately created thereafter. We create new buckets in
batches to reduce the possibility of conflicts.
Goals
- A low number of ZODB conflict errors (which reduce performance).
- Stability.
To Do
- Testing under ZEO.
......@@ -149,8 +149,8 @@ class DictionaryLike(Interface.Base):
def get(k, default='marker'):
"""
Return value associated with key k. If k does not exist and default
is not marker, return default, else raise KeyError.
Return value associated with key k. Return None or default if k
does not exist.
"""
def has_key(k):
......
......@@ -13,10 +13,10 @@
"""
Simple ZODB-based transient object implementation.
$Id: TransientObject.py,v 1.6 2002/06/11 15:19:38 chrism Exp $
$Id: TransientObject.py,v 1.7 2002/06/21 01:51:43 chrism Exp $
"""
__version__='$Revision: 1.6 $'[11:-2]
__version__='$Revision: 1.7 $'[11:-2]
from Persistence import Persistent
from Acquisition import Implicit
......@@ -74,6 +74,13 @@ class TransientObject(Persistent, Implicit):
#
def invalidate(self):
if hasattr(self, '_invalid'):
# we dont want to invalidate twice
return
trans_ob_container = getattr(self, 'aq_parent', None)
if trans_ob_container is not None:
if trans_ob_container.has_key(self.token):
del trans_ob_container[self.token]
self._invalid = None
def isValid(self):
......
import sys, os, time, unittest
if __name__=='__main__':
sys.path.insert(0, '..')
sys.path.insert(0, '../../..')
import ZODB # in order to get Persistence.Persistent working
from Testing import makerequest
import Acquisition
......@@ -74,7 +78,7 @@ class TestLastAccessed(TestBase):
sdo = self.app.sm.new_or_existing('TempObject')
la1 = sdo.getLastAccessed()
fauxtime.sleep(WRITEGRANULARITY + 1)
sdo = self.app.sm['TempObject']
sdo = self.app.sm.get('TempObject')
assert sdo.getLastAccessed() > la1, (sdo.getLastAccessed(), la1)
class TestNotifications(TestBase):
......@@ -90,13 +94,15 @@ class TestNotifications(TestBase):
self.app.sm.setDelNotificationTarget(delNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject')
timeout = self.timeout * 60
fauxtime.sleep(timeout + (timeout * .33))
try: sdo1 = self.app.sm['TempObject']
except KeyError: pass
fauxtime.sleep(timeout + (timeout * .75))
sdo1 = self.app.sm.get('TempObject')
for x in range(1, 100):
# force the sdm to do housekeeping
self.app.sm._getCurrentBucket()
now = fauxtime.time()
k = sdo.get('endtime')
assert type(k) == type(now)
assert k <= now
assert (type(k) == type(now)), type(k)
assert k <= now, (k, now)
def addNotificationTarget(item, context):
item['starttime'] = fauxtime.time()
......
......@@ -12,6 +12,9 @@
##############################################################################
import sys, os, time, random, unittest
if __name__ == "__main__":
sys.path.insert(0, '../../..')
import ZODB
from Products.Transience.Transience import TransientObjectContainer,\
MaxTransientObjectsExceeded
......@@ -23,7 +26,7 @@ from unittest import TestCase, TestSuite, TextTestRunner, makeSuite
import time as oldtime
import fauxtime
class TestTransientObjectContainer(TestCase):
class TestBase(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
......@@ -36,6 +39,7 @@ class TestTransientObjectContainer(TestCase):
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
class TestTransientObjectContainer(TestBase):
def testGetItemFails(self):
self.assertRaises(KeyError, self._getitemfail)
......@@ -280,6 +284,36 @@ class TestTransientObjectContainer(TestCase):
except KeyError:
if self.t.has_key(x): assert 1==2,"failed to delete %s" % x
def testChangingTimeoutWorks(self):
# 1 minute
for x in range(10, 110):
self.t[x] = x
fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys())
# 2 minutes
self.t._setTimeout(self.timeout/60*2)
self.t._reset()
for x in range(10, 110):
self.t[x] = x
fauxtime.sleep(self.timeout)
assert len(self.t.keys()) == 100, len(self.t.keys())
fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys())
# 3 minutes
self.t._setTimeout(self.timeout/60*3)
self.t._reset()
for x in range(10, 110):
self.t[x] = x
fauxtime.sleep(self.timeout)
assert len(self.t.keys()) == 100, len(self.t.keys())
fauxtime.sleep(self.timeout)
assert len(self.t.keys()) == 100, len(self.t.keys())
fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys())
def testItemsGetExpired(self):
for x in range(10, 110):
self.t[x] = x
......@@ -326,14 +360,14 @@ class TestTransientObjectContainer(TestCase):
fauxtime.sleep(self.timeout * (self.errmargin+1))
assert len(self.t.keys()) == 0, len(self.t.keys())
def testGetItemDelaysTimeout(self):
def testGetDelaysTimeout(self):
for x in range(10, 110):
self.t[x] = x
# current bucket will become old after we sleep for a while.
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]
self.t.get(x)
fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110):
......@@ -344,7 +378,7 @@ class TestTransientObjectContainer(TestCase):
self.t[x] = x
# current bucket will become old after we sleep for a while.
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 setitem
for x in range(10, 110):
self.t[x] = x + 1
fauxtime.sleep(self.timeout/2)
......@@ -352,19 +386,6 @@ class TestTransientObjectContainer(TestCase):
for x in range(10, 110):
assert self.t[x] == x + 1
def testGetDelaysTimeout(self):
for x in range(10, 110):
self.t[x] = x
# current bucket will become old after we sleep for a while.
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)
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
def testLen(self):
added = {}
r = range(10, 1010)
......@@ -428,6 +449,7 @@ def lsubtract(l1, l2):
def test_suite():
testsuite = makeSuite(TestTransientObjectContainer, 'test')
#testsuite = makeSuite(TestBase, 'test')
alltests = TestSuite((testsuite,))
return alltests
......
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