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

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

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

Powered by Google App Engine
This is Rietveld 408576698