Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 # TODO(http://crbug.com/660466): We should try to break dependencies. | |
| 6 | |
| 7 """This module provides a decorator to cache the results of a function. | 5 """This module provides a decorator to cache the results of a function. |
| 8 | 6 |
| 9 Examples: | 7 Examples: |
| 10 1. Decorate a function: | 8 1. Decorate a function: |
| 11 @cache_decorator.Cached() | 9 @cache_decorator.Cached() |
| 12 def Test(a): | 10 def Test(a): |
| 13 return a + a | 11 return a + a |
| 14 | 12 |
| 15 Test('a') | 13 Test('a') |
| 16 Test('a') # Returns the cached 'aa'. | 14 Test('a') # Returns the cached 'aa'. |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 29 def Download(self, path): | 27 def Download(self, path): |
| 30 return urllib2.urlopen(self.url + '/' + path).read() | 28 return urllib2.urlopen(self.url + '/' + path).read() |
| 31 | 29 |
| 32 d1 = Downloader('http://url', 4) | 30 d1 = Downloader('http://url', 4) |
| 33 d1.Download('path') | 31 d1.Download('path') |
| 34 | 32 |
| 35 d2 = Downloader('http://url', 5) | 33 d2 = Downloader('http://url', 5) |
| 36 d2.Download('path') # Returned the cached downloaded data. | 34 d2.Download('path') # Returned the cached downloaded data. |
| 37 """ | 35 """ |
| 38 | 36 |
| 39 import cStringIO | |
| 40 import functools | 37 import functools |
| 41 import hashlib | 38 import hashlib |
| 39 import logging | |
| 42 import inspect | 40 import inspect |
| 43 import logging | |
| 44 import pickle | 41 import pickle |
| 45 import zlib | |
| 46 | |
| 47 from google.appengine.api import memcache | |
| 48 | |
| 49 | |
| 50 # TODO(katesonia): Change this to a better name, e.g. Cache. | |
| 51 class Cacher(object): | |
| 52 """An interface to cache and retrieve data. | |
| 53 | |
| 54 Subclasses should implement the Get/Set functions. | |
| 55 TODO: Add a Delete function (default to no-op) if needed later. | |
| 56 """ | |
| 57 def Get(self, key): | |
| 58 """Returns the cached data for the given key if available. | |
| 59 | |
| 60 Args: | |
| 61 key (str): The key to identify the cached data. | |
| 62 """ | |
| 63 raise NotImplementedError() | |
| 64 | |
| 65 def Set(self, key, data, expire_time=0): | |
| 66 """Cache the given data which is identified by the given key. | |
| 67 | |
| 68 Args: | |
| 69 key (str): The key to identify the cached data. | |
| 70 data (object): The python object to be cached. | |
| 71 expire_time (int): Number of seconds from current time (up to 1 month). | |
| 72 """ | |
| 73 raise NotImplementedError() | |
| 74 | |
| 75 | |
| 76 class PickledMemCacher(Cacher): | |
| 77 """A memcache-backed implementation of the interface Cacher. | |
| 78 | |
| 79 The data to be cached should be pickleable. | |
| 80 Limitation: size of the pickled data and key should be <= 1MB. | |
| 81 """ | |
| 82 def Get(self, key): | |
| 83 return memcache.get(key) | |
| 84 | |
| 85 def Set(self, key, data, expire_time=0): | |
| 86 return memcache.set(key, data, time=expire_time) | |
| 87 | |
| 88 | |
| 89 class _CachedItemMetaData(object): | |
| 90 def __init__(self, number): | |
| 91 self.number = number | |
| 92 | |
| 93 | |
| 94 class CompressedMemCacher(Cacher): | |
| 95 """A memcache-backed implementation of the interface Cacher with compression. | |
| 96 | |
| 97 The data to be cached would be pickled and then compressed. | |
| 98 Data still > 1MB will be split into sub-piece and stored separately. | |
| 99 During retrieval, if any sub-piece is missing, None is returned. | |
| 100 """ | |
| 101 CHUNK_SIZE = 990000 | |
| 102 | |
| 103 def Get(self, key): | |
| 104 data = memcache.get(key) | |
| 105 if isinstance(data, _CachedItemMetaData): | |
| 106 num = data.number | |
| 107 sub_keys = ['%s-%s' % (key, i) for i in range(num)] | |
| 108 all_data = memcache.get_multi(sub_keys) | |
| 109 if len(all_data) != num: # Some data is missing. | |
| 110 return None | |
| 111 | |
| 112 data_output = cStringIO.StringIO() | |
| 113 for sub_key in sub_keys: | |
| 114 data_output.write(all_data[sub_key]) | |
| 115 data = data_output.getvalue() | |
| 116 | |
| 117 return None if data is None else pickle.loads(zlib.decompress(data)) | |
| 118 | |
| 119 def Set(self, key, data, expire_time=0): | |
| 120 pickled_data = pickle.dumps(data) | |
| 121 compressed_data = zlib.compress(pickled_data) | |
| 122 | |
| 123 all_data = {} | |
| 124 if len(compressed_data) > self.CHUNK_SIZE: | |
| 125 num = 0 | |
| 126 for index in range(0, len(compressed_data), self.CHUNK_SIZE): | |
| 127 sub_key = '%s-%s' % (key, num) | |
| 128 all_data[sub_key] = compressed_data[index : index + self.CHUNK_SIZE] | |
| 129 num += 1 | |
| 130 | |
| 131 all_data[key] = _CachedItemMetaData(num) | |
| 132 else: | |
| 133 all_data[key] = compressed_data | |
| 134 | |
| 135 keys_not_set = memcache.set_multi(all_data, time=expire_time) | |
| 136 return len(keys_not_set) == 0 | |
| 137 | 42 |
| 138 | 43 |
| 139 def _DefaultKeyGenerator(func, args, kwargs): | 44 def _DefaultKeyGenerator(func, args, kwargs): |
| 140 """Generates a key from the function and arguments passed to it. | 45 """Generates a key from the function and arguments passed to it. |
| 141 | 46 |
| 142 Args: | 47 Args: |
| 143 func (function): An arbitrary function. | 48 func (function): An arbitrary function. |
| 144 args (list): Positional arguments passed to ``func``. | 49 args (list): Positional arguments passed to ``func``. |
| 145 kwargs (dict): Keyword arguments passed to ``func``. | 50 kwargs (dict): Keyword arguments passed to ``func``. |
| 146 | 51 |
| 147 Returns: | 52 Returns: |
| 148 A string to represent a call to the given function with the given arguments. | 53 A string to represent a call to the given function with the given arguments. |
| 149 """ | 54 """ |
| 150 params = inspect.getcallargs(func, *args, **kwargs) | 55 params = inspect.getcallargs(func, *args, **kwargs) |
| 151 for var_name in params: | 56 for var_name in params: |
| 152 if not hasattr(params[var_name], 'identifier'): | 57 if not hasattr(params[var_name], 'identifier'): |
| 153 continue | 58 continue |
| 154 | 59 |
| 155 if callable(params[var_name].identifier): | 60 if callable(params[var_name].identifier): |
| 156 params[var_name] = params[var_name].identifier() | 61 params[var_name] = params[var_name].identifier() |
| 157 else: | 62 else: |
| 158 params[var_name] = params[var_name].identifier | 63 params[var_name] = params[var_name].identifier |
| 159 | 64 |
| 160 return hashlib.md5(pickle.dumps(params)).hexdigest() | 65 return hashlib.md5(pickle.dumps(params)).hexdigest() |
| 161 | 66 |
| 162 | 67 |
| 163 def Cached(namespace=None, | 68 def Cached(namespace=None, |
| 164 expire_time=0, | 69 expire_time=-1, |
| 165 key_generator=_DefaultKeyGenerator, | 70 key_generator=_DefaultKeyGenerator, |
| 166 cacher=PickledMemCacher()): | 71 cache=None): |
| 167 """Returns a decorator to cache the decorated function's results. | 72 """Returns a decorator to cache the decorated function's results. |
| 168 | 73 |
| 169 However, if the function returns None, empty list/dict, empty string, or other | 74 However, if the function returns None, empty list/dict, empty string, or other |
| 170 value that is evaluated as False, the results won't be cached. | 75 value that is evaluated as False, the results won't be cached. |
| 171 | 76 |
| 172 This decorator is to cache results of different calls to the decorated | 77 This decorator is to cache results of different calls to the decorated |
| 173 function, and avoid executing it again if the calls are equivalent. Two calls | 78 function, and avoid executing it again if the calls are equivalent. Two calls |
| 174 are equivalent, if the namespace is the same and the keys generated by the | 79 are equivalent, if the namespace is the same and the keys generated by the |
| 175 ``key_generator`` are the same. | 80 ``key_generator`` are the same. |
| 176 | 81 |
| 177 The usage of this decorator requires that: | 82 The usage of this decorator requires that: |
| 178 - If the default key generator is used, parameters passed to the decorated | 83 - If the default key generator is used, parameters passed to the decorated |
| 179 function should be pickleable, or each of the parameter has an identifier | 84 function should be pickleable, or each of the parameter has an identifier |
| 180 property or method which returns pickleable results. | 85 property or method which returns pickleable results. |
| 181 - If the default cacher is used, the returned results of the decorated | 86 - If the default cache is used, the returned results of the decorated |
| 182 function should be pickleable. | 87 function should be pickleable. |
| 183 | 88 |
| 184 Args: | 89 Args: |
| 185 namespace (str): A prefix to the key for the cache. Default to the | 90 namespace (str): A prefix to the key for the cache. Default to the |
| 186 combination of module name and function name of the decorated function. | 91 combination of module name and function name of the decorated function. |
| 187 expire_time (int): Expiration time, relative number of seconds from current | 92 expire_time (int): Expiration time, relative number of seconds from current |
| 188 time (up to 1 month). Defaults to 0 -- never expire. | 93 time (up to 0 month). Defaults to 0 -- never expire. |
|
wrengr
2016/12/06 21:52:57
it now defaults to -1 rather than 0...
Sharu Jiang
2016/12/06 23:58:17
After rebase this should defaults to 0, old versio
| |
| 189 key_generator (function): A function to generate a key to represent a call | 94 key_generator (function): A function to generate a key to represent a call |
| 190 to the decorated function. Defaults to :func:`_DefaultKeyGenerator`. | 95 to the decorated function. Defaults to :func:`_DefaultKeyGenerator`. |
| 191 cacher (Cacher): An instance of an implementation of interface `Cacher`. | 96 cache (Cache): An instance of an implementation of interface `Cache`. |
| 192 Defaults to one of `PickledMemCacher` which is based on memcache. | 97 Defaults to None. |
| 193 | 98 |
| 194 Returns: | 99 Returns: |
| 195 The cached results or the results of a new run of the decorated function. | 100 The cached results or the results of a new run of the decorated function. |
| 196 """ | 101 """ |
| 197 def GetPrefix(func, namespace): | 102 def GetPrefix(func, namespace): |
| 198 return namespace or '%s.%s' % (func.__module__, func.__name__) | 103 return namespace or '%s.%s' % (func.__module__, func.__name__) |
| 199 | 104 |
| 200 def Decorator(func): | 105 def Decorator(func): |
| 201 """Decorator to cache a function's results.""" | 106 """Decorator to cache a function's results.""" |
| 202 @functools.wraps(func) | 107 @functools.wraps(func) |
| 203 def Wrapped(*args, **kwargs): | 108 def Wrapped(*args, **kwargs): |
| 204 prefix = GetPrefix(func, namespace) | 109 prefix = GetPrefix(func, namespace) |
| 205 key = '%s-%s' % (prefix, key_generator(func, args, kwargs)) | 110 key = '%s-%s' % (prefix, key_generator(func, args, kwargs)) |
| 206 | 111 |
| 207 try: | 112 try: |
| 208 result = cacher.Get(key) | 113 result = cache.Get(key) |
| 209 except Exception: # pragma: no cover. | 114 except Exception: # pragma: no cover. |
| 210 result = None | 115 result = None |
| 211 logging.exception( | 116 logging.exception( |
| 212 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s', | 117 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s', |
| 213 func.__module__, func.__name__, repr(args), repr(kwargs)) | 118 func.__module__, func.__name__, repr(args), repr(kwargs)) |
| 214 | 119 |
| 215 if result is not None: | 120 if result is not None: |
| 216 return result | 121 return result |
| 217 | 122 |
| 218 result = func(*args, **kwargs) | 123 result = func(*args, **kwargs) |
| 219 if result: | 124 if result: |
| 220 try: | 125 try: |
| 221 cacher.Set(key, result, expire_time=expire_time) | 126 cache.Set(key, result, expire_time=expire_time) |
| 222 except Exception: # pragma: no cover. | 127 except Exception: # pragma: no cover. |
| 223 logging.exception( | 128 logging.exception( |
| 224 'Failed to cache data for function %s.%s, args=%s, kwargs=%s', | 129 'Failed to cache data for function %s.%s, args=%s, kwargs=%s', |
| 225 func.__module__, func.__name__, repr(args), repr(kwargs)) | 130 func.__module__, func.__name__, repr(args), repr(kwargs)) |
| 226 | 131 |
| 227 return result | 132 return result |
| 228 | 133 |
| 229 return Wrapped | 134 return Wrapped |
| 230 | 135 |
| 231 return Decorator | 136 return Decorator |
| OLD | NEW |