##// END OF EJS Templates
caches: synced cache logic with vcsserver.
marcink -
r3851:a5ece276 default
parent child Browse files
Show More
@@ -1,75 +1,78 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2019 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 import logging
22 22 from dogpile.cache import register_backend
23 23
24 24 register_backend(
25 25 "dogpile.cache.rc.memory_lru", "rhodecode.lib.rc_cache.backends",
26 26 "LRUMemoryBackend")
27 27
28 28 register_backend(
29 29 "dogpile.cache.rc.file_namespace", "rhodecode.lib.rc_cache.backends",
30 30 "FileNamespaceBackend")
31 31
32 32 register_backend(
33 33 "dogpile.cache.rc.redis", "rhodecode.lib.rc_cache.backends",
34 34 "RedisPickleBackend")
35 35
36 register_backend(
37 "dogpile.cache.rc.redis_msgpack", "rhodecode.lib.rc_cache.backends",
38 "RedisMsgPackBackend")
39
36 40
37 41 log = logging.getLogger(__name__)
38 42
39 43 from . import region_meta
40 44 from .utils import (
41 get_default_cache_settings, key_generator, get_or_create_region,
45 get_default_cache_settings, backend_key_generator, get_or_create_region,
42 46 clear_cache_namespace, make_region, InvalidationContext,
43 47 FreshRegionCache, ActiveRegionCache)
44 48
45 49
46 50 def configure_dogpile_cache(settings):
47 51 cache_dir = settings.get('cache_dir')
48 52 if cache_dir:
49 53 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
50 54
51 55 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
52 56
53 57 # inspect available namespaces
54 58 avail_regions = set()
55 59 for key in rc_cache_data.keys():
56 60 namespace_name = key.split('.', 1)[0]
57 61 avail_regions.add(namespace_name)
58 62 log.debug('dogpile: found following cache regions: %s', avail_regions)
59 63
60 64 # register them into namespace
61 65 for region_name in avail_regions:
62 66 new_region = make_region(
63 67 name=region_name,
64 function_key_generator=key_generator
68 function_key_generator=None
65 69 )
66 70
67 71 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
68
69 log.debug('dogpile: registering a new region %s[%s]',
70 region_name, new_region.__dict__)
72 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
73 log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
71 74 region_meta.dogpile_cache_regions[region_name] = new_region
72 75
73 76
74 77 def includeme(config):
75 78 configure_dogpile_cache(config.registry.settings)
@@ -1,215 +1,267 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2019 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 import time
22 22 import errno
23 23 import logging
24 24
25 import msgpack
25 26 import gevent
26 27
28 from dogpile.cache.api import CachedValue
27 29 from dogpile.cache.backends import memory as memory_backend
28 30 from dogpile.cache.backends import file as file_backend
29 31 from dogpile.cache.backends import redis as redis_backend
30 32 from dogpile.cache.backends.file import NO_VALUE, compat, FileLock
31 33 from dogpile.cache.util import memoized_property
32 34
33 35 from rhodecode.lib.memory_lru_dict import LRUDict, LRUDictDebug
34 36
35 37
36 38 _default_max_size = 1024
37 39
38 40 log = logging.getLogger(__name__)
39 41
40 42
41 43 class LRUMemoryBackend(memory_backend.MemoryBackend):
44 key_prefix = 'lru_mem_backend'
42 45 pickle_values = False
43 46
44 47 def __init__(self, arguments):
45 48 max_size = arguments.pop('max_size', _default_max_size)
46 49
47 50 LRUDictClass = LRUDict
48 51 if arguments.pop('log_key_count', None):
49 52 LRUDictClass = LRUDictDebug
50 53
51 54 arguments['cache_dict'] = LRUDictClass(max_size)
52 55 super(LRUMemoryBackend, self).__init__(arguments)
53 56
54 57 def delete(self, key):
55 58 try:
56 59 del self._cache[key]
57 60 except KeyError:
58 61 # we don't care if key isn't there at deletion
59 62 pass
60 63
61 64 def delete_multi(self, keys):
62 65 for key in keys:
63 66 self.delete(key)
64 67
65 68
66 class Serializer(object):
69 class PickleSerializer(object):
70
67 71 def _dumps(self, value, safe=False):
68 72 try:
69 73 return compat.pickle.dumps(value)
70 74 except Exception:
71 75 if safe:
72 76 return NO_VALUE
73 77 else:
74 78 raise
75 79
76 80 def _loads(self, value, safe=True):
77 81 try:
78 82 return compat.pickle.loads(value)
79 83 except Exception:
80 84 if safe:
81 85 return NO_VALUE
82 86 else:
83 87 raise
84 88
85 89
90 class MsgPackSerializer(object):
91
92 def _dumps(self, value, safe=False):
93 try:
94 return msgpack.packb(value)
95 except Exception:
96 if safe:
97 return NO_VALUE
98 else:
99 raise
100
101 def _loads(self, value, safe=True):
102 """
103 pickle maintained the `CachedValue` wrapper of the tuple
104 msgpack does not, so it must be added back in.
105 """
106 try:
107 value = msgpack.unpackb(value, use_list=False)
108 return CachedValue(*value)
109 except Exception:
110 if safe:
111 return NO_VALUE
112 else:
113 raise
114
115
86 116 import fcntl
87 117 flock_org = fcntl.flock
88 118
89 119
90 120 class CustomLockFactory(FileLock):
91 121
92 122 @memoized_property
93 123 def _module(self):
94 124
95 125 def gevent_flock(fd, operation):
96 126 """
97 127 Gevent compatible flock
98 128 """
99 129 # set non-blocking, this will cause an exception if we cannot acquire a lock
100 130 operation |= fcntl.LOCK_NB
101 131 start_lock_time = time.time()
102 132 timeout = 60 * 15 # 15min
103 133 while True:
104 134 try:
105 135 flock_org(fd, operation)
106 136 # lock has been acquired
107 137 break
108 138 except (OSError, IOError) as e:
109 139 # raise on other errors than Resource temporarily unavailable
110 140 if e.errno != errno.EAGAIN:
111 141 raise
112 142 elif (time.time() - start_lock_time) > timeout:
113 143 # waited to much time on a lock, better fail than loop for ever
114 144 log.error('Failed to acquire lock on `%s` after waiting %ss',
115 145 self.filename, timeout)
116 146 raise
117 147 wait_timeout = 0.03
118 148 log.debug('Failed to acquire lock on `%s`, retry in %ss',
119 149 self.filename, wait_timeout)
120 150 gevent.sleep(wait_timeout)
121 151
122 152 fcntl.flock = gevent_flock
123 153 return fcntl
124 154
125 155
126 class FileNamespaceBackend(Serializer, file_backend.DBMBackend):
156 class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
157 key_prefix = 'file_backend'
127 158
128 159 def __init__(self, arguments):
129 160 arguments['lock_factory'] = CustomLockFactory
130 161 super(FileNamespaceBackend, self).__init__(arguments)
131 162
132 163 def list_keys(self, prefix=''):
164 prefix = '{}:{}'.format(self.key_prefix, prefix)
165
133 166 def cond(v):
134 167 if not prefix:
135 168 return True
136 169
137 170 if v.startswith(prefix):
138 171 return True
139 172 return False
140 173
141 174 with self._dbm_file(True) as dbm:
142 175
143 176 return filter(cond, dbm.keys())
144 177
145 178 def get_store(self):
146 179 return self.filename
147 180
148 181 def get(self, key):
149 182 with self._dbm_file(False) as dbm:
150 183 if hasattr(dbm, 'get'):
151 184 value = dbm.get(key, NO_VALUE)
152 185 else:
153 186 # gdbm objects lack a .get method
154 187 try:
155 188 value = dbm[key]
156 189 except KeyError:
157 190 value = NO_VALUE
158 191 if value is not NO_VALUE:
159 192 value = self._loads(value)
160 193 return value
161 194
162 195 def set(self, key, value):
163 196 with self._dbm_file(True) as dbm:
164 197 dbm[key] = self._dumps(value)
165 198
166 199 def set_multi(self, mapping):
167 200 with self._dbm_file(True) as dbm:
168 201 for key, value in mapping.items():
169 202 dbm[key] = self._dumps(value)
170 203
171 204
172 class RedisPickleBackend(Serializer, redis_backend.RedisBackend):
205 class BaseRedisBackend(redis_backend.RedisBackend):
173 206 def list_keys(self, prefix=''):
174 if prefix:
175 prefix = prefix + '*'
207 prefix = '{}:{}*'.format(self.key_prefix, prefix)
176 208 return self.client.keys(prefix)
177 209
178 210 def get_store(self):
179 211 return self.client.connection_pool
180 212
181 213 def get(self, key):
182 214 value = self.client.get(key)
183 215 if value is None:
184 216 return NO_VALUE
185 217 return self._loads(value)
186 218
219 def get_multi(self, keys):
220 if not keys:
221 return []
222 values = self.client.mget(keys)
223 loads = self._loads
224 return [
225 loads(v) if v is not None else NO_VALUE
226 for v in values]
227
187 228 def set(self, key, value):
188 229 if self.redis_expiration_time:
189 230 self.client.setex(key, self.redis_expiration_time,
190 231 self._dumps(value))
191 232 else:
192 233 self.client.set(key, self._dumps(value))
193 234
194 235 def set_multi(self, mapping):
236 dumps = self._dumps
195 237 mapping = dict(
196 (k, self._dumps(v))
238 (k, dumps(v))
197 239 for k, v in mapping.items()
198 240 )
199 241
200 242 if not self.redis_expiration_time:
201 243 self.client.mset(mapping)
202 244 else:
203 245 pipe = self.client.pipeline()
204 246 for key, value in mapping.items():
205 247 pipe.setex(key, self.redis_expiration_time, value)
206 248 pipe.execute()
207 249
208 250 def get_mutex(self, key):
209 251 u = redis_backend.u
210 252 if self.distributed_lock:
211 253 lock_key = u('_lock_{0}').format(key)
212 254 log.debug('Trying to acquire Redis lock for key %s', lock_key)
213 255 return self.client.lock(lock_key, self.lock_timeout, self.lock_sleep)
214 256 else:
215 257 return None
258
259
260 class RedisPickleBackend(PickleSerializer, BaseRedisBackend):
261 key_prefix = 'redis_pickle_backend'
262 pass
263
264
265 class RedisMsgPackBackend(MsgPackSerializer, BaseRedisBackend):
266 key_prefix = 'redis_msgpack_backend'
267 pass
@@ -1,327 +1,338 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2019 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 import os
21 21 import time
22 22 import logging
23 23 import functools
24 24 import threading
25 25
26 26 from dogpile.cache import CacheRegion
27 27 from dogpile.cache.util import compat
28 28
29 29 import rhodecode
30 30 from rhodecode.lib.utils import safe_str, sha1
31 31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 32 from rhodecode.model.db import Session, CacheKey, IntegrityError
33 33
34 34 from . import region_meta
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class RhodeCodeCacheRegion(CacheRegion):
40 40
41 41 def conditional_cache_on_arguments(
42 42 self, namespace=None,
43 43 expiration_time=None,
44 44 should_cache_fn=None,
45 45 to_str=compat.string_type,
46 46 function_key_generator=None,
47 47 condition=True):
48 48 """
49 49 Custom conditional decorator, that will not touch any dogpile internals if
50 50 condition isn't meet. This works a bit different than should_cache_fn
51 51 And it's faster in cases we don't ever want to compute cached values
52 52 """
53 53 expiration_time_is_callable = compat.callable(expiration_time)
54 54
55 55 if function_key_generator is None:
56 56 function_key_generator = self.function_key_generator
57 57
58 58 def decorator(fn):
59 59 if to_str is compat.string_type:
60 60 # backwards compatible
61 61 key_generator = function_key_generator(namespace, fn)
62 62 else:
63 63 key_generator = function_key_generator(namespace, fn, to_str=to_str)
64 64
65 65 @functools.wraps(fn)
66 66 def decorate(*arg, **kw):
67 67 key = key_generator(*arg, **kw)
68 68
69 69 @functools.wraps(fn)
70 70 def creator():
71 71 return fn(*arg, **kw)
72 72
73 73 if not condition:
74 74 return creator()
75 75
76 76 timeout = expiration_time() if expiration_time_is_callable \
77 77 else expiration_time
78 78
79 79 return self.get_or_create(key, creator, timeout, should_cache_fn)
80 80
81 81 def invalidate(*arg, **kw):
82 82 key = key_generator(*arg, **kw)
83 83 self.delete(key)
84 84
85 85 def set_(value, *arg, **kw):
86 86 key = key_generator(*arg, **kw)
87 87 self.set(key, value)
88 88
89 89 def get(*arg, **kw):
90 90 key = key_generator(*arg, **kw)
91 91 return self.get(key)
92 92
93 93 def refresh(*arg, **kw):
94 94 key = key_generator(*arg, **kw)
95 95 value = fn(*arg, **kw)
96 96 self.set(key, value)
97 97 return value
98 98
99 99 decorate.set = set_
100 100 decorate.invalidate = invalidate
101 101 decorate.refresh = refresh
102 102 decorate.get = get
103 103 decorate.original = fn
104 104 decorate.key_generator = key_generator
105 105 decorate.__wrapped__ = fn
106 106
107 107 return decorate
108 108
109 109 return decorator
110 110
111 111
112 112 def make_region(*arg, **kw):
113 113 return RhodeCodeCacheRegion(*arg, **kw)
114 114
115 115
116 116 def get_default_cache_settings(settings, prefixes=None):
117 117 prefixes = prefixes or []
118 118 cache_settings = {}
119 119 for key in settings.keys():
120 120 for prefix in prefixes:
121 121 if key.startswith(prefix):
122 122 name = key.split(prefix)[1].strip()
123 123 val = settings[key]
124 124 if isinstance(val, compat.string_types):
125 125 val = val.strip()
126 126 cache_settings[name] = val
127 127 return cache_settings
128 128
129 129
130 130 def compute_key_from_params(*args):
131 131 """
132 132 Helper to compute key from given params to be used in cache manager
133 133 """
134 134 return sha1("_".join(map(safe_str, args)))
135 135
136 136
137 def key_generator(namespace, fn):
137 def backend_key_generator(backend):
138 """
139 Special wrapper that also sends over the backend to the key generator
140 """
141 def wrapper(namespace, fn):
142 return key_generator(backend, namespace, fn)
143 return wrapper
144
145
146 def key_generator(backend, namespace, fn):
138 147 fname = fn.__name__
139 148
140 149 def generate_key(*args):
141 namespace_pref = namespace or 'default'
150 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
151 namespace_pref = namespace or 'default_namespace'
142 152 arg_key = compute_key_from_params(*args)
143 final_key = "{}:{}_{}".format(namespace_pref, fname, arg_key)
153 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
144 154
145 155 return final_key
146 156
147 157 return generate_key
148 158
149 159
150 160 def get_or_create_region(region_name, region_namespace=None):
151 161 from rhodecode.lib.rc_cache.backends import FileNamespaceBackend
152 162 region_obj = region_meta.dogpile_cache_regions.get(region_name)
153 163 if not region_obj:
154 164 raise EnvironmentError(
155 165 'Region `{}` not in configured: {}.'.format(
156 166 region_name, region_meta.dogpile_cache_regions.keys()))
157 167
158 168 region_uid_name = '{}:{}'.format(region_name, region_namespace)
159 169 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
160 170 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
161 171 if region_exist:
162 172 log.debug('Using already configured region: %s', region_namespace)
163 173 return region_exist
164 174 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
165 175 expiration_time = region_obj.expiration_time
166 176
167 177 if not os.path.isdir(cache_dir):
168 178 os.makedirs(cache_dir)
169 179 new_region = make_region(
170 name=region_uid_name, function_key_generator=key_generator
180 name=region_uid_name,
181 function_key_generator=backend_key_generator(region_obj.actual_backend)
171 182 )
172 183 namespace_filename = os.path.join(
173 184 cache_dir, "{}.cache.dbm".format(region_namespace))
174 185 # special type that allows 1db per namespace
175 186 new_region.configure(
176 187 backend='dogpile.cache.rc.file_namespace',
177 188 expiration_time=expiration_time,
178 189 arguments={"filename": namespace_filename}
179 190 )
180 191
181 192 # create and save in region caches
182 log.debug('configuring new region: %s',region_uid_name)
193 log.debug('configuring new region: %s', region_uid_name)
183 194 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
184 195
185 196 return region_obj
186 197
187 198
188 199 def clear_cache_namespace(cache_region, cache_namespace_uid):
189 200 region = get_or_create_region(cache_region, cache_namespace_uid)
190 201 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
191 202 num_delete_keys = len(cache_keys)
192 203 if num_delete_keys:
193 204 region.delete_multi(cache_keys)
194 205 return num_delete_keys
195 206
196 207
197 208 class ActiveRegionCache(object):
198 209 def __init__(self, context, cache_data):
199 210 self.context = context
200 211 self.cache_data = cache_data
201 212
202 213 def should_invalidate(self):
203 214 return False
204 215
205 216
206 217 class FreshRegionCache(object):
207 218 def __init__(self, context, cache_data):
208 219 self.context = context
209 220 self.cache_data = cache_data
210 221
211 222 def should_invalidate(self):
212 223 return True
213 224
214 225
215 226 class InvalidationContext(object):
216 227 """
217 228 usage::
218 229
219 230 from rhodecode.lib import rc_cache
220 231
221 232 cache_namespace_uid = CacheKey.SOME_NAMESPACE.format(1)
222 233 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
223 234
224 235 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=True)
225 236 def heavy_compute(cache_name, param1, param2):
226 237 print('COMPUTE {}, {}, {}'.format(cache_name, param1, param2))
227 238
228 239 # invalidation namespace is shared namespace key for all process caches
229 240 # we use it to send a global signal
230 241 invalidation_namespace = 'repo_cache:1'
231 242
232 243 inv_context_manager = rc_cache.InvalidationContext(
233 244 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
234 245 with inv_context_manager as invalidation_context:
235 246 args = ('one', 'two')
236 247 # re-compute and store cache if we get invalidate signal
237 248 if invalidation_context.should_invalidate():
238 249 result = heavy_compute.refresh(*args)
239 250 else:
240 251 result = heavy_compute(*args)
241 252
242 253 compute_time = inv_context_manager.compute_time
243 254 log.debug('result computed in %.3fs', compute_time)
244 255
245 256 # To send global invalidation signal, simply run
246 257 CacheKey.set_invalidate(invalidation_namespace)
247 258
248 259 """
249 260
250 261 def __repr__(self):
251 262 return '<InvalidationContext:{}[{}]>'.format(
252 263 safe_str(self.cache_key), safe_str(self.uid))
253 264
254 265 def __init__(self, uid, invalidation_namespace='',
255 266 raise_exception=False, thread_scoped=None):
256 267 self.uid = uid
257 268 self.invalidation_namespace = invalidation_namespace
258 269 self.raise_exception = raise_exception
259 270 self.proc_id = safe_unicode(rhodecode.CONFIG.get('instance_id') or 'DEFAULT')
260 271 self.thread_id = 'global'
261 272
262 273 if thread_scoped is None:
263 274 # if we set "default" we can override this via .ini settings
264 275 thread_scoped = str2bool(rhodecode.CONFIG.get('cache_thread_scoped'))
265 276
266 277 # Append the thread id to the cache key if this invalidation context
267 278 # should be scoped to the current thread.
268 279 if thread_scoped is True:
269 280 self.thread_id = threading.current_thread().ident
270 281
271 282 self.cache_key = compute_key_from_params(uid)
272 283 self.cache_key = 'proc:{}|thread:{}|params:{}'.format(
273 284 self.proc_id, self.thread_id, self.cache_key)
274 285 self.compute_time = 0
275 286
276 287 def get_or_create_cache_obj(self, uid, invalidation_namespace=''):
277 288 cache_obj = CacheKey.get_active_cache(self.cache_key)
278 289 log.debug('Fetched cache obj %s using %s cache key.', cache_obj, self.cache_key)
279 290 invalidation_namespace = invalidation_namespace or self.invalidation_namespace
280 291 if not cache_obj:
281 292 cache_obj = CacheKey(self.cache_key, cache_args=invalidation_namespace)
282 293 return cache_obj
283 294
284 295 def __enter__(self):
285 296 """
286 297 Test if current object is valid, and return CacheRegion function
287 298 that does invalidation and calculation
288 299 """
289 300 log.debug('Entering cache invalidation check context: %s', self.invalidation_namespace)
290 301 # register or get a new key based on uid
291 302 self.cache_obj = self.get_or_create_cache_obj(uid=self.uid)
292 303 cache_data = self.cache_obj.get_dict()
293 304 self._start_time = time.time()
294 305 if self.cache_obj.cache_active:
295 306 # means our cache obj is existing and marked as it's
296 307 # cache is not outdated, we return ActiveRegionCache
297 308 self.skip_cache_active_change = True
298 309
299 310 return ActiveRegionCache(context=self, cache_data=cache_data)
300 311
301 312 # the key is either not existing or set to False, we return
302 313 # the real invalidator which re-computes value. We additionally set
303 314 # the flag to actually update the Database objects
304 315 self.skip_cache_active_change = False
305 316 return FreshRegionCache(context=self, cache_data=cache_data)
306 317
307 318 def __exit__(self, exc_type, exc_val, exc_tb):
308 319 # save compute time
309 320 self.compute_time = time.time() - self._start_time
310 321
311 322 if self.skip_cache_active_change:
312 323 return
313 324
314 325 try:
315 326 self.cache_obj.cache_active = True
316 327 Session().add(self.cache_obj)
317 328 Session().commit()
318 329 except IntegrityError:
319 330 # if we catch integrity error, it means we inserted this object
320 331 # assumption is that's really an edge race-condition case and
321 332 # it's safe is to skip it
322 333 Session().rollback()
323 334 except Exception:
324 335 log.exception('Failed to commit on cache key update')
325 336 Session().rollback()
326 337 if self.raise_exception:
327 338 raise
@@ -1,108 +1,108 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 import time
22 22
23 23 import pytest
24 24
25 25 from rhodecode.lib import rc_cache
26 26
27 27
28 28 @pytest.mark.usefixtures('app')
29 29 class TestCaches(object):
30 30
31 31 def test_cache_decorator_init_not_configured(self):
32 32 with pytest.raises(EnvironmentError):
33 33 rc_cache.get_or_create_region('dontexist')
34 34
35 35 @pytest.mark.parametrize('region_name', [
36 36 'cache_perms', u'cache_perms',
37 37 ])
38 38 def test_cache_decorator_init(self, region_name):
39 39 namespace = region_name
40 40 cache_region = rc_cache.get_or_create_region(
41 41 region_name, region_namespace=namespace)
42 42 assert cache_region
43 43
44 44 @pytest.mark.parametrize('example_input', [
45 45 ('',),
46 46 (u'/ac',),
47 47 (u'/ac', 1, 2, object()),
48 48 (u'/Δ™Δ‡c', 1, 2, object()),
49 49 ('/Δ…ac',),
50 50 (u'/ac', ),
51 51 ])
52 52 def test_cache_manager_create_key(self, example_input):
53 53 key = rc_cache.utils.compute_key_from_params(*example_input)
54 54 assert key
55 55
56 56 @pytest.mark.parametrize('example_namespace', [
57 57 'namespace', None
58 58 ])
59 59 @pytest.mark.parametrize('example_input', [
60 60 ('',),
61 61 (u'/ac',),
62 62 (u'/ac', 1, 2, object()),
63 63 (u'/Δ™Δ‡c', 1, 2, object()),
64 64 ('/Δ…ac',),
65 65 (u'/ac', ),
66 66 ])
67 67 def test_cache_keygen(self, example_input, example_namespace):
68 68 def func_wrapped():
69 69 return 1
70 func = rc_cache.utils.key_generator(example_namespace, func_wrapped)
70 func = rc_cache.utils.key_generator(None, example_namespace, func_wrapped)
71 71 key = func(*example_input)
72 72 assert key
73 73
74 74 def test_store_value_in_cache(self):
75 75 cache_region = rc_cache.get_or_create_region('cache_perms')
76 76 # make sure we empty the cache now
77 77 cache_region.delete_multi(cache_region.backend.list_keys())
78 78
79 79 assert cache_region.backend.list_keys() == []
80 80
81 81 @cache_region.conditional_cache_on_arguments(expiration_time=5)
82 82 def compute(key):
83 83 return time.time()
84 84
85 85 for x in range(10):
86 86 compute(x)
87 87
88 88 assert len(set(cache_region.backend.list_keys())) == 10
89 89
90 90 def test_store_and_get_value_from_region(self):
91 91 cache_region = rc_cache.get_or_create_region('cache_perms')
92 92 # make sure we empty the cache now
93 93 for key in cache_region.backend.list_keys():
94 94 cache_region.delete(key)
95 95 assert cache_region.backend.list_keys() == []
96 96
97 97 @cache_region.conditional_cache_on_arguments(expiration_time=5)
98 98 def compute(key):
99 99 return time.time()
100 100
101 101 result = set()
102 102 for x in range(10):
103 103 ret = compute('x')
104 104 result.add(ret)
105 105
106 106 # once computed we have only one value (the same from cache)
107 107 # after executing it 10x
108 108 assert len(result) == 1
General Comments 0
You need to be logged in to leave comments. Login now