Merge "Make cache region invalidation pluggable"

This commit is contained in:
mike bayer 2016-07-05 18:26:59 -04:00 committed by Gerrit Code Review
commit ad4b36f24f
3 changed files with 255 additions and 31 deletions

View File

@ -4,6 +4,18 @@ Changelog
.. changelog:: .. changelog::
:version: 0.6.2 :version: 0.6.2
.. change::
:tags: feature
:tickets: 38
Added a new system to allow custom plugins specific to the issue of
"invalidate the entire region", using a new base class
:class:`.RegionInvalidationStrategy`. As there are many potential
strategies to this (special backend function, storing special keys, etc.)
the mechanism for both soft and hard invalidation is now customizable.
New approaches to region invalidation can be contributed as documented
recipes. Pull request courtesy Alexander Makarov.
.. change:: .. change::
:tags: feature :tags: feature
:tickets: 43 :tickets: 43

View File

@ -25,6 +25,168 @@ values from a previous, backwards-incompatible version.
""" """
class RegionInvalidationStrategy(object):
"""Region invalidation strategy interface
Implement this interface and pass implementation instance
to :meth:`.CacheRegion.configure` to override default region invalidation.
Example::
class CustomInvalidationStrategy(RegionInvalidationStrategy):
def __init__(self):
self._soft_invalidated = None
self._hard_invalidated = None
def invalidate(self, hard=None):
if hard:
self._soft_invalidated = None
self._hard_invalidated = time.time()
else:
self._soft_invalidated = time.time()
self._hard_invalidated = None
def is_invalidated(self, timestamp):
return ((self._soft_invalidated and
timestamp < self._soft_invalidated) or
(self._hard_invalidated and
timestamp < self._hard_invalidated))
def was_hard_invalidated(self):
return bool(self._hard_invalidated)
def is_hard_invalidated(self, timestamp):
return (self._hard_invalidated and
timestamp < self._hard_invalidated)
def was_soft_invalidated(self):
return bool(self._soft_invalidated)
def is_soft_invalidated(self, timestamp):
return (self._soft_invalidated and
timestamp < self._soft_invalidated)
The custom implementation is injected into a :class:`.CacheRegion`
at configure time using the
:paramref:`.CacheRegion.configure.region_invalidator` parameter::
region = CacheRegion()
region = region.configure(region_invalidator=CustomInvalidationStrategy())
Invalidation strategies that wish to have access to the
:class:`.CacheRegion` itself should construct the invalidator given the
region as an argument::
class MyInvalidator(RegionInvalidationStrategy):
def __init__(self, region):
self.region = region
# ...
# ...
region = CacheRegion()
region = region.configure(region_invalidator=MyInvalidator(region))
.. versionadded:: 0.6.2
.. seealso::
:paramref:`.CacheRegion.configure.region_invalidator`
"""
def invalidate(self, hard=True):
"""Region invalidation.
:class:`.CacheRegion` propagated call.
The default invalidation system works by setting
a current timestamp (using ``time.time()``) to consider all older
timestamps effectively invalidated.
"""
raise NotImplementedError()
def is_hard_invalidated(self, timestamp):
"""Check timestamp to determine if it was hard invalidated.
:return: Boolean. True if ``timestamp`` is older than
the last region invalidation time and region is invalidated
in hard mode.
"""
raise NotImplementedError()
def is_soft_invalidated(self, timestamp):
"""Check timestamp to determine if it was soft invalidated.
:return: Boolean. True if ``timestamp`` is older than
the last region invalidation time and region is invalidated
in soft mode.
"""
raise NotImplementedError()
def is_invalidated(self, timestamp):
"""Check timestamp to determine if it was invalidated.
:return: Boolean. True if ``timestamp`` is older than
the last region invalidation time.
"""
raise NotImplementedError()
def was_soft_invalidated(self):
"""Indicate the region was invalidated in soft mode.
:return: Boolean. True if region was invalidated in soft mode.
"""
raise NotImplementedError()
def was_hard_invalidated(self):
"""Indicate the region was invalidated in hard mode.
:return: Boolean. True if region was invalidated in hard mode.
"""
raise NotImplementedError()
class DefaultInvalidationStrategy(RegionInvalidationStrategy):
def __init__(self):
self._is_hard_invalidated = None
self._invalidated = None
def invalidate(self, hard=True):
self._is_hard_invalidated = bool(hard)
self._invalidated = time.time()
def is_invalidated(self, timestamp):
return (self._invalidated is not None and
timestamp < self._invalidated)
def was_hard_invalidated(self):
return self._is_hard_invalidated is True
def is_hard_invalidated(self, timestamp):
return self.was_hard_invalidated() and self.is_invalidated(timestamp)
def was_soft_invalidated(self):
return self._is_hard_invalidated is False
def is_soft_invalidated(self, timestamp):
return self.was_soft_invalidated() and self.is_invalidated(timestamp)
class CacheRegion(object): class CacheRegion(object):
"""A front end to a particular cache backend. """A front end to a particular cache backend.
@ -177,9 +339,8 @@ class CacheRegion(object):
self.function_key_generator = function_key_generator self.function_key_generator = function_key_generator
self.function_multi_key_generator = function_multi_key_generator self.function_multi_key_generator = function_multi_key_generator
self.key_mangler = self._user_defined_key_mangler = key_mangler self.key_mangler = self._user_defined_key_mangler = key_mangler
self._hard_invalidated = None
self._soft_invalidated = None
self.async_creation_runner = async_creation_runner self.async_creation_runner = async_creation_runner
self.region_invalidator = DefaultInvalidationStrategy()
def configure( def configure(
self, backend, self, backend,
@ -189,6 +350,7 @@ class CacheRegion(object):
_config_prefix=None, _config_prefix=None,
wrap=None, wrap=None,
replace_existing_backend=False, replace_existing_backend=False,
region_invalidator=None
): ):
"""Configure a :class:`.CacheRegion`. """Configure a :class:`.CacheRegion`.
@ -234,6 +396,11 @@ class CacheRegion(object):
.. versionadded:: 0.5.7 .. versionadded:: 0.5.7
:param region_invalidator: Optional. Override default invalidation
strategy with custom implementation of
:class:`.RegionInvalidationStrategy`.
.. versionadded:: 0.6.2
""" """
@ -270,6 +437,9 @@ class CacheRegion(object):
for wrapper in reversed(wrap): for wrapper in reversed(wrap):
self.wrap(wrapper) self.wrap(wrapper)
if region_invalidator:
self.region_invalidator = region_invalidator
return self return self
def wrap(self, proxy): def wrap(self, proxy):
@ -311,8 +481,8 @@ class CacheRegion(object):
def invalidate(self, hard=True): def invalidate(self, hard=True):
"""Invalidate this :class:`.CacheRegion`. """Invalidate this :class:`.CacheRegion`.
Invalidation works by setting a current timestamp The default invalidation system works by setting
(using ``time.time()``) a current timestamp (using ``time.time()``)
representing the "minimum creation time" for representing the "minimum creation time" for
a value. Any retrieved value whose creation a value. Any retrieved value whose creation
time is prior to this timestamp time is prior to this timestamp
@ -345,12 +515,7 @@ class CacheRegion(object):
.. versionadded:: 0.5.1 .. versionadded:: 0.5.1
""" """
if hard: self.region_invalidator.invalidate(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): def configure_from_config(self, config_dict, prefix):
"""Configure from a configuration dictionary """Configure from a configuration dictionary
@ -473,16 +638,14 @@ class CacheRegion(object):
current_time = time.time() current_time = time.time()
invalidated = self._hard_invalidated or self._soft_invalidated
def value_fn(value): def value_fn(value):
if value is NO_VALUE: if value is NO_VALUE:
return value return value
elif expiration_time is not None and \ elif expiration_time is not None and \
current_time - value.metadata["ct"] > expiration_time: current_time - value.metadata["ct"] > expiration_time:
return NO_VALUE return NO_VALUE
elif invalidated and \ elif self.region_invalidator.is_invalidated(
value.metadata["ct"] < invalidated: value.metadata["ct"]):
return NO_VALUE return NO_VALUE
else: else:
return value return value
@ -615,16 +778,13 @@ class CacheRegion(object):
def get_value(): def get_value():
value = self.backend.get(key) value = self.backend.get(key)
if value is NO_VALUE or \ if (value is NO_VALUE or value.metadata['v'] != value_version or
value.metadata['v'] != value_version or \ self.region_invalidator.is_hard_invalidated(
( value.metadata["ct"])):
self._hard_invalidated and
value.metadata["ct"] < self._hard_invalidated):
raise NeedRegenerationException() raise NeedRegenerationException()
ct = value.metadata["ct"] ct = value.metadata["ct"]
if self._soft_invalidated: if self.region_invalidator.is_soft_invalidated(ct):
if ct < self._soft_invalidated: ct = time.time() - expiration_time - .0001
ct = time.time() - expiration_time - .0001
return value.payload, ct return value.payload, ct
@ -641,7 +801,8 @@ class CacheRegion(object):
if expiration_time is None: if expiration_time is None:
expiration_time = self.expiration_time expiration_time = self.expiration_time
if expiration_time is None and self._soft_invalidated: if (expiration_time is None and
self.region_invalidator.was_soft_invalidated()):
raise exception.DogpileCacheException( raise exception.DogpileCacheException(
"Non-None expiration time required " "Non-None expiration time required "
"for soft invalidation") "for soft invalidation")
@ -714,19 +875,17 @@ class CacheRegion(object):
def get_value(key): def get_value(key):
value = values.get(key, NO_VALUE) value = values.get(key, NO_VALUE)
if value is NO_VALUE or \ if (value is NO_VALUE or value.metadata['v'] != value_version or
value.metadata['v'] != value_version or \ self.region_invalidator.is_hard_invalidated(
(self._hard_invalidated and value.metadata['v'])):
value.metadata["ct"] < self._hard_invalidated):
# dogpile.core understands a 0 here as # dogpile.core understands a 0 here as
# "the value is not available", e.g. # "the value is not available", e.g.
# _has_value() will return False. # _has_value() will return False.
return value.payload, 0 return value.payload, 0
else: else:
ct = value.metadata["ct"] ct = value.metadata["ct"]
if self._soft_invalidated: if self.region_invalidator.is_soft_invalidated(ct):
if ct < self._soft_invalidated: ct = time.time() - expiration_time - .0001
ct = time.time() - expiration_time - .0001
return value.payload, ct return value.payload, ct
@ -739,7 +898,8 @@ class CacheRegion(object):
if expiration_time is None: if expiration_time is None:
expiration_time = self.expiration_time expiration_time = self.expiration_time
if expiration_time is None and self._soft_invalidated: if (expiration_time is None and
self.region_invalidator.was_soft_invalidated()):
raise exception.DogpileCacheException( raise exception.DogpileCacheException(
"Non-None expiration time required " "Non-None expiration time required "
"for soft invalidation") "for soft invalidation")

