diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-11-17 14:16:10 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2011-11-17 14:16:10 -0500 |
commit | e3f1bcfdc9ec1ab9acf09e3cfa366a44033da5e4 (patch) | |
tree | d7a29ccf718e752d38f4be33e93a676b21ae1b83 /dogpile | |
parent | 4fa66beb0cfeec213ba9ef5dc9606cb2abe6de57 (diff) | |
download | dogpile-core-e3f1bcfdc9ec1ab9acf09e3cfa366a44033da5e4.tar.gz |
docs
Diffstat (limited to 'dogpile')
-rw-r--r-- | dogpile/dogpile.py | 126 | ||||
-rw-r--r-- | dogpile/nameregistry.py | 38 | ||||
-rw-r--r-- | dogpile/readwrite_lock.py | 16 |
3 files changed, 87 insertions, 93 deletions
diff --git a/dogpile/dogpile.py b/dogpile/dogpile.py index 94a50c8..f6b1d11 100644 --- a/dogpile/dogpile.py +++ b/dogpile/dogpile.py @@ -1,72 +1,7 @@ -"""A "dogpile" lock, one which allows a single thread to generate -an expensive resource while other threads use the "old" value, until the -"new" value is ready. - -Usage:: - - # store a reference to a "resource", some - # object that is expensive to create. - the_resource = [None] - - def some_creation_function(): - # create the resource here - the_resource[0] = create_some_resource() - - def use_the_resource(): - # some function that uses - # the resource. Won't reach - # here until some_creation_function() - # has completed at least once. - the_resource[0].do_something() - - # create Dogpile with 3600 second - # expiry time - dogpile = Dogpile(3600) - - with dogpile.acquire(some_creation_function): - use_the_resource() - -Above, ``some_creation_function()`` will be called -when :meth:`.Dogpile.acquire` is first called. The -block then proceeds. Concurrent threads which -call :meth:`.Dogpile.acquire` during this initial period -will block until ``some_creation_function()`` completes. - -Once the creation function has completed successfully, -new calls to :meth:`.Dogpile.acquire` will route a single -thread into new calls of ``some_creation_function()`` -each time the expiration time is reached. Concurrent threads -which call :meth:`.Dogpile.acquire` during this period will -fall through, and not be blocked. It is expected that -the "stale" version of the resource remain available at this -time while the new one is generated. - -The dogpile lock can also provide a mutex to the creation -function itself, so that the creation function can perform -certain tasks only after all "stale reader" threads have finished. -The example of this is when the creation function has prepared a new -datafile to replace the old one, and would like to switch in the -"new" file only when other threads have finished using it. - -To enable this feature, use :class:`.SyncReaderDogpile`. -Then use :meth:`.SyncReaderDogpile.acquire_write_lock` for the critical section -where readers should be blocked:: - - from dogpile import SyncReaderDogpile - - dogpile = SyncReaderDogpile(3600) - - def some_creation_function(): - create_expensive_datafile() - with dogpile.acquire_write_lock(): - replace_old_datafile_with_new() - -""" from util import thread, threading import time import logging from readwrite_lock import ReadWriteMutex -from nameregistry import NameRegistry log = logging.getLogger(__name__) @@ -80,36 +15,41 @@ class NeedRegenerationException(Exception): NOT_REGENERATED = object() class Dogpile(object): - """Dogpile class. + """Dogpile lock class. - :param expiretime: Expiration time in seconds. + Provides an interface around an arbitrary mutex that allows one + thread/process to be elected as the creator of a new value, + while other threads/processes continue to return the previous version + of that value. """ - def __init__(self, expiretime, init=False): - self.dogpilelock = threading.Lock() + def __init__(self, expiretime, init=False, lock=None): + """Construct a new :class:`.Dogpile`. + + :param expiretime: Expiration time in seconds. + :param init: if True, set the 'createdtime' to the + current time. + :param lock: a mutex object that provides + ``acquire()`` and ``release()`` methods. + + """ + if lock: + self.dogpilelock = lock + else: + self.dogpilelock = threading.Lock() self.expiretime = expiretime if init: self.createdtime = time.time() - else: - self.createdtime = -1 - - @clasmethod - def registry(cls, *arg, **kw): - """Return a name-based registry of :class:`.Dogpile` objects. - - The registry is an instance of :class:`.NameRegistry`, - and calling its ``get()`` method with an identifying - key (anything hashable) will construct a new :class:`.Dogpile` - object, keyed to that key. Subsequent usages will return - the same :class:`.Dogpile` object for as long as the - object remains in scope. - The given arguments are passed along to the underlying - constructor of the :class:`.Dogpile` class. + createdtime = -1 + """The last known 'creation time' of the value, + stored as an epoch (i.e. from ``time.time()``). - """ - return NameRegistry(lambda identifier: cls(*arg, **kw)) + If the value here is -1, it is assumed the value + should recreate immediately. + + """ def acquire(self, creator, value_fn=None, @@ -129,8 +69,7 @@ class Dogpile(object): lock. This option removes the need for the dogpile lock itself to remain persistent across usages; another dogpile can come along later and pick up where the - previous one left off. Should be used in conjunction - with a :class:`.NameRegistry`. + previous one left off. """ dogpile = self @@ -214,11 +153,22 @@ class Dogpile(object): pass class SyncReaderDogpile(Dogpile): + """Provide a read-write lock function on top of the :class:`.Dogpile` + class. + + """ def __init__(self, *args, **kw): super(SyncReaderDogpile, self).__init__(*args, **kw) self.readwritelock = ReadWriteMutex() def acquire_write_lock(self): + """Return the "write" lock context manager. + + This will provide a section that is mutexed against + all readers/writers for the dogpile-maintained value. + + """ + dogpile = self class Lock(object): def __enter__(self): diff --git a/dogpile/nameregistry.py b/dogpile/nameregistry.py index cedcf52..159526e 100644 --- a/dogpile/nameregistry.py +++ b/dogpile/nameregistry.py @@ -10,6 +10,8 @@ class NameRegistry(object): class MyFoo(object): "some important object." + def __init__(self, identifier): + self.identifier = identifier registry = NameRegistry(MyFoo) @@ -19,24 +21,52 @@ class NameRegistry(object): # thread 2 my_foo = registry.get("foo1") - Above, "my_foo" in both thread #1 and #2 will - be *the same object*. + Above, ``my_foo`` in both thread #1 and #2 will + be *the same object*. The constructor for + ``MyFoo`` will be called once, passing the + identifier ``foo1`` as the argument. When thread 1 and thread 2 both complete or - otherwise delete references to "my_foo", the - object is *removed* from the NameRegistry as + otherwise delete references to ``my_foo``, the + object is *removed* from the :class:`.NameRegistry` as a result of Python garbage collection. + :class:`.NameRegistry` is a utility object that + is used to maintain new :class:`.Dogpile` objects + against a certain key, for as long as that particular key + is referenced within the application. An application + can deal with an arbitrary number of keys, ensuring that + all threads requesting a certain key use the same + :class:`.Dogpile` object, without the need to maintain + each :class:`.Dogpile` object persistently in memory. + """ _locks = weakref.WeakValueDictionary() _mutex = threading.RLock() def __init__(self, creator): + """Create a new :class:`.NameRegistry`. + + :param creator: A function that will create a new + value, given the identifier passed to the :meth:`.NameRegistry.get` + method. + + """ self._values = weakref.WeakValueDictionary() self._mutex = threading.RLock() self.creator = creator def get(self, identifier, *args, **kw): + """Get and possibly create the value. + + :param identifier: Hash key for the value. + If the creation function is called, this identifier + will also be passed to the creation function. + :param \*args, \**kw: Additional arguments which will + also be passed to the creation function if it is + called. + + """ try: if identifier in self._values: return self._values[identifier] diff --git a/dogpile/readwrite_lock.py b/dogpile/readwrite_lock.py index 51498f6..0c9cc2b 100644 --- a/dogpile/readwrite_lock.py +++ b/dogpile/readwrite_lock.py @@ -4,7 +4,17 @@ except ImportError: import dummy_threading as threading class ReadWriteMutex(object): - """A mutex which allows multiple readers, single writer.""" + """A mutex which allows multiple readers, single writer. + + :class:`.ReadWriteMutex` uses a Python ``threading.Condition`` + to provide this functionality across threads within a process. + + The Beaker package also contained a file-lock based version + of this concept, so that readers/writers could be synchronized + across processes with a common filesystem. A future Dogpile + release may include this additional class at some point. + + """ def __init__(self): # counts how many asynchronous methods are executing @@ -17,6 +27,7 @@ class ReadWriteMutex(object): self.condition = threading.Condition(threading.Lock()) def acquire_read_lock(self, wait = True): + """Acquire the 'read' lock.""" self.condition.acquire() try: # see if a synchronous operation is waiting to start @@ -37,6 +48,7 @@ class ReadWriteMutex(object): return True def release_read_lock(self): + """Release the 'read' lock.""" self.condition.acquire() try: self.async -= 1 @@ -55,6 +67,7 @@ class ReadWriteMutex(object): self.condition.release() def acquire_write_lock(self, wait = True): + """Acquire the 'write' lock.""" self.condition.acquire() try: # here, we are not a synchronous reader, and after returning, @@ -91,6 +104,7 @@ class ReadWriteMutex(object): return True def release_write_lock(self): + """Release the 'write' lock.""" self.condition.acquire() try: if self.current_sync_operation is not threading.currentThread(): |