From 3c72e9878de3d8190e3f3a75f9cc0fa72e767c6e Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 14 Dec 2017 13:36:51 -0500 Subject: [PATCH] Add the concept of a data model cache, for caching of Namedtuple objects from the data model Will be used to cache blobs, thus removing the need to hit the database in most blob requests --- data/cache/__init__.py | 48 +++++++++++++++++++++++++++++++++++ data/cache/cache_key.py | 8 ++++++ data/cache/test/test_cache.py | 16 ++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 data/cache/__init__.py create mode 100644 data/cache/cache_key.py create mode 100644 data/cache/test/test_cache.py diff --git a/data/cache/__init__.py b/data/cache/__init__.py new file mode 100644 index 000000000..01c572f38 --- /dev/null +++ b/data/cache/__init__.py @@ -0,0 +1,48 @@ +from datetime import datetime + +from abc import ABCMeta, abstractmethod +from six import add_metaclass + +from util.expiresdict import ExpiresDict +from util.timedeltastring import convert_to_timedelta + +def is_not_none(value): + return value is not None + + +@add_metaclass(ABCMeta) +class DataModelCache(object): + """ Defines an interface for cache storing and returning tuple data model objects. """ + + @abstractmethod + def retrieve(self, cache_key, loader, should_cache=is_not_none): + """ Checks the cache for the specified cache key and returns the value found (if any). If none + found, the loader is called to get a result and populate the cache. + """ + pass + + +class NoopDataModelCache(DataModelCache): + """ Implementation of the data model cache which does nothing. """ + + def retrieve(self, cache_key, loader, should_cache=is_not_none): + return loader() + + +class InMemoryDataModelCache(DataModelCache): + """ Implementation of the data model cache backed by an in-memory dictionary. """ + def __init__(self): + self.cache = ExpiresDict(rebuilder=lambda: {}) + + def retrieve(self, cache_key, loader, should_cache=is_not_none): + not_found = [None] + result = self.cache.get(cache_key.key, default_value=not_found) + if result != not_found: + return result + + result = loader() + if should_cache(result): + expires = convert_to_timedelta(cache_key.expiration) + datetime.now() + self.cache.set(cache_key.key, result, expires=expires) + + return result diff --git a/data/cache/cache_key.py b/data/cache/cache_key.py new file mode 100644 index 000000000..b0d4d7011 --- /dev/null +++ b/data/cache/cache_key.py @@ -0,0 +1,8 @@ +from collections import namedtuple + +class CacheKey(namedtuple('CacheKey', ['key', 'expiration'])): + """ Defines a key into the data model cache. """ + pass + +def for_repository_blob(namespace_name, repo_name, digest): + return CacheKey('repository_blob:%s:%s:%s' % (namespace_name, repo_name, digest), '60s') diff --git a/data/cache/test/test_cache.py b/data/cache/test/test_cache.py new file mode 100644 index 000000000..d9e3708d0 --- /dev/null +++ b/data/cache/test/test_cache.py @@ -0,0 +1,16 @@ +import pytest + +from data.cache import InMemoryDataModelCache, NoopDataModelCache +from data.cache.cache_key import CacheKey + +@pytest.mark.parametrize('cache_type', [ + (NoopDataModelCache), + (InMemoryDataModelCache), +]) +def test_caching(cache_type): + key = CacheKey('foo', '60m') + cache = cache_type() + + # Perform two retrievals, and make sure both return. + assert cache.retrieve(key, lambda: 1234) == 1234 + assert cache.retrieve(key, lambda: 1234) == 1234