Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(275)

Side by Side Diff: appengine/findit/lib/cache_decorator.py

Issue 2538373003: [Culprit-Finder] Merge lib/ to libs/. (Closed)
Patch Set: . Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
3 # found in the LICENSE file.
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.
8
9 Examples:
10 1. Decorate a function:
11 @cache_decorator.Cached()
12 def Test(a):
13 return a + a
14
15 Test('a')
16 Test('a') # Returns the cached 'aa'.
17
18 2. Decorate a method in a class:
19 class Downloader(object):
20 def __init__(self, url, retries):
21 self.url = url
22 self.retries = retries
23
24 @property
25 def identifier(self):
26 return self.url
27
28 @cache_decorator.Cached():
29 def Download(self, path):
30 return urllib2.urlopen(self.url + '/' + path).read()
31
32 d1 = Downloader('http://url', 4)
33 d1.Download('path')
34
35 d2 = Downloader('http://url', 5)
36 d2.Download('path') # Returned the cached downloaded data.
37 """
38
39 import cStringIO
40 import functools
41 import hashlib
42 import inspect
43 import logging
44 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
138
139 def _DefaultKeyGenerator(func, args, kwargs):
140 """Generates a key from the function and arguments passed to it.
141
142 Args:
143 func (function): An arbitrary function.
144 args (list): Positional arguments passed to ``func``.
145 kwargs (dict): Keyword arguments passed to ``func``.
146
147 Returns:
148 A string to represent a call to the given function with the given arguments.
149 """
150 params = inspect.getcallargs(func, *args, **kwargs)
151 for var_name in params:
152 if not hasattr(params[var_name], 'identifier'):
153 continue
154
155 if callable(params[var_name].identifier):
156 params[var_name] = params[var_name].identifier()
157 else:
158 params[var_name] = params[var_name].identifier
159
160 return hashlib.md5(pickle.dumps(params)).hexdigest()
161
162
163 def Cached(namespace=None,
164 expire_time=0,
165 key_generator=_DefaultKeyGenerator,
166 cacher=PickledMemCacher()):
167 """Returns a decorator to cache the decorated function's results.
168
169 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.
171
172 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
174 are equivalent, if the namespace is the same and the keys generated by the
175 ``key_generator`` are the same.
176
177 The usage of this decorator requires that:
178 - 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
180 property or method which returns pickleable results.
181 - If the default cacher is used, the returned results of the decorated
182 function should be pickleable.
183
184 Args:
185 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.
187 expire_time (int): Expiration time, relative number of seconds from current
188 time (up to 1 month). Defaults to 0 -- never expire.
189 key_generator (function): A function to generate a key to represent a call
190 to the decorated function. Defaults to :func:`_DefaultKeyGenerator`.
191 cacher (Cacher): An instance of an implementation of interface `Cacher`.
192 Defaults to one of `PickledMemCacher` which is based on memcache.
193
194 Returns:
195 The cached results or the results of a new run of the decorated function.
196 """
197 def GetPrefix(func, namespace):
198 return namespace or '%s.%s' % (func.__module__, func.__name__)
199
200 def Decorator(func):
201 """Decorator to cache a function's results."""
202 @functools.wraps(func)
203 def Wrapped(*args, **kwargs):
204 prefix = GetPrefix(func, namespace)
205 key = '%s-%s' % (prefix, key_generator(func, args, kwargs))
206
207 try:
208 result = cacher.Get(key)
209 except Exception: # pragma: no cover.
210 result = None
211 logging.exception(
212 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s',
213 func.__module__, func.__name__, repr(args), repr(kwargs))
214
215 if result is not None:
216 return result
217
218 result = func(*args, **kwargs)
219 if result:
220 try:
221 cacher.Set(key, result, expire_time=expire_time)
222 except Exception: # pragma: no cover.
223 logging.exception(
224 'Failed to cache data for function %s.%s, args=%s, kwargs=%s',
225 func.__module__, func.__name__, repr(args), repr(kwargs))
226
227 return result
228
229 return Wrapped
230
231 return Decorator
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698