Index: appengine/findit/common/cache_decorator.py |
diff --git a/appengine/findit/common/cache_decorator.py b/appengine/findit/common/cache_decorator.py |
deleted file mode 100644 |
index 6f1bb1efe42dc866aa7841e4df8eb84db698f26f..0000000000000000000000000000000000000000 |
--- a/appengine/findit/common/cache_decorator.py |
+++ /dev/null |
@@ -1,228 +0,0 @@ |
-# Copyright 2015 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""This module provides a decorator to cache the results of a function. |
- |
- Examples: |
- 1. Decorate a function: |
- @cache_decorator.Cached() |
- def Test(a): |
- return a + a |
- |
- Test('a') |
- Test('a') # Returns the cached 'aa'. |
- |
- 2. Decorate a method in a class: |
- class Downloader(object): |
- def __init__(self, url, retries): |
- self.url = url |
- self.retries = retries |
- |
- @property |
- def identifier(self): |
- return self.url |
- |
- @cache_decorator.Cached(): |
- def Download(self, path): |
- return urllib2.urlopen(self.url + '/' + path).read() |
- |
- d1 = Downloader('http://url', 4) |
- d1.Download('path') |
- |
- d2 = Downloader('http://url', 5) |
- d2.Download('path') # Returned the cached downloaded data. |
-""" |
- |
-import cStringIO |
-import functools |
-import hashlib |
-import inspect |
-import logging |
-import pickle |
-import zlib |
- |
-from google.appengine.api import memcache |
- |
- |
-class Cacher(object): |
- """An interface to cache and retrieve data. |
- |
- Subclasses should implement the Get/Set functions. |
- TODO: Add a Delete function (default to no-op) if needed later. |
- """ |
- def Get(self, key): |
- """Returns the cached data for the given key if available. |
- |
- Args: |
- key (str): The key to identify the cached data. |
- """ |
- raise NotImplementedError() |
- |
- def Set(self, key, data, expire_time=0): |
- """Cache the given data which is identified by the given key. |
- |
- Args: |
- key (str): The key to identify the cached data. |
- data (object): The python object to be cached. |
- expire_time (int): Number of seconds from current time (up to 1 month). |
- """ |
- raise NotImplementedError() |
- |
- |
-class PickledMemCacher(Cacher): |
- """A memcache-backed implementation of the interface Cacher. |
- |
- The data to be cached should be pickleable. |
- Limitation: size of the pickled data and key should be <= 1MB. |
- """ |
- def Get(self, key): |
- return memcache.get(key) |
- |
- def Set(self, key, data, expire_time=0): |
- return memcache.set(key, data, time=expire_time) |
- |
- |
-class _CachedItemMetaData(object): |
- def __init__(self, number): |
- self.number = number |
- |
- |
-class CompressedMemCacher(Cacher): |
- """A memcache-backed implementation of the interface Cacher with compression. |
- |
- The data to be cached would be pickled and then compressed. |
- Data still > 1MB will be split into sub-piece and stored separately. |
- During retrieval, if any sub-piece is missing, None is returned. |
- """ |
- CHUNK_SIZE = 990000 |
- |
- def Get(self, key): |
- data = memcache.get(key) |
- if isinstance(data, _CachedItemMetaData): |
- num = data.number |
- sub_keys = ['%s-%s' % (key, i) for i in range(num)] |
- all_data = memcache.get_multi(sub_keys) |
- if len(all_data) != num: # Some data is missing. |
- return None |
- |
- data_output = cStringIO.StringIO() |
- for sub_key in sub_keys: |
- data_output.write(all_data[sub_key]) |
- data = data_output.getvalue() |
- |
- return None if data is None else pickle.loads(zlib.decompress(data)) |
- |
- def Set(self, key, data, expire_time=0): |
- pickled_data = pickle.dumps(data) |
- compressed_data = zlib.compress(pickled_data) |
- |
- all_data = {} |
- if len(compressed_data) > self.CHUNK_SIZE: |
- num = 0 |
- for index in range(0, len(compressed_data), self.CHUNK_SIZE): |
- sub_key = '%s-%s' % (key, num) |
- all_data[sub_key] = compressed_data[index : index + self.CHUNK_SIZE] |
- num += 1 |
- |
- all_data[key] = _CachedItemMetaData(num) |
- else: |
- all_data[key] = compressed_data |
- |
- keys_not_set = memcache.set_multi(all_data, time=expire_time) |
- return len(keys_not_set) == 0 |
- |
- |
-def _DefaultKeyGenerator(func, args, kwargs): |
- """Generates a key from the function and arguments passed to it. |
- |
- Args: |
- func (function): An arbitrary function. |
- args (list): Positional arguments passed to ``func``. |
- kwargs (dict): Keyword arguments passed to ``func``. |
- |
- Returns: |
- A string to represent a call to the given function with the given arguments. |
- """ |
- params = inspect.getcallargs(func, *args, **kwargs) |
- for var_name in params: |
- if not hasattr(params[var_name], 'identifier'): |
- continue |
- |
- if callable(params[var_name].identifier): |
- params[var_name] = params[var_name].identifier() |
- else: |
- params[var_name] = params[var_name].identifier |
- |
- return hashlib.md5(pickle.dumps(params)).hexdigest() |
- |
- |
-def Cached(namespace=None, |
- expire_time=0, |
- key_generator=_DefaultKeyGenerator, |
- cacher=PickledMemCacher()): |
- """Returns a decorator to cache the decorated function's results. |
- |
- However, if the function returns None, empty list/dict, empty string, or other |
- value that is evaluated as False, the results won't be cached. |
- |
- This decorator is to cache results of different calls to the decorated |
- function, and avoid executing it again if the calls are equivalent. Two calls |
- are equivalent, if the namespace is the same and the keys generated by the |
- ``key_generator`` are the same. |
- |
- The usage of this decorator requires that: |
- - If the default key generator is used, parameters passed to the decorated |
- function should be pickleable, or each of the parameter has an identifier |
- property or method which returns pickleable results. |
- - If the default cacher is used, the returned results of the decorated |
- function should be pickleable. |
- |
- Args: |
- namespace (str): A prefix to the key for the cache. Default to the |
- combination of module name and function name of the decorated function. |
- expire_time (int): Expiration time, relative number of seconds from current |
- time (up to 1 month). Defaults to 0 -- never expire. |
- key_generator (function): A function to generate a key to represent a call |
- to the decorated function. Defaults to :func:`_DefaultKeyGenerator`. |
- cacher (Cacher): An instance of an implementation of interface `Cacher`. |
- Defaults to one of `PickledMemCacher` which is based on memcache. |
- |
- Returns: |
- The cached results or the results of a new run of the decorated function. |
- """ |
- def GetPrefix(func, namespace): |
- return namespace or '%s.%s' % (func.__module__, func.__name__) |
- |
- def Decorator(func): |
- """Decorator to cache a function's results.""" |
- @functools.wraps(func) |
- def Wrapped(*args, **kwargs): |
- prefix = GetPrefix(func, namespace) |
- key = '%s-%s' % (prefix, key_generator(func, args, kwargs)) |
- |
- try: |
- result = cacher.Get(key) |
- except Exception: # pragma: no cover. |
- result = None |
- logging.exception( |
- 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s', |
- func.__module__, func.__name__, repr(args), repr(kwargs)) |
- |
- if result is not None: |
- return result |
- |
- result = func(*args, **kwargs) |
- if result: |
- try: |
- cacher.Set(key, result, expire_time=expire_time) |
- except Exception: # pragma: no cover. |
- logging.exception( |
- 'Failed to cache data for function %s.%s, args=%s, kwargs=%s', |
- func.__module__, func.__name__, repr(args), repr(kwargs)) |
- |
- return result |
- |
- return Wrapped |
- |
- return Decorator |