Commits

Mike Bayer committed 138d3d7

- The :meth:`.CacheRegion.invalidate` method now supports an option
``hard=True|False``. A "hard" invalidation, equivalent to the
existing functionality of :meth:`.CacheRegion.invalidate`, means
:meth:`.CacheRegion.get_or_create` will not return the "old" value at
all, forcing all getters to regenerate or wait for a regeneration.
"soft" invalidation means that getters can continue to return the
old value until a new one is generated. #38

Comments (0)

Files changed (3)

docs/build/changelog.rst

 
     .. change::
         :tags: feature
+        :tickets: 38
+
+      The :meth:`.CacheRegion.invalidate` method now supports an option
+      ``hard=True|False``.  A "hard" invalidation, equivalent to the
+      existing functionality of :meth:`.CacheRegion.invalidate`, means
+      :meth:`.CacheRegion.get_or_create` will not return the "old" value at
+      all, forcing all getters to regenerate or wait for a regeneration.
+      "soft" invalidation means that getters can continue to return the
+      old value until a new one is generated.
+
+    .. change::
+        :tags: feature
         :tickets: 40
 
       New dogpile-specific exception classes have been added, so that

dogpile/cache/region.py

             self.key_mangler = key_mangler
         else:
             self.key_mangler = None
-        self._invalidated = None
+        self._hard_invalidated = None
+        self._soft_invalidated = None
         self.async_creation_runner = async_creation_runner
 
     def configure(self, backend,
         else:
             return self._LockWrapper()
 
-    def invalidate(self):
+    def invalidate(self, hard=True):
         """Invalidate this :class:`.CacheRegion`.
 
         Invalidation works by setting a current timestamp
         local to this instance of :class:`.CacheRegion`.
 
         Once set, the invalidation time is honored by
-        the :meth:`.CacheRegion.get_or_create` and
+        the :meth:`.CacheRegion.get_or_create`,
+        :meth:`.CacheRegion.get_or_create_multi` and
         :meth:`.CacheRegion.get` methods.
 
+        The method
+        supports both "hard" and "soft" invalidation options.  With "hard"
+        invalidation, :meth:`.CacheRegion.get_or_create` will force an immediate
+        regeneration of the value which all getters will wait for.  With
+        "soft" invalidation, subsequent getters will return the "old" value until
+        the new one is available.
+
+        Usage of "soft" invalidation requires that the region or the method
+        is given a non-None expiration time.
+
         .. versionadded:: 0.3.0
 
+        :param hard: if True, cache values will all require immediate
+         regeneration; dogpile logic won't be used.  If False, the
+         creation time of existing values will be pushed back before
+         the expiration time so that a return+regen will be invoked.
+
+         .. versionadded:: 0.5.1
+
         """
-        self._invalidated = time.time()
+        if hard:
+            self._hard_invalidated = time.time()
+            self._soft_invalidated = None
+        else:
+            self._hard_invalidated = None
+            self._soft_invalidated = time.time()
 
     def configure_from_config(self, config_dict, prefix):
         """Configure from a configuration dictionary
 
             current_time = time.time()
 
+            invalidated = self._hard_invalidated or self._soft_invalidated
             def value_fn(value):
                 if value is NO_VALUE:
                     return value
                 elif expiration_time is not None and \
                       current_time - value.metadata["ct"] > expiration_time:
                     return NO_VALUE
-                elif self._invalidated and \
-                        value.metadata["ct"] < self._invalidated:
+                elif invalidated and \
+                        value.metadata["ct"] < invalidated:
                     return NO_VALUE
                 else:
                     return value
             value = self.backend.get(key)
             if value is NO_VALUE or \
                 value.metadata['v'] != value_version or \
-                    (self._invalidated and
-                    value.metadata["ct"] < self._invalidated):
+                    (self._hard_invalidated and
+                    value.metadata["ct"] < self._hard_invalidated):
                 raise NeedRegenerationException()
-            return value.payload, value.metadata["ct"]
+            ct = value.metadata["ct"]
+            if self._soft_invalidated:
+                if ct < self._soft_invalidated:
+                    ct = time.time() - expiration_time
+
+            return value.payload, ct
 
         def gen_value():
             created_value = creator()
         if expiration_time is None:
             expiration_time = self.expiration_time
 
+        if expiration_time is None and self._soft_invalidated:
+            raise exception.DogpileCacheException(
+                    "Non-None expiration time required "
+                    "for soft invalidation")
+
         if self.async_creation_runner:
             def async_creator(mutex):
                 return self.async_creation_runner(self, key, creator, mutex)
 
         def get_value(key):
             value = values.get(key, NO_VALUE)
+
             if value is NO_VALUE or \
                 value.metadata['v'] != value_version or \
-                    (self._invalidated and
-                    value.metadata["ct"] < self._invalidated):
+                    (self._hard_invalidated and
+                    value.metadata["ct"] < self._hard_invalidated):
+                # dogpile.core understands a 0 here as
+                # "the value is not available", e.g.
+                # _has_value() will return False.
                 return value.payload, 0
             else:
-                return value.payload, value.metadata["ct"]
+                ct = value.metadata["ct"]
+                if self._soft_invalidated:
+                    if ct < self._soft_invalidated:
+                        ct = time.time() - expiration_time
+
+                return value.payload, ct
 
         def gen_value():
             raise NotImplementedError()
         if expiration_time is None:
             expiration_time = self.expiration_time
 
+        if expiration_time is None and self._soft_invalidated:
+            raise exception.DogpileCacheException(
+                    "Non-None expiration time required "
+                    "for soft invalidation")
+
         mutexes = {}
 
         sorted_unique_keys = sorted(set(keys))

tests/cache/test_region.py

         eq_(reg.get("some key"), "some value 2")
 
 
-    def test_invalidate_get(self):
+    def test_hard_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):
+    def test_hard_invalidate_get_or_create(self):
         reg = self._region()
         counter = itertools.count(1)
         def creator():
         eq_(reg.get_or_create("some key", creator),
                     "some value 2")
 
+    def test_soft_invalidate_get(self):
+        reg = self._region(config_args={"expiration_time": 1})
+        reg.set("some key", "some value")
+        reg.invalidate(hard=False)
+        is_(reg.get("some key"), NO_VALUE)
+
+    def test_soft_invalidate_get_or_create(self):
+        reg = self._region(config_args={"expiration_time": 1})
+        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(hard=False)
+        eq_(reg.get_or_create("some key", creator),
+                    "some value 2")
+
+    def test_soft_invalidate_get_or_create_multi(self):
+        reg = self._region(config_args={"expiration_time": 5})
+        values = [1, 2, 3]
+        def creator(*keys):
+            v = values.pop(0)
+            return [v for k in keys]
+        ret = reg.get_or_create_multi(
+                    [1, 2], creator)
+        eq_(ret, [1, 1])
+        reg.invalidate(hard=False)
+        ret = reg.get_or_create_multi(
+                    [1, 2], creator)
+        eq_(ret, [2, 2])
+
+    def test_soft_invalidate_requires_expire_time_get(self):
+        reg = self._region()
+        reg.invalidate(hard=False)
+        assert_raises_message(
+            exception.DogpileCacheException,
+            "Non-None expiration time required for soft invalidation",
+            reg.get_or_create, "some key", lambda: "x"
+        )
+
+    def test_soft_invalidate_requires_expire_time_get_multi(self):
+        reg = self._region()
+        reg.invalidate(hard=False)
+        assert_raises_message(
+            exception.DogpileCacheException,
+            "Non-None expiration time required for soft invalidation",
+            reg.get_or_create_multi, ["k1", "k2"], lambda k: "x"
+        )
+
     def test_should_cache_fn(self):
         reg = self._region()
         values = [1, 2, 3]
         eq_(ret, 3)
         eq_(reg.backend._cache['some key'][0], 3)
 
+
     def test_should_cache_fn_multi(self):
         reg = self._region()
         values = [1, 2, 3]