Skip to content

Commit 61ca083

Browse files
Added DecayingLRUCache
New class `DecayingLRUCache`, which provides the capability to test the age of cached elements. If a cached entry fails to pass the test, the entry will be evicted and forcibly reloaded upon access.
1 parent cacc491 commit 61ca083

File tree

1 file changed

+167
-3
lines changed

1 file changed

+167
-3
lines changed

darts/lib/utils/lru.py

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -942,8 +942,172 @@ def load(self, key, default=None):
942942
return default
943943
else:
944944
return value
945+
946+
947+
def identity(value):
948+
return value
949+
950+
def good(value):
951+
return True
952+
953+
954+
class DecayingLRUCache(object):
955+
956+
"""Auto-LRU cache with support for stale entry detection
957+
958+
This class implements a variant of `AutoLRUCache`, which
959+
also supports the detection of entries, that are too old
960+
and need to be reloaded even if they are are present.
961+
962+
The client application needs to provide the following
963+
parameters:
964+
965+
- `loader`
966+
A callable, which is applied to a key value in order
967+
to load the associated object. This must be a function
968+
with the signature `lambda key: ...`
969+
970+
- `tester`
971+
Is applied to a cached element before that is handed
972+
out to the caller; if this function returns false, the
973+
cached element is considered "too old" and dropped from
974+
the cache.
975+
976+
- `key`
977+
A callable, which is applied to a key value in order
978+
to produce a properly hashable value from it. The
979+
default key extraction function is `identity`, i.e.,
980+
we use the supplied key value unchanged.
981+
982+
- `capacity`
983+
Maximum number of elements in kept in the LRU cache. The
984+
cache starts evicting elements, which have not been
985+
recently accessed, if the number of preserved elements
986+
reaches this limit.
987+
988+
Note, that eviction is *not* in any way controlled by
989+
the `tester` function, but by access order only!
990+
"""
991+
992+
__slots__ = (
993+
'__weakref__',
994+
'_DecayingLRUCache__lock',
995+
'_DecayingLRUCache__cache',
996+
'_DecayingLRUCache__loader',
997+
'_DecayingLRUCache__loading',
998+
'_DecayingLRUCache__tester',
999+
'_DecayingLRUCache__key',
1000+
)
1001+
1002+
def __init__(self, loader, tester=good, key=identity, capacity=1024):
1003+
1004+
"""Initialize a new instance
1005+
1006+
"""
1007+
1008+
super(CarefulLRUCache, self).__init__()
1009+
self.__lock = Lock()
1010+
self.__cache = LRUDict(capacity)
1011+
self.__loader = loader
1012+
self.__loading = dict()
1013+
self.__tester = tester
1014+
self.__key = key
1015+
1016+
def clear(self, discard_loads=False):
9451017

1018+
"""Remove all cached values
9461019
947-
# TODO: The above class can greatly be simplified, if Twisted is
948-
# available and used. Provide a separate implementation using Twisted
949-
# and its Deferreds.
1020+
This method removes all values from this cache. If
1021+
`discard_loads`, then the method also forces all currently
1022+
running load operations to fail with a `CacheAbandonedError`.
1023+
Note, that this method has no way of interrupting load
1024+
operations, so all pending operations will have to complete
1025+
before the discard condition can be detected.
1026+
"""
1027+
1028+
with self.__lock:
1029+
self.__cache.clear()
1030+
if discard_loads:
1031+
conditions = list()
1032+
keys = tuple(self.__loading.iterkeys())
1033+
for k in keys:
1034+
placeholder = self.__loading.pop(k)
1035+
if placeholder._state is loading:
1036+
placeholder._state = discarded
1037+
conditions.append(placeholder._condition)
1038+
while conditions:
1039+
conditions.pop().notifyAll()
1040+
1041+
def load(self, key):
1042+
1043+
"""Load a value
1044+
1045+
Returns the value associated with `key`. If no
1046+
matching value is currently present in this cache,
1047+
or if the value present is considered "too old"
1048+
by the `tester` function, then a value is loaded via
1049+
the cache's `loader` function.
1050+
"""
1051+
1052+
loader, tester, keyfn = self.__loader, self.__tester, self.__key
1053+
kref = keyfn(key)
1054+
1055+
with self.__lock:
1056+
item = self.__cache.get(kref, missing)
1057+
if item is not missing:
1058+
if tester(item):
1059+
return item
1060+
else:
1061+
del self.__cache[kref]
1062+
placeholder = self.__loading.get(kref)
1063+
if placeholder is not None:
1064+
while placeholder._state is loading:
1065+
placeholder._condition.wait()
1066+
if placeholder._state is failed:
1067+
raise CacheLoadError(key, placeholder._value)
1068+
else:
1069+
if placeholder._state is available:
1070+
return placeholder._value
1071+
else:
1072+
assert placeholder._state is discarded
1073+
raise CacheAbandonedError(key=key)
1074+
assert False, "this line should never be reached"
1075+
else:
1076+
placeholder = Placeholder(self.__lock)
1077+
self.__loading[kref] = placeholder
1078+
# The previous line was the point of no return.
1079+
# Reaching this point means, that we are the thread
1080+
# which has to do the actual loading. It also means,
1081+
# that we must never leave this method without properly
1082+
# cleaning up behind us.
1083+
try:
1084+
value = loader(key)
1085+
except:
1086+
with self.__lock:
1087+
if placeholder._state is loading:
1088+
# We are still responsible for the placeholder.
1089+
del self.__loading[kref]
1090+
placeholder._value = sys.exc_info()
1091+
placeholder._state = failed
1092+
placeholder._condition.notifyAll()
1093+
raise CacheLoadError(key, placeholder._value)
1094+
else:
1095+
# Do not notify the condition variable, since
1096+
# that should already have been done by whoever
1097+
# changed the placeholder's state
1098+
raise CacheAbandonedError(key=key, exc_info=sys.exc_info())
1099+
else:
1100+
with self.__lock:
1101+
if placeholder._state is loading:
1102+
# We are still responsible for the placeholder.
1103+
del self.__loading[kref]
1104+
placeholder._value = value
1105+
placeholder._state = available
1106+
self.__cache[kref] = value
1107+
placeholder._condition.notifyAll()
1108+
else:
1109+
# Do not notify the condition variable, since
1110+
# that should already have been done by whoever
1111+
# changed the placeholder's state
1112+
raise CacheAbandonedError(key=key, value=value)
1113+
return value

0 commit comments

Comments
 (0)