Mike Bayer avatar Mike Bayer committed eab8371

- get() method now checks expiration time
by default. Use ignore_expiration=True
to bypass this.

- Added new invalidate() method. Sets the current
timestamp as a minimum value that all retrieved
values must be created after. Is honored by the
get_or_create() and get() methods. #7

Comments (0)

Files changed (6)

+0.3.0
+=====
+- get() method now checks expiration time 
+  by default.   Use ignore_expiration=True
+  to bypass this.
+
+- Added new invalidate() method.  Sets the current
+  timestamp as a minimum value that all retrieved
+  values must be created after.  Is honored by the
+  get_or_create() and get() methods. #7
+
 0.2.4
 =====
 - Fixed py3k issue with config string coerce,

dogpile/cache/__init__.py

-__version__ = '0.2.4'
+__version__ = '0.3.0'
 
 from .region import CacheRegion, register_backend, make_region

dogpile/cache/region.py

             self.key_mangler = key_mangler
         else:
             self.key_mangler = None
+        self._invalidated = None
 
     def configure(self, backend,
             expiration_time=None,
                 lock=self.backend.get_mutex(identifier)
             )
 
+    def invalidate(self):
+        """Invalidate this :class:`.CacheRegion`.
+        
+        Invalidation works by setting a current timestamp
+        (using ``time.time()``)
+        representing the "minimum creation time" for 
+        a value.  Any retrieved value whose creation
+        time is prior to this timestamp 
+        is considered to be stale.  It does not
+        affect the data in the cache in any way, and is also
+        local to this instance of :class:`.CacheRegion`.
+        
+        Once set, the invalidation time is honored by
+        the :meth:`.CacheRegion.get_or_create` and 
+        :meth:`.CacheRegion.get` methods.
+        
+        .. versionadded:: 0.3.0
+        
+        """
+        self._invalidated = time.time()
+
     def configure_from_config(self, config_dict, prefix):
         """Configure from a configuration dictionary 
         and a prefix.
     def backend(self):
         raise Exception("No backend is configured on this region.")
 
-    def get(self, key):
+    def get(self, key, expiration_time=None, ignore_expiration=False):
         """Return a value from the cache, based on the given key.
 
-        While it's typical the key is a string, it's 
-        passed through to the underlying backend so can be 
-        of any type recognized by the backend. If
-        the value is not present, returns the token 
-        ``NO_VALUE``. ``NO_VALUE`` evaluates to False, but is 
-        separate from ``None`` to distinguish between a cached value 
-        of ``None``. Note that the ``expiration_time`` argument is 
-        **not** used here - this method is a direct line to the
-        backend's behavior. 
+        If the value is not present, the method returns the token
+        ``NO_VALUE``. ``NO_VALUE`` evaluates to False, but is separate from
+        ``None`` to distinguish between a cached value of ``None``.
+
+        By default, the configured expiration time of the
+        :class:`.CacheRegion`, or alternatively the expiration
+        time supplied by the ``expiration_time`` argument,
+        is tested against the creation time of the retrieved
+        value versus the current time (as reported by ``time.time()``).
+        If stale, the cached value is ignored and the ``NO_VALUE`` 
+        token is returned.  Passing the flag ``ignore_expiration=True``
+        bypasses the expiration time check.
+        
+        .. versionchanged:: 0.3.0
+           :meth:`.CacheRegion.get` now checks the value's creation time
+           against the expiration time, rather than returning
+           the value unconditionally.
+        
+        The method also interprets the cached value in terms
+        of the current "invalidation" time as set by 
+        the :meth:`.invalidate` method.   If a value is present,
+        but its creation time is older than the current 
+        invalidation time, the ``NO_VALUE`` token is returned.
+        Passing the flag ``ignore_expiration=True`` bypasses
+        the invalidation time check.
+        
+        .. versionadded:: 0.3.0 
+           Support for the :meth:`.CacheRegion.invalidate`
+           method.
+        
+        :param key: Key to be retrieved. While it's typical for a key to be a
+         string, it is ultimately passed directly down to the cache backend,
+         before being optionally processed by the key_mangler function, so can
+         be of any type recognized by the backend or by the key_mangler
+         function, if present.
+
+        :param expiration_time: Optional expiration time value 
+         which will supersede that configured on the :class:`.CacheRegion`
+         itself.
+         
+         .. versionadded:: 0.3.0
+         
+        :param ignore_expiration: if ``True``, the value is returned
+         from the cache if present, regardless of configured
+         expiration times or whether or not :meth:`.invalidate`
+         was called.
+         
+         .. versionadded:: 0.3.0
 
         """
 
         if self.key_mangler:
             key = self.key_mangler(key)
         value = self.backend.get(key)
+
+        if not ignore_expiration:
+            if expiration_time is None:
+                expiration_time = self.expiration_time
+            if expiration_time is not None and \
+                time.time() - value.metadata["ct"] > expiration_time:
+                return NO_VALUE
+            elif self._invalidated and value.metadata["ct"] < self._invalidated:
+                return NO_VALUE
+
         return value.payload
 
     def get_or_create(self, key, creator, expiration_time=None):
-        """Similar to ``get``, will use the given "creation" 
-        function to create a new
-        value if the value does not exist.
+        """Return a cached value based on the given key.  
+        
+        If the value does not exist or is considered to be expired
+        based on its creation time, the given 
+        creation function may or may not be used to recreate the value
+        and persist the newly generated value in the cache.
+        
+        Whether or not the function is used depends on if the
+        *dogpile lock* can be acquired or not.  If it can't, it means
+        a different thread or process is already running a creation
+        function for this key against the cache.  When the dogpile
+        lock cannot be acquired, the method will block if no
+        previous value is available, until the lock is released and
+        a new value available.  If a previous value 
+        is available, that value is returned immediately without blocking.
+        
+        If the :meth:`.invalidate` method has been called, and 
+        the retrieved value's timestamp is older than the invalidation
+        timestamp, the value is unconditionally prevented from
+        being returned.  The method will attempt to acquire the dogpile 
+        lock to generate a new value, or will wait
+        until the lock is released to return the new value.
 
-        This will use the underlying dogpile/
-        expiration mechanism to determine when/how 
-        the creation function is called.
+        .. versionchanged:: 0.3.0
+          The value is unconditionally regenerated if the creation
+          time is older than the last call to :meth:`.invalidate`.
 
-        :param key: Key to retrieve
+        :param key: Key to be retrieved. While it's typical for a key to be a
+         string, it is ultimately passed directly down to the cache backend,
+         before being optionally processed by the key_mangler function, so can
+         be of any type recognized by the backend or by the key_mangler
+         function, if present.
+
         :param creator: function which creates a new value.
         :param expiration_time: optional expiration time which will overide
          the expiration time already configured on this :class:`.CacheRegion`
             the :class:`.CacheRegion` as a whole is expected to be the
             usual mode of operation.
 
+        See also:
+        
+        :meth:`.CacheRegion.cache_on_arguments` - applies :meth:`.get_or_create`
+        to any function using a decorator.
+         
         """
         if self.key_mangler:
             key = self.key_mangler(key)
         def get_value():
             value = self.backend.get(key)
             if value is NO_VALUE or \
