Skip to content

Commit

Permalink
Add nested timer
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Dec 10, 2014
1 parent d7cb994 commit 9d947a1
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 68 deletions.
173 changes: 109 additions & 64 deletions cachetools/ttl.py
Expand Up @@ -2,6 +2,7 @@
from .decorators import cachedfunc
from .lock import RLock

import functools
import time


Expand All @@ -28,6 +29,32 @@ def unlink(self):
lru_next.lru_prev = lru_prev


class NestedTimer(object):

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

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

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

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

def __getattr__(self, name):
# FIXME: for unittests timer.tick()
return getattr(self.__timer, name)


class TTLCache(Cache):
"""LRU Cache implementation with per-item time-to-live (TTL) value.
Expand All @@ -52,10 +79,10 @@ def __init__(self, maxsize, ttl, timer=time.time, missing=None,
self.getsizeof = getsizeof
else:
Cache.__init__(self, maxsize, missing)
self.__timer = NestedTimer(timer)
self.__root = root = Link()
root.ttl_prev = root.ttl_next = root
root.lru_prev = root.lru_next = root
self.__timer = timer
self.__ttl = ttl

def __repr__(self, cache_getitem=Cache.__getitem__):
Expand All @@ -70,81 +97,85 @@ def __repr__(self, cache_getitem=Cache.__getitem__):
def __getitem__(self, key,
cache_getitem=Cache.__getitem__,
cache_missing=Cache.__missing__):
link = cache_getitem(self, key)
if link.expire < self.__timer():
return cache_missing(self, key).value
next = link.lru_next
prev = link.lru_prev
prev.lru_next = next
next.lru_prev = prev
link.lru_next = root = self.__root
link.lru_prev = tail = root.lru_prev
tail.lru_next = root.lru_prev = link
return link.value
with self.__timer as time:
link = cache_getitem(self, key)
if link.expire < time:
return cache_missing(self, key).value
next = link.lru_next
prev = link.lru_prev
prev.lru_next = next
next.lru_prev = prev
link.lru_next = root = self.__root
link.lru_prev = tail = root.lru_prev
tail.lru_next = root.lru_prev = link
return link.value

def __setitem__(self, key, value,
cache_contains=Cache.__contains__,
cache_getitem=Cache.__getitem__,
cache_setitem=Cache.__setitem__):
time = self.__timer()
self.expire(time)
if cache_contains(self, key):
oldlink = cache_getitem(self, key)
else:
oldlink = None
link = Link()
link.key = key
link.value = value
link.expire = time + self.__ttl
link.size = self.getsizeof(value)
cache_setitem(self, key, link)
if oldlink:
oldlink.unlink()
link.ttl_next = root = self.__root
link.ttl_prev = tail = root.ttl_prev
tail.ttl_next = root.ttl_prev = link
link.lru_next = root
link.lru_prev = tail = root.lru_prev
tail.lru_next = root.lru_prev = link
with self.__timer as time:
self.expire(time)
if cache_contains(self, key):
oldlink = cache_getitem(self, key)
else:
oldlink = None
link = Link()
link.key = key
link.value = value
link.expire = time + self.__ttl
link.size = self.getsizeof(value)
cache_setitem(self, key, link)
if oldlink:
oldlink.unlink()
link.ttl_next = root = self.__root
link.ttl_prev = tail = root.ttl_prev
tail.ttl_next = root.ttl_prev = link
link.lru_next = root
link.lru_prev = tail = root.lru_prev
tail.lru_next = root.lru_prev = link

def __delitem__(self, key,
cache_contains=Cache.__contains__,
cache_getitem=Cache.__getitem__,
cache_delitem=Cache.__delitem__):
if not cache_contains(self, key):
raise KeyError(key)
link = cache_getitem(self, key)
cache_delitem(self, key)
link.unlink()
self.expire()
with self.__timer as time:
self.expire(time)
if not cache_contains(self, key):
raise KeyError(key)
link = cache_getitem(self, key)
cache_delitem(self, key)
link.unlink()

def __contains__(self, key,
cache_contains=Cache.__contains__,
cache_getitem=Cache.__getitem__):
if not cache_contains(self, key):
return False
elif cache_getitem(self, key).expire < self.__timer():
return False
else:
return True
with self.__timer as time:
if not cache_contains(self, key):
return False
elif cache_getitem(self, key).expire < time:
return False
else:
return True

def __iter__(self):
timer = self.__timer
root = self.__root
curr = root.ttl_next
while curr is not root:
if not (curr.expire < timer()):
yield curr.key
with timer as time:
if not (curr.expire < time):
yield curr.key
curr = curr.ttl_next

def __len__(self, cache_len=Cache.__len__):
expired = 0
time = self.__timer()
root = self.__root
head = root.ttl_next
while head is not root and head.expire < time:
expired += 1
head = head.ttl_next
expired = 0
with self.__timer as time:
while head is not root and head.expire < time:
expired += 1
head = head.ttl_next
return cache_len(self) - expired

def expire(self, time=None):
Expand All @@ -167,24 +198,26 @@ def expire(self, time=None):

def popitem(self):
"""Remove and return the `(key, value)` pair least recently used."""
root = self.__root
link = root.lru_next
if link is root:
raise KeyError('cache is empty')
key = link.key
Cache.__delitem__(self, key)
link.unlink()
return (key, link.value)
with self.__timer as time:
self.expire(time)
root = self.__root
link = root.lru_next
if link is root:
raise KeyError('cache is empty')
key = link.key
Cache.__delitem__(self, key)
link.unlink()
return (key, link.value)

@property
def currsize(self):
expired = 0
time = self.__timer()
root = self.__root
head = root.ttl_next
while head is not root and head.expire < time:
expired += head.size
head = head.ttl_next
expired = 0
with self.__timer as time:
while head is not root and head.expire < time:
expired += head.size
head = head.ttl_next
return super(TTLCache, self).currsize - expired

@property
Expand All @@ -197,6 +230,18 @@ def ttl(self):
"""Return the time-to-live of the cache."""
return self.__ttl

# mixin methods

def __nested(method):
def wrapper(self, *args, **kwargs):
with self.__timer:
return method(self, *args, **kwargs)
return functools.update_wrapper(wrapper, method)

get = __nested(Cache.get)
pop = __nested(Cache.pop)
setdefault = __nested(Cache.setdefault)


def ttl_cache(maxsize=128, ttl=600, timer=time.time, typed=False,
getsizeof=None, lock=RLock):
Expand Down
27 changes: 23 additions & 4 deletions tests/test_ttl.py
Expand Up @@ -5,14 +5,17 @@


class Timer:
def __init__(self):
self.__time = 0
def __init__(self, auto=False):
self.auto = auto
self.time = 0

def __call__(self):
return self.__time
if self.auto:
self.time += 1
return self.time

def tick(self):
self.__time += 1
self.time += 1


class TTLCacheTest(unittest.TestCase, CacheTestMixin, DecoratorTestMixin):
Expand Down Expand Up @@ -106,6 +109,11 @@ def test_ttl(self):
self.assertNotIn(2, cache)
self.assertNotIn(3, cache)

with self.assertRaises(KeyError):
del cache[1]
with self.assertRaises(KeyError):
cache.pop(2)

def test_expire(self):
cache = self.cache(maxsize=3, ttl=2)
self.assertEqual(2, cache.ttl)
Expand Down Expand Up @@ -156,6 +164,17 @@ def test_expire(self):
self.assertNotIn(2, cache)
self.assertNotIn(3, cache)

def test_atomic(self):
cache = TTLCache(maxsize=1, ttl=1, timer=Timer(auto=True))
cache[1] = 1
self.assertEqual(1, cache[1])
cache[1] = 1
self.assertEqual(1, cache.get(1))
cache[1] = 1
self.assertEqual(1, cache.pop(1))
cache[1] = 1
self.assertEqual(1, cache.setdefault(1))

def test_tuple_key(self):
cache = self.cache(maxsize=1, ttl=0)
self.assertEqual(0, cache.ttl)
Expand Down

0 comments on commit 9d947a1

Please sign in to comment.