View File

@ -4,6 +4,7 @@ from dogpile.cache import exception
from dogpile.cache import make_region, CacheRegion from dogpile.cache import make_region, CacheRegion
from dogpile.cache.proxy import ProxyBackend from dogpile.cache.proxy import ProxyBackend
from dogpile.cache.region import _backend_loader from dogpile.cache.region import _backend_loader
from dogpile.cache.region import RegionInvalidationStrategy
from . import eq_, is_, assert_raises_message, io, configparser from . import eq_, is_, assert_raises_message, io, configparser
import time import time
import datetime import datetime
@ -425,6 +426,57 @@ class ProxyRegionTest(RegionTest):
return reg return reg
class CustomInvalidationStrategyTest(RegionTest):
"""Try region tests with custom invalidation strategy.
This is exactly the same as the region test above, but it uses custom
invalidation strategy. The purpose of this is to make sure the tests
still run successfully even when there is a proxy.
"""
class CustomInvalidationStrategy(RegionInvalidationStrategy):
def __init__(self):
self._soft_invalidated = None
self._hard_invalidated = None
def invalidate(self, hard=None):
if hard:
self._soft_invalidated = None
self._hard_invalidated = time.time()
else:
self._soft_invalidated = time.time()
self._hard_invalidated = None
def is_invalidated(self, timestamp):
return ((self._soft_invalidated and
timestamp < self._soft_invalidated) or
(self._hard_invalidated and
timestamp < self._hard_invalidated))
def was_hard_invalidated(self):
return bool(self._hard_invalidated)
def is_hard_invalidated(self, timestamp):
return (self._hard_invalidated and
timestamp < self._hard_invalidated)
def was_soft_invalidated(self):
return bool(self._soft_invalidated)
def is_soft_invalidated(self, timestamp):
return (self._soft_invalidated and
timestamp < self._soft_invalidated)
def _region(self, init_args={}, config_args={}, backend="mock"):
reg = CacheRegion(**init_args)
invalidator = self.CustomInvalidationStrategy()
reg.configure(backend, region_invalidator=invalidator, **config_args)
return reg
class ProxyBackendTest(TestCase): class ProxyBackendTest(TestCase):
class GetCounterProxy(ProxyBackend): class GetCounterProxy(ProxyBackend):