##// END OF EJS Templates
caches: don't use lambda for caching query for easier cache debugging.
marcink -
r2671:465c98d6 default
parent child Browse files
Show More
@@ -1,322 +1,325 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 caching_query.py
23 23
24 24 Represent persistence structures which allow the usage of
25 25 Beaker caching with SQLAlchemy.
26 26
27 27 The three new concepts introduced here are:
28 28
29 29 * CachingQuery - a Query subclass that caches and
30 30 retrieves results in/from Beaker.
31 31 * FromCache - a query option that establishes caching
32 32 parameters on a Query
33 33 * RelationshipCache - a variant of FromCache which is specific
34 34 to a query invoked during a lazy load.
35 35 * _params_from_query - extracts value parameters from
36 36 a Query.
37 37
38 38 The rest of what's here are standard SQLAlchemy and
39 39 Beaker constructs.
40 40
41 41 """
42 42 import beaker
43 43 from beaker.exceptions import BeakerException
44 44
45 45 from sqlalchemy.orm.interfaces import MapperOption
46 46 from sqlalchemy.orm.query import Query
47 47 from sqlalchemy.sql import visitors
48 48
49 49 from rhodecode.lib.utils2 import safe_str
50 50
51 51
52 52 class CachingQuery(Query):
53 53 """A Query subclass which optionally loads full results from a Beaker
54 54 cache region.
55 55
56 56 The CachingQuery stores additional state that allows it to consult
57 57 a Beaker cache before accessing the database:
58 58
59 59 * A "region", which is a cache region argument passed to a
60 60 Beaker CacheManager, specifies a particular cache configuration
61 61 (including backend implementation, expiration times, etc.)
62 62 * A "namespace", which is a qualifying name that identifies a
63 63 group of keys within the cache. A query that filters on a name
64 64 might use the name "by_name", a query that filters on a date range
65 65 to a joined table might use the name "related_date_range".
66 66
67 67 When the above state is present, a Beaker cache is retrieved.
68 68
69 69 The "namespace" name is first concatenated with
70 70 a string composed of the individual entities and columns the Query
71 71 requests, i.e. such as ``Query(User.id, User.name)``.
72 72
73 73 The Beaker cache is then loaded from the cache manager based
74 74 on the region and composed namespace. The key within the cache
75 75 itself is then constructed against the bind parameters specified
76 76 by this query, which are usually literals defined in the
77 77 WHERE clause.
78 78
79 79 The FromCache and RelationshipCache mapper options below represent
80 80 the "public" method of configuring this state upon the CachingQuery.
81 81
82 82 """
83 83
84 84 def __init__(self, manager, *args, **kw):
85 85 self.cache_manager = manager
86 86 Query.__init__(self, *args, **kw)
87 87
88 88 def __iter__(self):
89 89 """override __iter__ to pull results from Beaker
90 90 if particular attributes have been configured.
91 91
92 92 Note that this approach does *not* detach the loaded objects from
93 93 the current session. If the cache backend is an in-process cache
94 94 (like "memory") and lives beyond the scope of the current session's
95 95 transaction, those objects may be expired. The method here can be
96 96 modified to first expunge() each loaded item from the current
97 97 session before returning the list of items, so that the items
98 98 in the cache are not the same ones in the current Session.
99 99
100 100 """
101 101 if hasattr(self, '_cache_parameters'):
102 return self.get_value(createfunc=lambda:
103 list(Query.__iter__(self)))
102
103 def caching_query():
104 return list(Query.__iter__(self))
105
106 return self.get_value(createfunc=caching_query)
104 107 else:
105 108 return Query.__iter__(self)
106 109
107 110 def invalidate(self):
108 111 """Invalidate the value represented by this Query."""
109 112
110 113 cache, cache_key = _get_cache_parameters(self)
111 114 cache.remove(cache_key)
112 115
113 116 def get_value(self, merge=True, createfunc=None):
114 117 """Return the value from the cache for this query.
115 118
116 119 Raise KeyError if no value present and no
117 120 createfunc specified.
118 121
119 122 """
120 123 cache, cache_key = _get_cache_parameters(self)
121 124 ret = cache.get_value(cache_key, createfunc=createfunc)
122 125 if merge:
123 126 ret = self.merge_result(ret, load=False)
124 127 return ret
125 128
126 129 def set_value(self, value):
127 130 """Set the value in the cache for this query."""
128 131
129 132 cache, cache_key = _get_cache_parameters(self)
130 133 cache.put(cache_key, value)
131 134
132 135
133 136 def query_callable(manager, query_cls=CachingQuery):
134 137 def query(*arg, **kw):
135 138 return query_cls(manager, *arg, **kw)
136 139 return query
137 140
138 141
139 142 def get_cache_region(name, region):
140 143 if region not in beaker.cache.cache_regions:
141 144 raise BeakerException('Cache region `%s` not configured '
142 145 'Check if proper cache settings are in the .ini files' % region)
143 146 kw = beaker.cache.cache_regions[region]
144 147 return beaker.cache.Cache._get_cache(name, kw)
145 148
146 149
147 150 def _get_cache_parameters(query):
148 151 """For a query with cache_region and cache_namespace configured,
149 152 return the correspoinding Cache instance and cache key, based
150 153 on this query's current criterion and parameter values.
151 154
152 155 """
153 156 if not hasattr(query, '_cache_parameters'):
154 157 raise ValueError("This Query does not have caching "
155 158 "parameters configured.")
156 159
157 160 region, namespace, cache_key = query._cache_parameters
158 161
159 162 namespace = _namespace_from_query(namespace, query)
160 163
161 164 if cache_key is None:
162 165 # cache key - the value arguments from this query's parameters.
163 166 args = [safe_str(x) for x in _params_from_query(query)]
164 167 args.extend(filter(lambda k: k not in ['None', None, u'None'],
165 168 [str(query._limit), str(query._offset)]))
166 169
167 170 cache_key = " ".join(args)
168 171
169 172 if cache_key is None:
170 173 raise Exception('Cache key cannot be None')
171 174
172 175 # get cache
173 176 #cache = query.cache_manager.get_cache_region(namespace, region)
174 177 cache = get_cache_region(namespace, region)
175 178 # optional - hash the cache_key too for consistent length
176 179 # import uuid
177 180 # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
178 181
179 182 return cache, cache_key
180 183
181 184
182 185 def _namespace_from_query(namespace, query):
183 186 # cache namespace - the token handed in by the
184 187 # option + class we're querying against
185 188 namespace = " ".join([namespace] + [str(x) for x in query._entities])
186 189
187 190 # memcached wants this
188 191 namespace = namespace.replace(' ', '_')
189 192
190 193 return namespace
191 194
192 195
193 196 def _set_cache_parameters(query, region, namespace, cache_key):
194 197
195 198 if hasattr(query, '_cache_parameters'):
196 199 region, namespace, cache_key = query._cache_parameters
197 200 raise ValueError("This query is already configured "
198 201 "for region %r namespace %r" %
199 202 (region, namespace))
200 203 query._cache_parameters = region, namespace, cache_key
201 204
202 205
203 206 class FromCache(MapperOption):
204 207 """Specifies that a Query should load results from a cache."""
205 208
206 209 propagate_to_loaders = False
207 210
208 211 def __init__(self, region, namespace, cache_key=None):
209 212 """Construct a new FromCache.
210 213
211 214 :param region: the cache region. Should be a
212 215 region configured in the Beaker CacheManager.
213 216
214 217 :param namespace: the cache namespace. Should
215 218 be a name uniquely describing the target Query's
216 219 lexical structure.
217 220
218 221 :param cache_key: optional. A string cache key
219 222 that will serve as the key to the query. Use this
220 223 if your query has a huge amount of parameters (such
221 224 as when using in_()) which correspond more simply to
222 225 some other identifier.
223 226
224 227 """
225 228 self.region = region
226 229 self.namespace = namespace
227 230 self.cache_key = cache_key
228 231
229 232 def process_query(self, query):
230 233 """Process a Query during normal loading operation."""
231 234
232 235 _set_cache_parameters(query, self.region, self.namespace,
233 236 self.cache_key)
234 237
235 238
236 239 class RelationshipCache(MapperOption):
237 240 """Specifies that a Query as called within a "lazy load"
238 241 should load results from a cache."""
239 242
240 243 propagate_to_loaders = True
241 244
242 245 def __init__(self, region, namespace, attribute):
243 246 """Construct a new RelationshipCache.
244 247
245 248 :param region: the cache region. Should be a
246 249 region configured in the Beaker CacheManager.
247 250
248 251 :param namespace: the cache namespace. Should
249 252 be a name uniquely describing the target Query's
250 253 lexical structure.
251 254
252 255 :param attribute: A Class.attribute which
253 256 indicates a particular class relationship() whose
254 257 lazy loader should be pulled from the cache.
255 258
256 259 """
257 260 self.region = region
258 261 self.namespace = namespace
259 262 self._relationship_options = {
260 263 (attribute.property.parent.class_, attribute.property.key): self
261 264 }
262 265
263 266 def process_query_conditionally(self, query):
264 267 """Process a Query that is used within a lazy loader.
265 268
266 269 (the process_query_conditionally() method is a SQLAlchemy
267 270 hook invoked only within lazyload.)
268 271
269 272 """
270 273 if query._current_path:
271 274 mapper, key = query._current_path[-2:]
272 275
273 276 for cls in mapper.class_.__mro__:
274 277 if (cls, key) in self._relationship_options:
275 278 relationship_option = \
276 279 self._relationship_options[(cls, key)]
277 280 _set_cache_parameters(
278 281 query,
279 282 relationship_option.region,
280 283 relationship_option.namespace,
281 284 None)
282 285
283 286 def and_(self, option):
284 287 """Chain another RelationshipCache option to this one.
285 288
286 289 While many RelationshipCache objects can be specified on a single
287 290 Query separately, chaining them together allows for a more efficient
288 291 lookup during load.
289 292
290 293 """
291 294 self._relationship_options.update(option._relationship_options)
292 295 return self
293 296
294 297
295 298 def _params_from_query(query):
296 299 """Pull the bind parameter values from a query.
297 300
298 301 This takes into account any scalar attribute bindparam set up.
299 302
300 303 E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
301 304 would return [5, 7].
302 305
303 306 """
304 307 v = []
305 308 def visit_bindparam(bind):
306 309
307 310 if bind.key in query._params:
308 311 value = query._params[bind.key]
309 312 elif bind.callable:
310 313 # lazyloader may dig a callable in here, intended
311 314 # to late-evaluate params after autoflush is called.
312 315 # convert to a scalar value.
313 316 value = bind.callable()
314 317 else:
315 318 value = bind.value
316 319
317 320 v.append(value)
318 321 if query._criterion is not None:
319 322 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
320 323 for f in query._from_obj:
321 324 visitors.traverse(f, {}, {'bindparam':visit_bindparam})
322 325 return v
General Comments 0
You need to be logged in to leave comments. Login now