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)
def test_should_throw_exception_on_wrapped_method_failure(self): # given @memoize() @gen.coroutine def get_value(arg, kwarg=None): raise ValueError("Get lost") # when with self.assertRaises(Exception) as context: yield get_value('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_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_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
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
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