Skip to content

Commit

Permalink
Fix #157: Add basic TLRU implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Oct 3, 2021
1 parent a05b686 commit 8c39172
Show file tree
Hide file tree
Showing 2 changed files with 450 additions and 32 deletions.
223 changes: 191 additions & 32 deletions src/cachetools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"LRUCache",
"MRUCache",
"RRCache",
"TLRUCache",
"TTLCache",
"cached",
"cachedmethod",
Expand Down Expand Up @@ -37,6 +38,53 @@ def pop(self, _):
return 1


class _Link:

__slots__ = ("key", "expire", "next", "prev")

def __init__(self, key=None, expire=None):
self.key = key
self.expire = expire

def __reduce__(self):
return _Link, (self.key, self.expire)

def unlink(self):
next = self.next
prev = self.prev
prev.next = next
next.prev = prev


class _Timer:
def __init__(self, timer):
self.__timer = timer
self.__nesting = 0

def __call__(self):
if self.__nesting == 0:
return self.__timer()
else:
return self.__time

def __enter__(self):
if self.__nesting == 0:
self.__time = time = self.__timer()
else:
time = self.__time
self.__nesting += 1
return time

def __exit__(self, *exc):
self.__nesting -= 1

def __reduce__(self):
return _Timer, (self.__timer,)

def __getattr__(self, name):
return getattr(self.__timer, name)


class Cache(collections.abc.MutableMapping):
"""Mutable mapping to serve as a simple cache or cache base class."""

Expand Down Expand Up @@ -294,51 +342,162 @@ def popitem(self):
return (key, self.pop(key))


class _Timer:
def __init__(self, timer):
self.__timer = timer
self.__nesting = 0
class TLRUCache(Cache):
"""LRU Cache implementation with per-item time-to-use (TTU) value."""

def __call__(self):
if self.__nesting == 0:
return self.__timer()
def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
Cache.__init__(self, maxsize, getsizeof)
self.__root = root = _Link()
root.prev = root.next = root
self.__links = collections.OrderedDict()
self.__timer = _Timer(timer)
self.__ttu = ttu

def __contains__(self, key):
try:
link = self.__links[key] # no reordering
except KeyError:
return False
else:
return self.__time
return not (link.expire < self.__timer())

def __enter__(self):
if self.__nesting == 0:
self.__time = time = self.__timer()
def __getitem__(self, key, cache_getitem=Cache.__getitem__):
try:
link = self.__getlink(key)
except KeyError:
expired = False
else:
time = self.__time
self.__nesting += 1
return time
expired = link.expire < self.__timer()
if expired:
return self.__missing__(key)
else:
return cache_getitem(self, key)

def __exit__(self, *exc):
self.__nesting -= 1
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
with self.__timer as time:
self.expire(time)
cache_setitem(self, key, value)
try:
link = self.__getlink(key)
except KeyError:
self.__links[key] = link = _Link(key)
else:
link.unlink()
link.expire = time + self.__ttu(value)
# TODO: insert in sorted order or change data structure
link.next = root = self.__root
link.prev = prev = root.prev
prev.next = root.prev = link

def __reduce__(self):
return _Timer, (self.__timer,)
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
cache_delitem(self, key)
link = self.__links.pop(key)
link.unlink()
if link.expire < self.__timer():
raise KeyError(key)

def __getattr__(self, name):
return getattr(self.__timer, name)
def __iter__(self):
root = self.__root
curr = root.next
while curr is not root:
# "freeze" time for iterator access
with self.__timer as time:
if not (curr.expire < time):
yield curr.key
curr = curr.next

def __len__(self):
root = self.__root
curr = root.next
time = self.__timer()
count = len(self.__links)
# TODO: prevent iterating over all elements
while curr is not root:
if curr.expire < time:
count -= 1
curr = curr.next
return count

class _Link:
def __setstate__(self, state):
self.__dict__.update(state)
root = self.__root
root.prev = root.next = root
for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
link.next = root
link.prev = prev = root.prev
prev.next = root.prev = link
self.expire(self.__timer())

__slots__ = ("key", "expire", "next", "prev")
def __repr__(self, cache_repr=Cache.__repr__):
with self.__timer as time:
self.expire(time)
return cache_repr(self)

def __init__(self, key=None, expire=None):
self.key = key
self.expire = expire
@property
def currsize(self):
with self.__timer as time:
self.expire(time)
return super().currsize

def __reduce__(self):
return _Link, (self.key, self.expire)
@property
def timer(self):
"""The timer function used by the cache."""
return self.__timer

def unlink(self):
next = self.next
prev = self.prev
prev.next = next
next.prev = prev
def expire(self, time=None):
"""Remove expired items from the cache."""
if time is None:
time = self.__timer()
root = self.__root
curr = root.next
links = self.__links
cache_delitem = Cache.__delitem__
# TODO: prevent iterating over all elements
while curr is not root:
if curr.expire < time:
cache_delitem(self, curr.key)
del links[curr.key]
next = curr.next
curr.unlink()
curr = next
else:
curr = curr.next

def clear(self):
with self.__timer as time:
self.expire(time)
Cache.clear(self)

def get(self, *args, **kwargs):
with self.__timer:
return Cache.get(self, *args, **kwargs)

def pop(self, *args, **kwargs):
with self.__timer:
return Cache.pop(self, *args, **kwargs)

def setdefault(self, *args, **kwargs):
with self.__timer:
return Cache.setdefault(self, *args, **kwargs)

def popitem(self):
"""Remove and return the `(key, value)` pair least recently used that
has not already expired.
"""
with self.__timer as time:
self.expire(time)
try:
key = next(iter(self.__links))
except StopIteration:
raise KeyError("%s is empty" % self.__class__.__name__) from None
else:
return (key, self.pop(key))

def __getlink(self, key):
value = self.__links[key]
self.__links.move_to_end(key)
return value


class TTLCache(Cache):
Expand Down
Loading

0 comments on commit 8c39172

Please sign in to comment.