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