-                value.metadata['v'] != value_version:
+                value.metadata['v'] != value_version or \
+                    (self._invalidated and 
+                    value.metadata["ct"] < self._invalidated):
                 raise NeedRegenerationException()
             return value.payload, value.metadata["ct"]
 
         value of the function using a key derived from the 
         function itself and its arguments.
         
+        The decorator internally makes use of the
+        :meth:`.CacheRegion.get_or_create` method to access the
+        cache and conditionally call the function.  See that 
+        method for additional behavioral details.
+        
         E.g.::
         
             @someregion.cache_on_arguments()

tests/cache/__init__.py

     """Assert a == b, with repr messaging on failure."""
     assert a == b, msg or "%r != %r" % (a, b)
 
+def is_(a, b, msg=None):
+    """Assert a is b, with repr messaging on failure."""
+    assert a is b, msg or "%r is not %r" % (a, b)
+
 def ne_(a, b, msg=None):
     """Assert a != b, with repr messaging on failure."""
     assert a != b, msg or "%r == %r" % (a, b)

tests/cache/_fixtures.py

             return "some value %d" % next(counter)
         eq_(reg.get_or_create("some key", creator), "some value 1")
         time.sleep(.4)
-        eq_(reg.get("some key"), "some value 1")
+        eq_(reg.get("some key", ignore_expiration=True), "some value 1")
         eq_(reg.get_or_create("some key", creator), "some value 2")
         eq_(reg.get("some key"), "some value 2")
 

tests/cache/test_region.py

 from unittest import TestCase
 from dogpile.cache.api import CacheBackend, CachedValue, NO_VALUE
 from dogpile.cache import make_region, register_backend, CacheRegion, util
-from . import eq_, assert_raises_message, io, configparser
+from . import eq_, is_, assert_raises_message, io, configparser
 import time
 import itertools
 
             return "some value %d" % next(counter)
         eq_(reg.get_or_create("some key", creator), "some value 1")
         time.sleep(2)
-        eq_(reg.get("some key"), "some value 1")
+        is_(reg.get("some key"), NO_VALUE)
+        eq_(reg.get("some key", ignore_expiration=True), "some value 1")
         eq_(reg.get_or_create("some key", creator), "some value 2")
         eq_(reg.get("some key"), "some value 2")
 
+    def test_expire_on_get(self):
+        reg = self._region(config_args={"expiration_time":.5})
+        reg.set("some key", "some value")
+        eq_(reg.get("some key"), "some value")
+        time.sleep(1)
+        is_(reg.get("some key"), NO_VALUE)
+
+    def test_ignore_expire_on_get(self):
+        reg = self._region(config_args={"expiration_time":.5})
+        reg.set("some key", "some value")
+        eq_(reg.get("some key"), "some value")
+        time.sleep(1)
+        eq_(reg.get("some key", ignore_expiration=True), "some value")
+
+    def test_override_expire_on_get(self):
+        reg = self._region(config_args={"expiration_time":.5})
+        reg.set("some key", "some value")
+        eq_(reg.get("some key"), "some value")
+        time.sleep(1)
+        eq_(reg.get("some key", expiration_time=5), "some value")
+        is_(reg.get("some key"), NO_VALUE)
+
     def test_expire_override(self):
         reg = self._region(config_args={"expiration_time":5})
         counter = itertools.count(1)
                     "some value 2")
         eq_(reg.get("some key"), "some value 2")
 
+
+    def test_invalidate_get(self):
+        reg = self._region()
+        reg.set("some key", "some value")
+        reg.invalidate()
+        is_(reg.get("some key"), NO_VALUE)
+
+    def test_invalidate_get_or_create(self):
+        reg = self._region()
+        counter = itertools.count(1)
+        def creator():
+            return "some value %d" % next(counter)
+        eq_(reg.get_or_create("some key", creator), 
+                    "some value 1")
+
+        reg.invalidate()
+        eq_(reg.get_or_create("some key", creator), 
+                    "some value 2")
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.