async def test_should_return_current_value_on_first_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 async def get_value(arg, kwarg=None): return value get_value_cached = memoize( method=get_value, configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=100), expire_after=timedelta(minutes=5))) ) # when res1 = await get_value_cached('test', kwarg='args') time.sleep(.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test', kwarg='args') # then self.assertEqual(0, res1) self.assertEqual(0, res2)
def test_should_return_current_value_on_first_call_after_update_time_reached_but_not_expiration_time( self): # given value = 0 @memoize(configuration=DefaultInMemoryCacheConfiguration( update_after=timedelta(milliseconds=100), expire_after=timedelta(minutes=5))) @gen.coroutine def get_value(arg, kwarg=None): return value # when res1 = yield get_value('test', kwarg='args') time.sleep(.200) yield _ensure_background_tasks_finished() value = 1 # calling thrice be more confident about behaviour of parallel execution res2 = yield [ get_value('test', kwarg='args'), get_value('test', kwarg='args'), get_value('test', kwarg='args'), ] # then self.assertEqual(0, res1) self.assertEqual([0, 0, 0], res2)
def test_should_inform_eviction_strategy_on_entry_updated(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') eviction_strategy = Mock() @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=100))) .set_key_extractor(key_extractor) .set_eviction_strategy(eviction_strategy) ) @gen.coroutine def sample_method(arg, kwarg=None): return arg, kwarg yield sample_method('test', kwarg='args') yield _ensure_background_tasks_finished() time.sleep(.200) eviction_strategy.mark_written.reset_mock() # when yield sample_method('test', kwarg='args') yield _ensure_background_tasks_finished() # then _assert_called_once_with(self, eviction_strategy.mark_written, ('key', AnyObject()), {})
def test_should_release_keys_on_caching_multiple_elements(self): # given value = 0 storage = LocalInMemoryCacheStorage() key_extractor = Mock() key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args[0], kwargs.get('kwarg')))) @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2)) .set_key_extractor(key_extractor) .set_storage(storage) ) @gen.coroutine def get_value(arg, kwarg=None): return value # when yield get_value('test1', kwarg='args1') yield get_value('test2', kwarg='args2') yield get_value('test3', kwarg='args3') yield get_value('test4', kwarg='args4') yield _ensure_background_tasks_finished() # then s1 = yield storage.get("('test1', 'args1')") s2 = yield storage.get("('test2', 'args2')") s3 = yield storage.get("('test3', 'args3')") s4 = yield storage.get("('test4', 'args4')") self.assertIsNone(s1) self.assertIsNone(s2) self.assertIsNotNone(s3) self.assertIsNotNone(s4)
def test_should_return_current_value_on_second_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(milliseconds=100), expire_after=timedelta(minutes=5))) ) @gen.coroutine def get_value(arg, kwarg=None): return value # when res1 = yield get_value('test', kwarg='args') time.sleep(.200) yield _ensure_background_tasks_finished() value = 1 yield get_value('test', kwarg='args') yield _ensure_background_tasks_finished() res2 = yield get_value('test', kwarg='args') # then self.assertEqual(0, res1) self.assertEqual(1, res2)
async def test_should_return_same_value_on_constant_key_function(self): # given value = 0 key_extractor = Mock() key_extractor.format_key = Mock(return_value='lol') async def get_value(arg, kwarg=None): return value get_value_cached = memoize( method=get_value, configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_key_extractor(key_extractor) ) # when res1 = await get_value_cached('test1', kwarg='args1') time.sleep(.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test2', kwarg='args2') # then self.assertEqual(0, res1) self.assertEqual(0, res2)
def test_should_retrieve_entry_to_release_on_entry_added(self): # given key_extractor = Mock() key_extractor.format_key = Mock(side_effect=lambda method, args, kwargs: str((args, kwargs))) eviction_strategy = Mock() eviction_strategy.next_to_release = Mock(return_value='release-test') storage = Mock() storage.get = Mock(return_value=_as_future(None)) storage.offer = Mock(return_value=_as_future(None)) storage.release = Mock(return_value=_as_future(None)) @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_storage(storage) .set_key_extractor(key_extractor) .set_eviction_strategy(eviction_strategy) ) @gen.coroutine def sample_method(arg, kwarg=None): return arg, kwarg # when yield sample_method('test', kwarg='args') yield _ensure_background_tasks_finished() # then eviction_strategy.next_to_release.assert_called_once_with() storage.release.assert_called_once_with('release-test')
async def test_should_return_current_value_on_second_call_after_update_time_reached_but_not_expiration_time(self): # given value = 0 async def get_value(arg, kwarg=None): return value get_value_cached = memoize( method=get_value, configuration=DefaultInMemoryCacheConfiguration(update_after=timedelta(milliseconds=100), expire_after=timedelta(minutes=5))) # when res1 = await get_value_cached('test', kwarg='args') time.sleep(.200) await _ensure_asyncio_background_tasks_finished() value = 1 await get_value_cached('test', kwarg='args') await _ensure_asyncio_background_tasks_finished() # calling thrice be more confident about behaviour of parallel execution res2 = await self._call_thrice(lambda: get_value_cached('test', kwarg='args')) # then self.assertEqual(0, res1) self.assertEqual([1, 1, 1], res2)
def test_should_call_key_extractor_on_method_used(self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') @memoize(configuration=MutableCacheConfiguration.initialized_with( DefaultInMemoryCacheConfiguration()).set_key_extractor( key_extractor)) @gen.coroutine def sample_method(arg, kwarg=None): return arg, kwarg # when yield sample_method('test', kwarg='args') yield _ensure_background_tasks_finished() # then # ToDo: assert wrapped methods match somehow _assert_called_once_with(self, key_extractor.format_key, ( AnyObject(), ('test', ), { 'kwarg': 'args' }, ), {})
def test_should_return_same_value_on_constant_key_function(self): # given value = 0 key_extractor = Mock() key_extractor.format_key = Mock(return_value='lol') @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_key_extractor(key_extractor) ) @gen.coroutine def get_value(arg, kwarg=None): return value # when res1 = yield get_value('test1', kwarg='args1') time.sleep(.200) yield _ensure_background_tasks_finished() value = 1 res2 = yield get_value('test2', kwarg='args2') # then self.assertEqual(0, res1) self.assertEqual(0, res2)
async def test_should_throw_exception_on_wrapped_method_failure(self): # given async def get_value(arg, kwarg=None): raise ValueError("Get lost") get_value_cached = memoize(method=get_value, configuration=DefaultInMemoryCacheConfiguration()) # when with self.assertRaises(Exception) as context: await get_value_cached('test1', kwarg='args1') # then expected = CachedMethodFailedException('Refresh failed to complete', ValueError('Get lost', )) self.assertEqual(str(expected), str(context.exception)) # ToDo: consider better comparision
async def test_should_throw_exception_on_configuration_not_ready(self): # given @memoize(configuration=MutableCacheConfiguration.initialized_with( DefaultInMemoryCacheConfiguration()).set_configured(False)) async def get_value(arg, kwarg=None): raise ValueError("Get lost") # when with self.assertRaises(Exception) as context: await get_value('test1', kwarg='args1') # then expected = NotConfiguredCacheCalledException() self.assertEqual(str(expected), str(context.exception))
async def test_should_throw_exception_on_refresh_timeout(self): # given async def get_value(arg, kwarg=None): await _ensure_asyncio_background_tasks_finished() time.sleep(.200) await _ensure_asyncio_background_tasks_finished() return 0 get_value_cached = memoize( method=get_value, configuration=DefaultInMemoryCacheConfiguration(method_timeout=timedelta(milliseconds=100))) # when with self.assertRaises(Exception) as context: await get_value_cached('test1', kwarg='args1') # then expected = CachedMethodFailedException('Refresh timed out') self.assertEqual(str(expected), str(context.exception)) # ToDo: consider better comparision
async def test_should_return_different_values_on_different_kwargs_with_default_key(self): # given value = 0 async def get_value(arg, kwarg=None): return value get_value_cached = memoize(method=get_value, configuration=DefaultInMemoryCacheConfiguration()) # when res1 = await get_value_cached('test', kwarg='args1') time.sleep(.200) await _ensure_asyncio_background_tasks_finished() value = 1 res2 = await get_value_cached('test', kwarg='args2') # then self.assertEqual(0, res1) self.assertEqual(1, res2)
async def test_should_return_cached_value_on_expiration_time_not_reached( self): # given value = 0 @memoize(configuration=DefaultInMemoryCacheConfiguration( update_after=timedelta(minutes=1), expire_after=timedelta(minutes=2))) async def get_value(arg, kwarg=None): return value # when res1 = await get_value('test', kwarg='args') time.sleep(.200) await _ensure_asyncio_background_tasks_finished() value = 1 # calling thrice be more confident about behaviour of parallel execution res2 = await self._call_thrice(lambda: get_value('test', kwarg='args')) # then self.assertEqual(0, res1) self.assertEqual([0, 0, 0], res2)
def test_should_throw_exception_on_refresh_timeout(self): # given @memoize( configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_method_timeout(timedelta(milliseconds=100)) ) @gen.coroutine def get_value(arg, kwarg=None): yield _ensure_background_tasks_finished() time.sleep(.200) yield _ensure_background_tasks_finished() return 0 # when with self.assertRaises(Exception) as context: yield get_value('test1', kwarg='args1') # then expected = CachedMethodFailedException('Refresh timed out') self.assertEqual(str(expected), str(context.exception)) # ToDo: consider better comparision
def test_should_pass_extracted_key_to_storage_on_entry_added_to_cache( self): # given key_extractor = Mock() key_extractor.format_key = Mock(return_value='key') storage = Mock() storage.get = Mock(return_value=_as_future(None)) storage.offer = Mock(return_value=_as_future(None)) @memoize(configuration=MutableCacheConfiguration.initialized_with( DefaultInMemoryCacheConfiguration()).set_key_extractor( key_extractor).set_storage(storage)) @gen.coroutine def sample_method(arg, kwarg=None): return arg, kwarg # when yield sample_method('test', kwarg='args') yield _ensure_background_tasks_finished() # then storage.get.assert_called_once_with('key') _assert_called_once_with(self, storage.offer, ('key', AnyObject()), {})
async def test_complex_showcase(self): # given UPDATE_MS = 400.0 UPDATE_S = UPDATE_MS / 1000 EXPIRE_MS = 800.0 EXPIRE_S = EXPIRE_MS / 1000 @memoize(configuration=DefaultInMemoryCacheConfiguration( update_after=timedelta(milliseconds=UPDATE_MS), expire_after=timedelta(milliseconds=EXPIRE_MS))) async def get_value_or_throw(arg, kwarg=None): if should_throw: raise ValueError(value) else: return value # when #1: initial call value, should_throw = 'ok #1', False res1 = await get_value_or_throw('test', kwarg='args') # when #2: background refresh - returns stale time.sleep(UPDATE_S) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'ok #2', False res2 = await get_value_or_throw('test', kwarg='args') # when #3: no refresh (cache up-to-date) time.sleep(.10) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'ok #3', False res3 = await get_value_or_throw('test', kwarg='args') # when #4: no refresh (cache up-to-date) but starts throwing time.sleep(.10) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'throws #1', True res4 = await get_value_or_throw('test', kwarg='args') # when #5: background refresh while throwing - returns stale time.sleep(UPDATE_S) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'throws #2', True res5 = await get_value_or_throw('test', kwarg='args') # when #6: stale value from cache as method raised during background refresh at #5 time.sleep(.10) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'throws #3', True res6 = await get_value_or_throw('test', kwarg='args') # when #7: stale expired - cache throws synchronously time.sleep(EXPIRE_S) await _ensure_asyncio_background_tasks_finished() value, should_throw = 'throws #4', True with self.assertRaises(Exception) as context: await get_value_or_throw('test', kwarg='args') # then self.assertEqual('ok #1', res1) self.assertEqual('ok #1', res2) # previous value - refresh in background self.assertEqual('ok #2', res3) # value from cache - still relevant self.assertEqual('ok #2', res4) # value from cache - still relevant self.assertEqual('ok #2', res5) # stale from cache - refresh in background self.assertEqual( 'ok #2', res6) # stale from cache - should be updated but method throws expected = CachedMethodFailedException('Refresh failed to complete', ValueError('throws #4', )) self.assertEqual(str(expected), str( context.exception)) # ToDo: consider better comparision
from datetime import timedelta from memoize.configuration import MutableCacheConfiguration, DefaultInMemoryCacheConfiguration from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder from memoize.eviction import LeastRecentlyUpdatedEvictionStrategy from memoize.key import EncodedMethodNameAndArgsKeyExtractor from memoize.storage import LocalInMemoryCacheStorage from memoize.wrapper import memoize @memoize(configuration=MutableCacheConfiguration .initialized_with(DefaultInMemoryCacheConfiguration()) .set_method_timeout(value=timedelta(minutes=2)) .set_entry_builder(ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta(minutes=2), expire_after=timedelta(minutes=5))) .set_eviction_strategy(LeastRecentlyUpdatedEvictionStrategy(capacity=2048)) .set_key_extractor(EncodedMethodNameAndArgsKeyExtractor(skip_first_arg_as_self=False)) .set_storage(LocalInMemoryCacheStorage()) ) async def cached(): return 'dummy'
from memoize.configuration import DefaultInMemoryCacheConfiguration from memoize.wrapper import memoize @memoize(configuration=DefaultInMemoryCacheConfiguration()) async def cached(): return 'dummy'
from memoize.entrybuilder import ProvidedLifeSpanCacheEntryBuilder from memoize.wrapper import memoize # scenario configuration concurrent_requests = 5 request_batches_execution_count = 50 cached_value_ttl_millis = 200 delay_between_request_batches_millis = 70 # results/statistics unique_calls_under_memoize = 0 unique_calls_under_different_cache = 0 @memoize(configuration=MutableCacheConfiguration.initialized_with( DefaultInMemoryCacheConfiguration()).set_entry_builder( ProvidedLifeSpanCacheEntryBuilder(update_after=timedelta( milliseconds=cached_value_ttl_millis)))) async def cached_with_memoize(): global unique_calls_under_memoize unique_calls_under_memoize += 1 await asyncio.sleep(0.01) return unique_calls_under_memoize @cached(ttl=cached_value_ttl_millis / 1000, cache=SimpleMemoryCache) async def cached_with_different_cache(): global unique_calls_under_different_cache unique_calls_under_different_cache += 1 await asyncio.sleep(0.01) return unique_calls_under_different_cache
def memoize(method: Optional[Callable] = None, configuration: CacheConfiguration = None): """Wraps function with memoization. If entry reaches time it should be updated, refresh is performed in background, but current entry is still valid and may be returned. Once expiration time is reached, refresh is blocking and current entry is considered invalid. Note: If wrapped method times out after `method_timeout` (see configuration) the cache will not be populated and a failure occurs. Note: If wrapped method throws an exception the cache will not be populated and failure occurs. Note: Failures are indicated by designated exceptions (not original ones). To force refreshing immediately upon call to a cached method, set 'force_refresh_memoized' keyword flag, so the method will block until it's cache is refreshed. Warning: Leaving default configuration is a bad idea as it may not fit your data (may cause OOMs or cache for an inappropriate time). :param function method: function to be decorated :param CacheConfiguration configuration: cache configuration; default: DefaultInMemoryCacheConfiguration :raises: CachedMethodFailedException upon call: if cached method timed-out or thrown an exception :raises: NotConfiguredCacheCalledException upon call: if provided configuration is not ready """ if method is None: if configuration is None: configuration = DefaultInMemoryCacheConfiguration() return functools.partial( memoize, configuration=configuration, ) logger = logging.getLogger('{}@{}'.format(memoize.__name__, method.__name__)) logger.debug('wrapping %s with memoization - configuration: %s', method.__name__, configuration) update_statuses = UpdateStatuses() async def try_release(key: CacheKey, configuration_snapshot: CacheConfiguration) -> bool: if update_statuses.is_being_updated(key): return False try: await configuration_snapshot.storage().release(key) configuration_snapshot.eviction_strategy().mark_released(key) logger.debug('Released cache key %s', key) return True except Exception as e: logger.error('Failed to release cache key %s', key, e) return False async def refresh(actual_entry: Optional[CacheEntry], key: CacheKey, value_future_provider: Callable[[], asyncio.Future], configuration_snapshot: CacheConfiguration): if actual_entry is None and update_statuses.is_being_updated(key): logger.debug( 'As entry expired, waiting for results of concurrent refresh %s', key) entry = await update_statuses.await_updated(key) if entry is None: raise CachedMethodFailedException( 'Concurrent refresh failed to complete') return entry elif actual_entry is not None and update_statuses.is_being_updated( key): logger.debug( 'As update point reached but concurrent update already in progress, ' 'relying on concurrent refresh to finish %s', key) return actual_entry elif not update_statuses.is_being_updated(key): update_statuses.mark_being_updated(key) try: value_future = value_future_provider() value = await value_future offered_entry = configuration_snapshot.entry_builder().build( key, value) await configuration_snapshot.storage().offer( key, offered_entry) update_statuses.mark_updated(key, offered_entry) logger.debug('Successfully refreshed cache for key %s', key) eviction_strategy = configuration_snapshot.eviction_strategy() eviction_strategy.mark_written(key, offered_entry) to_release = eviction_strategy.next_to_release() if to_release is not None: _call_soon(try_release, to_release, configuration_snapshot) return offered_entry except _timeout_error_type() as e: logger.debug('Timeout for %s: %s', key, e) update_statuses.mark_update_aborted(key) raise CachedMethodFailedException('Refresh timed out') except Exception as e: logger.debug('Error while refreshing cache for %s: %s', key, e) update_statuses.mark_update_aborted(key) raise CachedMethodFailedException('Refresh failed to complete', e) @functools.wraps(method) async def wrapper(*args, **kwargs): if not configuration.configured(): raise NotConfiguredCacheCalledException() configuration_snapshot = MutableCacheConfiguration.initialized_with( configuration) force_refresh = kwargs.pop('force_refresh_memoized', False) key = configuration_snapshot.key_extractor().format_key( method, args, kwargs) current_entry = await configuration_snapshot.storage().get( key) # type: Optional[CacheEntry] if current_entry is not None: configuration_snapshot.eviction_strategy().mark_read(key) now = datetime.datetime.utcnow() def value_future_provider(): return _apply_timeout(configuration_snapshot.method_timeout(), method(*args, **kwargs)) if current_entry is None: logger.debug('Creating (blocking) entry for key %s', key) result = await refresh(current_entry, key, value_future_provider, configuration_snapshot) elif force_refresh: logger.debug('Forced entry update (blocking) for key %s', key) result = await refresh(current_entry, key, value_future_provider, configuration_snapshot) elif current_entry.expires_after <= now: logger.debug( 'Entry expiration reached - entry update (blocking) for key %s', key) result = await refresh(current_entry, key, value_future_provider, configuration_snapshot) elif current_entry.update_after <= now: logger.debug( 'Entry update point expired - entry update (async - current entry returned) for key %s', key) _call_soon(refresh, current_entry, key, value_future_provider, configuration_snapshot) result = current_entry else: result = current_entry return result.value return wrapper
from memoize.configuration import DefaultInMemoryCacheConfiguration from memoize.wrapper import memoize # scenario configuration concurrent_requests = 5 request_batches_execution_count = 50 cached_value_ttl_ms = 200 delay_between_request_batches_ms = 70 # results/statistics unique_calls_under_memoize = 0 unique_calls_under_different_cache = 0 @memoize(configuration=DefaultInMemoryCacheConfiguration( update_after=timedelta(milliseconds=cached_value_ttl_ms))) async def cached_with_memoize(): global unique_calls_under_memoize unique_calls_under_memoize += 1 await asyncio.sleep(0.01) return unique_calls_under_memoize @cached(ttl=cached_value_ttl_ms / 1000, cache=SimpleMemoryCache) async def cached_with_different_cache(): global unique_calls_under_different_cache unique_calls_under_different_cache += 1 await asyncio.sleep(0.01) return unique_calls_under_different_cache
from datetime import timedelta from memoize.configuration import DefaultInMemoryCacheConfiguration from memoize.wrapper import memoize @memoize(configuration=DefaultInMemoryCacheConfiguration( capacity=4096, method_timeout=timedelta(minutes=2), update_after=timedelta(minutes=10), expire_after=timedelta(minutes=30))) async def cached(): return 'dummy'
nom=nom, prenom="lala", ) def get_tokens(self, code: str, redirect_uri: str) -> Tokens: return Tokens(access_token="dummy_access_token", refresh_token="dummy_refresh_token") # Ademe # ------ from api.utils.endpoints import * @memoize(configuration=DefaultInMemoryCacheConfiguration( update_after=timedelta(milliseconds=3000))) async def get_authorization_header() -> dict: """Retrieve a service token to call ADEME users API""" token_parameters = { "client_id": AUTH_CLIENT_ID, "client_secret": AUTH_SECRET, "grant_type": "client_credentials", } token_response = requests.post(token_endpoint, data=token_parameters) if not token_response.ok: raise HTTPException( status_code=503, detail=f"{token_response.status_code} {token_endpoint}") token_json = token_response.json()