blissdata 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blissdata/__init__.py +17 -0
- blissdata/beacon/__init__.py +1 -0
- blissdata/beacon/_base.py +106 -0
- blissdata/beacon/config.py +24 -0
- blissdata/beacon/data.py +45 -0
- blissdata/beacon/files.py +141 -0
- blissdata/client.py +62 -0
- blissdata/common/__init__.py +15 -0
- blissdata/common/utils.py +159 -0
- blissdata/data/__init__.py +15 -0
- blissdata/data/events/__init__.py +22 -0
- blissdata/data/events/channel.py +149 -0
- blissdata/data/events/lima.py +478 -0
- blissdata/data/events/node.py +28 -0
- blissdata/data/events/scan.py +49 -0
- blissdata/data/events/walk.py +42 -0
- blissdata/data/expiration.py +42 -0
- blissdata/data/lima_image.py +465 -0
- blissdata/data/node.py +1619 -0
- blissdata/data/nodes/__init__.py +11 -0
- blissdata/data/nodes/channel.py +394 -0
- blissdata/data/nodes/dataset.py +82 -0
- blissdata/data/nodes/dataset_collection.py +12 -0
- blissdata/data/nodes/lima.py +422 -0
- blissdata/data/nodes/node_ref_channel.py +34 -0
- blissdata/data/nodes/proposal.py +12 -0
- blissdata/data/nodes/scan.py +192 -0
- blissdata/data/nodes/scan_group.py +12 -0
- blissdata/data/remote_node.py +204 -0
- blissdata/data/scan.py +666 -0
- blissdata/h5api/__init__.py +1 -0
- blissdata/h5api/abstract.py +97 -0
- blissdata/h5api/dynamic_hdf5.py +153 -0
- blissdata/h5api/file_arguments.py +28 -0
- blissdata/h5api/static_hdf5.py +139 -0
- blissdata/h5api/utils/__init__.py +0 -0
- blissdata/h5api/utils/bliss.py +138 -0
- blissdata/h5api/utils/hdf5.py +280 -0
- blissdata/h5api/utils/hdf5_retry.py +98 -0
- blissdata/h5api/utils/lima.py +286 -0
- blissdata/h5api/utils/types.py +13 -0
- blissdata/redis/__init__.py +12 -0
- blissdata/redis/caching.py +390 -0
- blissdata/redis/connection.py +169 -0
- blissdata/redis/manager.py +164 -0
- blissdata/redis/proxy.py +971 -0
- blissdata/redis/scripting.py +24 -0
- blissdata/settings.py +1174 -0
- blissdata/streaming.py +819 -0
- blissdata/streaming_events.py +355 -0
- blissdata/tests/__init__.py +0 -0
- blissdata/tests/beacon/__init__.py +0 -0
- blissdata/tests/beacon/test_config.py +26 -0
- blissdata/tests/beacon/test_data.py +50 -0
- blissdata/tests/beacon/test_files.py +80 -0
- blissdata/tests/conftest.py +0 -0
- blissdata/tests/h5api/__init__.py +0 -0
- blissdata/tests/h5api/scanner.py +390 -0
- blissdata/tests/h5api/test_dynamic_files.py +309 -0
- blissdata/tests/h5api/test_static_files.py +169 -0
- blissdata/tests/redis/__init__.py +0 -0
- blissdata/tests/redis/manager.py +23 -0
- blissdata-0.3.4.dist-info/LICENSE +165 -0
- blissdata-0.3.4.dist-info/METADATA +79 -0
- blissdata-0.3.4.dist-info/RECORD +67 -0
- blissdata-0.3.4.dist-info/WHEEL +5 -0
- blissdata-0.3.4.dist-info/top_level.txt +1 -0
blissdata/redis/proxy.py
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"""Redis database proxies.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import enum
|
|
6
|
+
import weakref
|
|
7
|
+
import fnmatch
|
|
8
|
+
import time
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from collections.abc import MutableMapping
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
import gevent
|
|
14
|
+
import gevent.event
|
|
15
|
+
import gevent.lock
|
|
16
|
+
import redis
|
|
17
|
+
import redis.client
|
|
18
|
+
|
|
19
|
+
from blissdata.redis.caching import RedisCacheError
|
|
20
|
+
from blissdata.redis.caching import RedisCache
|
|
21
|
+
from blissdata.common.utils import grouped
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def key_value_list_or_mapping_to_dict(key_value_list_or_mapping):
|
|
25
|
+
"""Helper function to convert a list key1,value1, ..., keyN, valueN to a dict,
|
|
26
|
+
or to return a mapping as it is
|
|
27
|
+
"""
|
|
28
|
+
if len(key_value_list_or_mapping) == 1:
|
|
29
|
+
# must be a mapping
|
|
30
|
+
assert isinstance(key_value_list_or_mapping[0], MutableMapping)
|
|
31
|
+
mapping = key_value_list_or_mapping[0]
|
|
32
|
+
else:
|
|
33
|
+
# function parameters as key1, value1, ..., keyN, valueN
|
|
34
|
+
mapping = dict(grouped(key_value_list_or_mapping, 2))
|
|
35
|
+
return mapping
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncRedisDbProxy(redis.client.Pipeline):
|
|
39
|
+
"""All redis commands are executed asynchronously. Call `execute`
|
|
40
|
+
to execute the commands or `reset` to cancel them.
|
|
41
|
+
|
|
42
|
+
As this takes a fixed connection from the connection pool, it
|
|
43
|
+
is not greenlet-safe.
|
|
44
|
+
|
|
45
|
+
Reponse callbacks are applied to the result of a certain command.
|
|
46
|
+
Execute callbacks are called upon execution of the pipeline.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
connection_pool=None,
|
|
52
|
+
response_callbacks=None,
|
|
53
|
+
transaction=True,
|
|
54
|
+
shard_hint=None,
|
|
55
|
+
):
|
|
56
|
+
if response_callbacks is None:
|
|
57
|
+
response_callbacks = dict()
|
|
58
|
+
if connection_pool is None:
|
|
59
|
+
raise ValueError("connection_pool not provided")
|
|
60
|
+
self._execute_callbacks = list()
|
|
61
|
+
super().__init__(connection_pool, response_callbacks, transaction, shard_hint)
|
|
62
|
+
|
|
63
|
+
def pipeline(self, **kw):
|
|
64
|
+
kw.setdefault("connection_pool", self.connection_pool)
|
|
65
|
+
kw.setdefault("response_callbacks", self.response_callbacks)
|
|
66
|
+
# Do not use `super` here or you get an instance of the base class
|
|
67
|
+
return self.__class__(**kw)
|
|
68
|
+
|
|
69
|
+
def reset(self):
|
|
70
|
+
super().reset()
|
|
71
|
+
self._execute_callbacks = list()
|
|
72
|
+
|
|
73
|
+
def execute(self, **kw):
|
|
74
|
+
callbacks = self._execute_callbacks
|
|
75
|
+
result = super().execute(**kw)
|
|
76
|
+
for func, args, kwargs in callbacks:
|
|
77
|
+
func(*args, **kwargs)
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def add_execute_callback(self, func, *args, **kw):
|
|
81
|
+
self._execute_callbacks.append((func, args, kw))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MonitoringAsyncRedisDbProxy(AsyncRedisDbProxy):
|
|
85
|
+
"""An asynchronous Redis proxy which monitors time, total buffered
|
|
86
|
+
command size and events per stream.
|
|
87
|
+
|
|
88
|
+
Use `wait_maximum_reached` to wait until one of the monitored resources
|
|
89
|
+
hit their maximum.
|
|
90
|
+
|
|
91
|
+
Use `maximum_is_reached` to manually say the maximum is reached.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, **kw):
|
|
95
|
+
"""
|
|
96
|
+
:param int or None max_stream_events: maximum event per stream
|
|
97
|
+
:param int or None max_bytes: maximum on the buffered commands
|
|
98
|
+
:param num or None max_time: maximum on time since first buffered command
|
|
99
|
+
"""
|
|
100
|
+
self._max_stream_events = kw.pop("max_stream_events", None)
|
|
101
|
+
self._max_bytes = kw.pop("max_bytes", None)
|
|
102
|
+
self._max_time = kw.pop("max_time", None)
|
|
103
|
+
self._max_event = gevent.event.Event()
|
|
104
|
+
self._reset_monitoring()
|
|
105
|
+
super().__init__(**kw)
|
|
106
|
+
|
|
107
|
+
def wait_maximum_reached(self):
|
|
108
|
+
"""Wait until any of the resouce maxima is reached"""
|
|
109
|
+
return self._max_event.wait(timeout=self._time_left)
|
|
110
|
+
|
|
111
|
+
def maximum_is_reached(self):
|
|
112
|
+
"""Causes an existing or future `wait_maximum_reached` call to
|
|
113
|
+
return immediately (until reset).
|
|
114
|
+
"""
|
|
115
|
+
self._max_event.set()
|
|
116
|
+
|
|
117
|
+
def _reset_monitoring(self):
|
|
118
|
+
"""Restart resource monitoring"""
|
|
119
|
+
self._nbytes = 0
|
|
120
|
+
self._nstream_events = Counter()
|
|
121
|
+
self._start_time = None
|
|
122
|
+
self._max_event.clear()
|
|
123
|
+
if (
|
|
124
|
+
self._max_stream_events is None
|
|
125
|
+
and self._max_bytes is None
|
|
126
|
+
and self._max_time is None
|
|
127
|
+
):
|
|
128
|
+
self._max_event.set()
|
|
129
|
+
|
|
130
|
+
def reset(self):
|
|
131
|
+
super().reset()
|
|
132
|
+
self._reset_monitoring()
|
|
133
|
+
|
|
134
|
+
def xadd(self, name, fields, **kw):
|
|
135
|
+
super().xadd(name, fields, **kw)
|
|
136
|
+
self._monitor_stream_events(name)
|
|
137
|
+
|
|
138
|
+
def pipeline_execute_command(self, *args, **options):
|
|
139
|
+
super().pipeline_execute_command(*args, **options)
|
|
140
|
+
self._monitor_time()
|
|
141
|
+
self._monitor_data_size(args)
|
|
142
|
+
|
|
143
|
+
def _monitor_stream_events(self, name):
|
|
144
|
+
"""Increase the stream counter and check maximum"""
|
|
145
|
+
if self._max_event.is_set() or self._max_stream_events is None:
|
|
146
|
+
return
|
|
147
|
+
self._nstream_events[name] += 1
|
|
148
|
+
if self._nstream_events[name] >= self._max_stream_events:
|
|
149
|
+
self.maximum_is_reached()
|
|
150
|
+
|
|
151
|
+
def _monitor_data_size(self, data):
|
|
152
|
+
"""Increase the total buffered command size and check maximum"""
|
|
153
|
+
if self._max_event.is_set() or self._max_bytes is None:
|
|
154
|
+
return
|
|
155
|
+
self._nbytes += sys.getsizeof(data)
|
|
156
|
+
if self._nbytes >= self._max_bytes:
|
|
157
|
+
self.maximum_is_reached()
|
|
158
|
+
|
|
159
|
+
def _monitor_time(self):
|
|
160
|
+
"""Start timing when needed and check time maximum"""
|
|
161
|
+
if self._time_left == 0:
|
|
162
|
+
self.maximum_is_reached()
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def _time_left(self):
|
|
166
|
+
"""Returns `None` when not monitoring time or some maximum is
|
|
167
|
+
already reached.
|
|
168
|
+
"""
|
|
169
|
+
if self._max_event.is_set() or self._max_time is None:
|
|
170
|
+
return None
|
|
171
|
+
if self._start_time is None:
|
|
172
|
+
self._start_time = time.perf_counter()
|
|
173
|
+
return max(self._max_time - (time.perf_counter() - self._start_time), 0)
|
|
174
|
+
|
|
175
|
+
def pipeline(self, **kw):
|
|
176
|
+
kw.setdefault("max_stream_events", self._max_stream_events)
|
|
177
|
+
kw.setdefault("max_bytes", self._max_bytes)
|
|
178
|
+
kw.setdefault("max_time", self._max_time)
|
|
179
|
+
return super().pipeline(**kw)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class MonitoringAsyncRedisDbProxyManager:
|
|
183
|
+
"""Rotate asynchronous Redis proxies and execute in the background
|
|
184
|
+
upon rotation. Usage:
|
|
185
|
+
|
|
186
|
+
mgr = proxy.rotating_pipeline()
|
|
187
|
+
with mgr.async_proxy() as async_proxy:
|
|
188
|
+
async_proxy.set("key1", "value1")
|
|
189
|
+
async_proxy.xadd("key2", {"key": "value2"})
|
|
190
|
+
|
|
191
|
+
with mgr.async_proxy() as async_proxy:
|
|
192
|
+
async_proxy.set("key3", "value3")
|
|
193
|
+
async_proxy.xadd("key4", {"key": "value4"})
|
|
194
|
+
async_proxy.add_execute_callback(func, "var1", kwarg1="kwarg1")
|
|
195
|
+
|
|
196
|
+
mgr.flush() # to force a pipeline execution
|
|
197
|
+
|
|
198
|
+
The underlying pipeline is rotated and executed when `flush` is called
|
|
199
|
+
or when the pipeline has reached its rotating criterea (see MonitoringAsyncRedisDbProxy).
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, async_proxy: MonitoringAsyncRedisDbProxy):
|
|
203
|
+
self._task = None
|
|
204
|
+
self._async_proxy = async_proxy
|
|
205
|
+
# protect the rotation of _async_proxy
|
|
206
|
+
self._rotating_lock = gevent.lock.RLock()
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def _pipeline_is_empty(self) -> bool:
|
|
210
|
+
"""Returns True when no commands are buffered in the pipeline."""
|
|
211
|
+
return not len(self._async_proxy)
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def _inside_task(self) -> bool:
|
|
215
|
+
"""Returns True when the current greenlet is the task greenlet"""
|
|
216
|
+
return gevent.getcurrent() is self._task
|
|
217
|
+
|
|
218
|
+
@contextmanager
|
|
219
|
+
def async_proxy(self):
|
|
220
|
+
"""Context to add commands to the pipeline."""
|
|
221
|
+
with self._rotating_lock:
|
|
222
|
+
async_proxy = self._async_proxy
|
|
223
|
+
yield async_proxy
|
|
224
|
+
if self._inside_task:
|
|
225
|
+
if len(async_proxy):
|
|
226
|
+
async_proxy.execute()
|
|
227
|
+
else:
|
|
228
|
+
self._ensure_task()
|
|
229
|
+
|
|
230
|
+
def flush(self, blocking=True, timeout=None, raise_error=False) -> bool:
|
|
231
|
+
"""Flush all commands in the current pipeline. Returns `True` when flush completed."""
|
|
232
|
+
with self._rotating_lock:
|
|
233
|
+
if self._pipeline_is_empty:
|
|
234
|
+
return True
|
|
235
|
+
if self._inside_task:
|
|
236
|
+
self._async_proxy.execute()
|
|
237
|
+
return True
|
|
238
|
+
task = self._ensure_task()
|
|
239
|
+
# Set the rotation event on which the task is or will be waiting
|
|
240
|
+
self._async_proxy.maximum_is_reached()
|
|
241
|
+
|
|
242
|
+
if blocking and task is not None:
|
|
243
|
+
# Note that while we wait, another `flush` or
|
|
244
|
+
# `async_proxy` call can be executed.
|
|
245
|
+
try:
|
|
246
|
+
with gevent.Timeout(timeout) as local_timeout:
|
|
247
|
+
if raise_error:
|
|
248
|
+
task.get()
|
|
249
|
+
else:
|
|
250
|
+
task.join()
|
|
251
|
+
except gevent.Timeout as raised_timeout:
|
|
252
|
+
if local_timeout is not raised_timeout:
|
|
253
|
+
raise
|
|
254
|
+
return not bool(task)
|
|
255
|
+
|
|
256
|
+
def _ensure_task(self):
|
|
257
|
+
"""Ensure the execute and rotate task is running when the
|
|
258
|
+
pipeline contains commands.
|
|
259
|
+
"""
|
|
260
|
+
# Return when task is already running or no commands in the pipeline
|
|
261
|
+
if self._task or self._pipeline_is_empty:
|
|
262
|
+
return self._task
|
|
263
|
+
|
|
264
|
+
# Raise error when the previous task failed
|
|
265
|
+
if self._task is not None:
|
|
266
|
+
self._task.get()
|
|
267
|
+
|
|
268
|
+
# Launch execute and rotate task
|
|
269
|
+
self._task = gevent.spawn(self._execute_and_rotate_on_maximum)
|
|
270
|
+
return self._task
|
|
271
|
+
|
|
272
|
+
def _execute_and_rotate_on_maximum(self):
|
|
273
|
+
"""Wait for the rotation event and then execute+rotate the pipeline.
|
|
274
|
+
When the pipeline is empty: return immediately.
|
|
275
|
+
"""
|
|
276
|
+
if self._pipeline_is_empty:
|
|
277
|
+
return
|
|
278
|
+
self._async_proxy.wait_maximum_reached()
|
|
279
|
+
with self._rotating_lock:
|
|
280
|
+
async_proxy = self._async_proxy
|
|
281
|
+
self._async_proxy = self._async_proxy.pipeline()
|
|
282
|
+
# As a result of add_execute_callback, execute could cause
|
|
283
|
+
# a call to async_proxy or flush.
|
|
284
|
+
async_proxy.execute()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class RedisDbProxyBase(redis.Redis):
|
|
288
|
+
"""A proxy to a particular Redis server and database as determined
|
|
289
|
+
by the connection pool.
|
|
290
|
+
|
|
291
|
+
The `close` method, called upon garbage collection, will return any
|
|
292
|
+
`redis.connection.Connection` instance this proxy may have back to
|
|
293
|
+
its connection pool. This does not close the connection.
|
|
294
|
+
|
|
295
|
+
The method `pipeline` returns a `redis.client.Pipeline` instance which
|
|
296
|
+
behaves like a proxy, except that all `execute_command` calls are buffered
|
|
297
|
+
until `execute` is called. It also works as a context manager: calls
|
|
298
|
+
`execute` on finalization.
|
|
299
|
+
|
|
300
|
+
The `AsyncRedisDbProxy` instance uses the same connection pool.
|
|
301
|
+
The `AsyncRedisDbProxy` instance is NOT greenlet-safe.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
connection_pool=None,
|
|
307
|
+
async_class=AsyncRedisDbProxy,
|
|
308
|
+
monitoring_async_class=MonitoringAsyncRedisDbProxy,
|
|
309
|
+
**kw,
|
|
310
|
+
):
|
|
311
|
+
self._async_class = async_class
|
|
312
|
+
self._monitoring_async_class = monitoring_async_class
|
|
313
|
+
if connection_pool is None:
|
|
314
|
+
raise ValueError("connection_pool not provided")
|
|
315
|
+
super().__init__(connection_pool=connection_pool, **kw)
|
|
316
|
+
|
|
317
|
+
def hset(self, name, *key_value_list_or_mapping, mapping=None):
|
|
318
|
+
"""hset method, compatible with deprecated hmset
|
|
319
|
+
|
|
320
|
+
If mapping is given: just call redis.Redis "hset"
|
|
321
|
+
If not mapping is given, individual key, value args can be passed,
|
|
322
|
+
or a dictionary
|
|
323
|
+
"""
|
|
324
|
+
if mapping is None:
|
|
325
|
+
mapping = key_value_list_or_mapping_to_dict(key_value_list_or_mapping)
|
|
326
|
+
|
|
327
|
+
return super().hset(name, mapping=mapping)
|
|
328
|
+
|
|
329
|
+
def pipeline(self, **kw):
|
|
330
|
+
kw.setdefault("connection_pool", self.connection_pool)
|
|
331
|
+
kw.setdefault("response_callbacks", self.response_callbacks)
|
|
332
|
+
return self._async_class(**kw)
|
|
333
|
+
|
|
334
|
+
def rotating_pipeline(self, **kw):
|
|
335
|
+
kw.setdefault("connection_pool", self.connection_pool)
|
|
336
|
+
kw.setdefault("response_callbacks", self.response_callbacks)
|
|
337
|
+
async_proxy = self._monitoring_async_class(**kw)
|
|
338
|
+
return MonitoringAsyncRedisDbProxyManager(async_proxy)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class RedisDbProxy(RedisDbProxyBase):
|
|
342
|
+
"""It gets a Connection from the pool every time it executes a command.
|
|
343
|
+
|
|
344
|
+
This proxy is greenlet-safe although the connections are not.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def __init__(self, *args, **kw):
|
|
348
|
+
kw["single_connection_client"] = False
|
|
349
|
+
super().__init__(*args, **kw)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class SingleConnectionRedisDbProxy(RedisDbProxyBase):
|
|
353
|
+
"""It gets a connection from the pool during instantiation and holds that
|
|
354
|
+
connection until `close` is called (called upon garbage collection).
|
|
355
|
+
|
|
356
|
+
This proxy is not greenlet-safe.
|
|
357
|
+
|
|
358
|
+
The `AsyncRedisDbProxy` creates new connections.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, *args, **kw):
|
|
362
|
+
kw["single_connection_client"] = True
|
|
363
|
+
super().__init__(*args, **kw)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class CachingAsyncRedisDbProxy(AsyncRedisDbProxy):
|
|
367
|
+
"""Handle cache manipulation asynchronously, just like the Redis
|
|
368
|
+
commands (i.e. execute upon calling `execute` and drop upon `reset`).
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def __init__(self, root_proxy=None, **kw):
|
|
372
|
+
self._root_proxy = root_proxy
|
|
373
|
+
self._db_cache_tasks = list()
|
|
374
|
+
self._connection = None
|
|
375
|
+
super().__init__(**kw)
|
|
376
|
+
|
|
377
|
+
def pipeline(self, **kw):
|
|
378
|
+
kw.setdefault("root_proxy", self._root_proxy)
|
|
379
|
+
return super().pipeline(**kw)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def connection(self):
|
|
383
|
+
if self._connection is None:
|
|
384
|
+
return self._root_proxy.connection
|
|
385
|
+
else:
|
|
386
|
+
return self._connection
|
|
387
|
+
|
|
388
|
+
@connection.setter
|
|
389
|
+
def connection(self, value):
|
|
390
|
+
self._connection = value
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def _caching_lock(self):
|
|
394
|
+
return self._root_proxy._caching_lock
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def db_cache(self):
|
|
398
|
+
return self._root_proxy.db_cache
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def _enable_caching(self):
|
|
402
|
+
return self._root_proxy._enable_caching
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def _use_caching(self):
|
|
406
|
+
return self._enable_caching and self._db_cache_tasks
|
|
407
|
+
|
|
408
|
+
def reset(self):
|
|
409
|
+
super().reset()
|
|
410
|
+
self._db_cache_tasks.clear()
|
|
411
|
+
|
|
412
|
+
def delete(self, name):
|
|
413
|
+
def delete_func():
|
|
414
|
+
self.db_cache.pop(name, None)
|
|
415
|
+
|
|
416
|
+
self._db_cache_tasks.append(delete_func)
|
|
417
|
+
return super().delete(name)
|
|
418
|
+
|
|
419
|
+
def set(self, name, value, *args, **kwargs):
|
|
420
|
+
def set_func():
|
|
421
|
+
self.db_cache[name] = value
|
|
422
|
+
|
|
423
|
+
self._db_cache_tasks.append(set_func)
|
|
424
|
+
return super().set(name, value, *args, **kwargs)
|
|
425
|
+
|
|
426
|
+
def hdel(self, name, *keys):
|
|
427
|
+
def hdel_func():
|
|
428
|
+
cached_dict = self.db_cache.get(name)
|
|
429
|
+
if cached_dict is not None:
|
|
430
|
+
for k in keys:
|
|
431
|
+
cached_dict.pop(k.encode(), None)
|
|
432
|
+
|
|
433
|
+
self._db_cache_tasks.append(hdel_func)
|
|
434
|
+
return super().hdel(name, *keys)
|
|
435
|
+
|
|
436
|
+
def hset(self, name, *key_value_list_or_mapping, mapping=None):
|
|
437
|
+
if mapping is None:
|
|
438
|
+
mapping = key_value_list_or_mapping_to_dict(key_value_list_or_mapping)
|
|
439
|
+
|
|
440
|
+
def hset_func():
|
|
441
|
+
cached_dict = self.db_cache.get(name)
|
|
442
|
+
if cached_dict is not None:
|
|
443
|
+
for key, value in mapping.items():
|
|
444
|
+
cached_dict[key.encode()] = value
|
|
445
|
+
|
|
446
|
+
self._db_cache_tasks.append(hset_func)
|
|
447
|
+
return super().hset(name, mapping=mapping)
|
|
448
|
+
|
|
449
|
+
def lpop(self, name, *values):
|
|
450
|
+
def lpop_func():
|
|
451
|
+
cached_list = self.db_cache.get(name)
|
|
452
|
+
if cached_list is not None:
|
|
453
|
+
try:
|
|
454
|
+
cached_list.pop(0)
|
|
455
|
+
except IndexError:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
self._db_cache_tasks.append(lpop_func)
|
|
459
|
+
return super().lpop(name, *values)
|
|
460
|
+
|
|
461
|
+
def lpush(self, name, *values):
|
|
462
|
+
def lpush_func():
|
|
463
|
+
cache_list = self.db_cache.get(name)
|
|
464
|
+
if cache_list is not None:
|
|
465
|
+
for v in values:
|
|
466
|
+
cache_list.insert(0, v)
|
|
467
|
+
|
|
468
|
+
self._db_cache_tasks.append(lpush_func)
|
|
469
|
+
return super().lpush(name, *values)
|
|
470
|
+
|
|
471
|
+
def rpush(self, name, *values):
|
|
472
|
+
def rpush_func():
|
|
473
|
+
cache_list = self.db_cache.get(name)
|
|
474
|
+
if cache_list is not None:
|
|
475
|
+
cache_list.extend(values)
|
|
476
|
+
|
|
477
|
+
self._db_cache_tasks.append(rpush_func)
|
|
478
|
+
return super().rpush(name, *values)
|
|
479
|
+
|
|
480
|
+
def rpop(self, name):
|
|
481
|
+
def rpop_func():
|
|
482
|
+
cache_list = self.db_cache.get(name)
|
|
483
|
+
if cache_list is not None:
|
|
484
|
+
try:
|
|
485
|
+
cache_list.pop(-1)
|
|
486
|
+
except IndexError:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
self._db_cache_tasks.append(rpop_func)
|
|
490
|
+
return super().rpop(name)
|
|
491
|
+
|
|
492
|
+
def lrem(self, name, *args):
|
|
493
|
+
def clear_cache():
|
|
494
|
+
self.db_cache.pop(name, None)
|
|
495
|
+
|
|
496
|
+
self._db_cache_tasks.append(clear_cache)
|
|
497
|
+
return super().lrem(name, *args)
|
|
498
|
+
|
|
499
|
+
def _execute_cache_tasks(self):
|
|
500
|
+
for task in self._db_cache_tasks:
|
|
501
|
+
task()
|
|
502
|
+
self._db_cache_tasks.clear()
|
|
503
|
+
|
|
504
|
+
def immediate_execute_command(self, *args, **kw):
|
|
505
|
+
if self._use_caching:
|
|
506
|
+
with self._caching_lock:
|
|
507
|
+
self._execute_cache_tasks()
|
|
508
|
+
return super().immediate_execute_command(*args, **kw)
|
|
509
|
+
else:
|
|
510
|
+
return super().immediate_execute_command(*args, **kw)
|
|
511
|
+
|
|
512
|
+
def execute(self, **kw):
|
|
513
|
+
if self._use_caching:
|
|
514
|
+
with self._caching_lock:
|
|
515
|
+
self._execute_cache_tasks()
|
|
516
|
+
return super().execute(**kw)
|
|
517
|
+
else:
|
|
518
|
+
return super().execute(**kw)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class MonitoringCachingAsyncRedisDbProxy(
|
|
522
|
+
CachingAsyncRedisDbProxy, MonitoringAsyncRedisDbProxy
|
|
523
|
+
):
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class CachedSettingsDict(MutableMapping):
|
|
528
|
+
"""Dictionary of BaseSettings that are cached. This is used so that
|
|
529
|
+
the associated Redis keys are removed from the cache when the setting
|
|
530
|
+
is removed from CachedSettingsDict.
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
def __init__(self, db_cache):
|
|
534
|
+
self._cached_settings = weakref.WeakKeyDictionary()
|
|
535
|
+
self.db_cache = db_cache
|
|
536
|
+
|
|
537
|
+
def __setitem__(self, key, value):
|
|
538
|
+
self._cached_settings[key] = value
|
|
539
|
+
|
|
540
|
+
def __getitem__(self, key):
|
|
541
|
+
return self._cached_settings[key]
|
|
542
|
+
|
|
543
|
+
def __delitem__(self, key):
|
|
544
|
+
name, _ = self._cached_settings[key]
|
|
545
|
+
del self._cached_settings[key]
|
|
546
|
+
try:
|
|
547
|
+
self.db_cache.pop(name, None)
|
|
548
|
+
except RedisCacheError:
|
|
549
|
+
pass # Not cached anyway
|
|
550
|
+
|
|
551
|
+
def __iter__(self):
|
|
552
|
+
return iter(self._cached_settings)
|
|
553
|
+
|
|
554
|
+
def __len__(self):
|
|
555
|
+
return len(self._cached_settings)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def lock_proxy(method):
|
|
559
|
+
"""To protect the proxy from concurrent access."""
|
|
560
|
+
|
|
561
|
+
@wraps(method)
|
|
562
|
+
def _lock_proxy(self, *args, **kwargs):
|
|
563
|
+
with self._caching_lock:
|
|
564
|
+
return method(self, *args, **kwargs)
|
|
565
|
+
|
|
566
|
+
return _lock_proxy
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def assert_tracking(method):
|
|
570
|
+
"""Raise exception when we cannot use the tracking connection."""
|
|
571
|
+
|
|
572
|
+
@wraps(method)
|
|
573
|
+
def _assert_tracking(self, *args, **kwargs):
|
|
574
|
+
if not self._use_tracking_connection():
|
|
575
|
+
raise RedisCacheError(
|
|
576
|
+
f"{repr(method)} can only be executed when we are allowed to use the tracking connection"
|
|
577
|
+
)
|
|
578
|
+
return method(self, *args, **kwargs)
|
|
579
|
+
|
|
580
|
+
return _assert_tracking
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def caching_command(method):
|
|
584
|
+
"""This waits until we are allowed to use the tracking connection
|
|
585
|
+
and prevents other greenlets from using that connection until we
|
|
586
|
+
are done with it.
|
|
587
|
+
|
|
588
|
+
When caching is disabled however, we will not use the tracking
|
|
589
|
+
connection (in fact their is None). As a result we will executed
|
|
590
|
+
the Redis command non-cached over a Connection it gets from the
|
|
591
|
+
Redis connection pool.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
@wraps(method)
|
|
595
|
+
def _caching_command(self, *args, **kw):
|
|
596
|
+
if self._use_caching:
|
|
597
|
+
with self._caching_lock:
|
|
598
|
+
return method(self, *args, **kw)
|
|
599
|
+
else:
|
|
600
|
+
# WARNING: this only works for the current inheritance structure.
|
|
601
|
+
# If you derive a class from CachingRedisDbProxy it will not work.
|
|
602
|
+
parent_method = getattr(super(CachingRedisDbProxy, self), method.__name__)
|
|
603
|
+
return parent_method(*args, **kw)
|
|
604
|
+
|
|
605
|
+
return _caching_command
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class CachingRedisDbProxy(RedisDbProxyBase):
|
|
609
|
+
"""Setting/getting Redis keys through this proxy will cache them in
|
|
610
|
+
the connection pool's cache.
|
|
611
|
+
|
|
612
|
+
When settings are registered with `add_prefetch`, they will be fetched
|
|
613
|
+
when they are missing from the cache. It is not required to have those
|
|
614
|
+
settings cached however. It just refetches whenever invalidated due
|
|
615
|
+
to changes in the Redis database.
|
|
616
|
+
|
|
617
|
+
This could be done only if Redis version >= 6.
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
TYPE = enum.Enum("TYPE", "HASH KEY QUEUE ZSET")
|
|
621
|
+
|
|
622
|
+
def __init__(self, connection_pool=None, **kw):
|
|
623
|
+
self.db_cache = RedisCache(connection_pool=connection_pool)
|
|
624
|
+
self._caching_lock = gevent.lock.RLock()
|
|
625
|
+
self._enable_caching = True
|
|
626
|
+
self._cached_settings = CachedSettingsDict(self.db_cache)
|
|
627
|
+
kw.setdefault("async_class", CachingAsyncRedisDbProxy)
|
|
628
|
+
kw.setdefault("monitoring_async_class", MonitoringCachingAsyncRedisDbProxy)
|
|
629
|
+
super().__init__(connection_pool=connection_pool, **kw)
|
|
630
|
+
self.enable_caching()
|
|
631
|
+
|
|
632
|
+
def pipeline(self, **kw):
|
|
633
|
+
kw.setdefault("root_proxy", self)
|
|
634
|
+
return super().pipeline(**kw)
|
|
635
|
+
|
|
636
|
+
def rotating_pipeline(self, **kw):
|
|
637
|
+
kw.setdefault("root_proxy", self)
|
|
638
|
+
return super().rotating_pipeline(**kw)
|
|
639
|
+
|
|
640
|
+
def _use_tracking_connection(self):
|
|
641
|
+
return self._caching_lock._is_owned()
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def connection(self):
|
|
645
|
+
"""Use the caching connection when RedisCache is connected and
|
|
646
|
+
when we hold the proxy lock.
|
|
647
|
+
"""
|
|
648
|
+
if self._use_tracking_connection():
|
|
649
|
+
# Returns None when cache is disabled
|
|
650
|
+
return self.db_cache.connection
|
|
651
|
+
else:
|
|
652
|
+
# A connection will be taken from the pool
|
|
653
|
+
return None
|
|
654
|
+
|
|
655
|
+
@connection.setter
|
|
656
|
+
def connection(self, value):
|
|
657
|
+
# This proxy should never own a connection
|
|
658
|
+
pass
|
|
659
|
+
|
|
660
|
+
def close(self):
|
|
661
|
+
self.disable_caching()
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def _use_caching(self):
|
|
665
|
+
return self._enable_caching
|
|
666
|
+
|
|
667
|
+
@lock_proxy
|
|
668
|
+
def disable_caching(self):
|
|
669
|
+
"""After this command, the proxy behaves like `RedisDbProxy`.
|
|
670
|
+
All pre-fetched objects will be removed.
|
|
671
|
+
"""
|
|
672
|
+
self._enable_caching = False
|
|
673
|
+
self.clear_all_prefetch()
|
|
674
|
+
self.db_cache.disconnect()
|
|
675
|
+
|
|
676
|
+
@lock_proxy
|
|
677
|
+
def enable_caching(self):
|
|
678
|
+
self._enable_caching = True
|
|
679
|
+
self.db_cache.connect()
|
|
680
|
+
|
|
681
|
+
@lock_proxy
|
|
682
|
+
def add_prefetch(self, *objects):
|
|
683
|
+
"""Adds object to be pre-fetched in block in case of any cache failed.
|
|
684
|
+
Objects will be always kept in memory even if they are not accessed.
|
|
685
|
+
Redis communication happens in the method so not not execute in
|
|
686
|
+
a pipeline.
|
|
687
|
+
|
|
688
|
+
Any setting can be added to be pre-fetched.
|
|
689
|
+
"""
|
|
690
|
+
from blissdata import settings
|
|
691
|
+
|
|
692
|
+
for obj in objects:
|
|
693
|
+
name = obj.name
|
|
694
|
+
if not isinstance(name, (str, bytes)):
|
|
695
|
+
raise TypeError(
|
|
696
|
+
f"Cannot prefetch {obj} inside an asynchronous Redis pipeline"
|
|
697
|
+
)
|
|
698
|
+
if isinstance(obj, settings.SimpleSetting):
|
|
699
|
+
self._cached_settings[obj] = (name, self.TYPE.KEY)
|
|
700
|
+
elif isinstance(obj, settings.BaseHashSetting):
|
|
701
|
+
self._cached_settings[obj] = (name, self.TYPE.HASH)
|
|
702
|
+
elif isinstance(obj, settings.Struct):
|
|
703
|
+
self._cached_settings[obj._proxy] = (name, self.TYPE.HASH)
|
|
704
|
+
else:
|
|
705
|
+
raise ValueError(f"Type not yet managed {obj}")
|
|
706
|
+
|
|
707
|
+
@lock_proxy
|
|
708
|
+
def remove_prefetch(self, *objects):
|
|
709
|
+
for obj in objects:
|
|
710
|
+
self._cached_settings.pop(obj, None)
|
|
711
|
+
|
|
712
|
+
@lock_proxy
|
|
713
|
+
def clear_all_prefetch(self):
|
|
714
|
+
self._cached_settings.clear()
|
|
715
|
+
|
|
716
|
+
@caching_command
|
|
717
|
+
def evalsha(self, script_name, n, *args):
|
|
718
|
+
keys = args[:n]
|
|
719
|
+
super().evalsha(script_name, n, *args)
|
|
720
|
+
# invalidate cache for those keys
|
|
721
|
+
for k in keys:
|
|
722
|
+
self.db_cache.pop(k, None)
|
|
723
|
+
|
|
724
|
+
@caching_command
|
|
725
|
+
def delete(self, name):
|
|
726
|
+
self.db_cache.pop(name, None)
|
|
727
|
+
super().delete(name)
|
|
728
|
+
|
|
729
|
+
# KEY
|
|
730
|
+
@caching_command
|
|
731
|
+
def get(self, name):
|
|
732
|
+
return self._get_cache_key(name)
|
|
733
|
+
|
|
734
|
+
@caching_command
|
|
735
|
+
def set(self, name, value, ex=None, px=None, nx=False, xx=False):
|
|
736
|
+
return_val = super().set(name, value, ex, px, nx, xx)
|
|
737
|
+
self.db_cache[name] = value
|
|
738
|
+
return return_val
|
|
739
|
+
|
|
740
|
+
@caching_command
|
|
741
|
+
def testincr(self, name, amount=1):
|
|
742
|
+
# Just for concurrancy testing. The real incr needs to be atomic
|
|
743
|
+
# so we can't do it through the cache.
|
|
744
|
+
result = super().incr(name, amount=amount)
|
|
745
|
+
self.set(name, b"%d" % result)
|
|
746
|
+
return result
|
|
747
|
+
|
|
748
|
+
# HASH
|
|
749
|
+
@caching_command
|
|
750
|
+
def hdel(self, name, *keys):
|
|
751
|
+
return_val = super().hdel(name, *keys)
|
|
752
|
+
cached_dict = self._get_cache_dict(name)
|
|
753
|
+
for k in keys:
|
|
754
|
+
cached_dict.pop(k.encode(), None)
|
|
755
|
+
return return_val
|
|
756
|
+
|
|
757
|
+
@caching_command
|
|
758
|
+
def hexists(self, name, key):
|
|
759
|
+
cached_dict = self._get_cache_dict(name)
|
|
760
|
+
return key.encode() in cached_dict
|
|
761
|
+
|
|
762
|
+
@caching_command
|
|
763
|
+
def hget(self, name, key):
|
|
764
|
+
cached_dict = self._get_cache_dict(name)
|
|
765
|
+
return cached_dict.get(key.encode() if isinstance(key, str) else key)
|
|
766
|
+
|
|
767
|
+
@caching_command
|
|
768
|
+
def hgetall(self, name):
|
|
769
|
+
return self._get_cache_dict(name)
|
|
770
|
+
|
|
771
|
+
@caching_command
|
|
772
|
+
def hlen(self, name):
|
|
773
|
+
cached_dict = self._get_cache_dict(name)
|
|
774
|
+
return len(cached_dict)
|
|
775
|
+
|
|
776
|
+
@caching_command
|
|
777
|
+
def hset(self, name, *key_value_list_or_mapping, mapping=None):
|
|
778
|
+
if mapping is None:
|
|
779
|
+
mapping = key_value_list_or_mapping_to_dict(key_value_list_or_mapping)
|
|
780
|
+
cached_dict = self._get_cache_dict(name)
|
|
781
|
+
for key, value in mapping.items():
|
|
782
|
+
cached_dict[key.encode()] = value
|
|
783
|
+
|
|
784
|
+
return super().hset(name, mapping=mapping)
|
|
785
|
+
|
|
786
|
+
@caching_command
|
|
787
|
+
def hscan(self, name, cursor=0, match=None, count=None):
|
|
788
|
+
cached_dict = self._get_cache_dict(name)
|
|
789
|
+
if count is None or count >= len(cached_dict):
|
|
790
|
+
if match is None:
|
|
791
|
+
return 0, cached_dict
|
|
792
|
+
else:
|
|
793
|
+
return (
|
|
794
|
+
0,
|
|
795
|
+
{
|
|
796
|
+
k: v
|
|
797
|
+
for k, v in cached_dict.items()
|
|
798
|
+
if fnmatch.fnmatch(k.decode(), match)
|
|
799
|
+
},
|
|
800
|
+
)
|
|
801
|
+
else:
|
|
802
|
+
# This part is really not optimize but it's not use
|
|
803
|
+
# in the base code.
|
|
804
|
+
key_list = list(cached_dict.keys())
|
|
805
|
+
return_dict = dict()
|
|
806
|
+
index = cursor
|
|
807
|
+
while count:
|
|
808
|
+
try:
|
|
809
|
+
key = key_list[index]
|
|
810
|
+
except IndexError:
|
|
811
|
+
return 0, return_dict
|
|
812
|
+
index += 1
|
|
813
|
+
if match is not None and not fnmatch.fnmatch(key.decode(), match):
|
|
814
|
+
continue
|
|
815
|
+
return_dict[key] = cached_dict[key]
|
|
816
|
+
count -= 1
|
|
817
|
+
return index, return_dict
|
|
818
|
+
|
|
819
|
+
# LIST COMMANDS
|
|
820
|
+
@caching_command
|
|
821
|
+
def lindex(self, name, index):
|
|
822
|
+
cache_list = self._get_cache_list(name)
|
|
823
|
+
return cache_list[index]
|
|
824
|
+
|
|
825
|
+
@caching_command
|
|
826
|
+
def llen(self, name):
|
|
827
|
+
cache_list = self._get_cache_list(name)
|
|
828
|
+
return len(cache_list)
|
|
829
|
+
|
|
830
|
+
@caching_command
|
|
831
|
+
def lpop(self, name):
|
|
832
|
+
return_val = super().lpop(name)
|
|
833
|
+
cache_list = self._get_cache_list(name)
|
|
834
|
+
if cache_list and return_val == cache_list[0]:
|
|
835
|
+
cache_list.pop(0)
|
|
836
|
+
return return_val
|
|
837
|
+
|
|
838
|
+
@caching_command
|
|
839
|
+
def lpush(self, name, *values):
|
|
840
|
+
return_val = super().lpush(name, *values)
|
|
841
|
+
cache_list = self._get_cache_list(name)
|
|
842
|
+
for v in values:
|
|
843
|
+
cache_list.insert(0, v)
|
|
844
|
+
return return_val
|
|
845
|
+
|
|
846
|
+
@caching_command
|
|
847
|
+
def rpush(self, name, *values):
|
|
848
|
+
return_val = super().lpush(name, *values)
|
|
849
|
+
cache_list = self._get_cache_list(name)
|
|
850
|
+
cache_list.extend(values)
|
|
851
|
+
return return_val
|
|
852
|
+
|
|
853
|
+
@caching_command
|
|
854
|
+
def lrange(self, name, start, end):
|
|
855
|
+
cache_list = self._get_cache_list(name)
|
|
856
|
+
if end == -1:
|
|
857
|
+
end = len(cache_list)
|
|
858
|
+
else:
|
|
859
|
+
end += 1
|
|
860
|
+
return cache_list[start:end]
|
|
861
|
+
|
|
862
|
+
@caching_command
|
|
863
|
+
def rpop(self, name):
|
|
864
|
+
return_val = super().rpop(name)
|
|
865
|
+
cache_list = self._get_cache_list(name)
|
|
866
|
+
if cache_list and return_val == cache_list[-1]:
|
|
867
|
+
cache_list.pop(-1)
|
|
868
|
+
return return_val
|
|
869
|
+
|
|
870
|
+
@caching_command
|
|
871
|
+
def lrem(self, name, count, value):
|
|
872
|
+
return_val = super().lrem(name, count, value)
|
|
873
|
+
if count >= 0:
|
|
874
|
+
cache_list = self._get_cache_list(name)
|
|
875
|
+
for i in range(return_val):
|
|
876
|
+
try:
|
|
877
|
+
cache_list.remove(value)
|
|
878
|
+
except ValueError: # already removed
|
|
879
|
+
pass
|
|
880
|
+
else: # re-synchronization on next get
|
|
881
|
+
self.db_cache.pop(name, None)
|
|
882
|
+
|
|
883
|
+
# SORTED SET COMMANDS
|
|
884
|
+
@caching_command
|
|
885
|
+
def zrange(
|
|
886
|
+
self, name, start, end, desc=False, withscores=False, score_cast_func=float
|
|
887
|
+
):
|
|
888
|
+
cache_sorted_set = self._get_cache_sorted_set(name)
|
|
889
|
+
if end == -1:
|
|
890
|
+
end = len(cache_sorted_set)
|
|
891
|
+
else:
|
|
892
|
+
end += 1
|
|
893
|
+
if desc:
|
|
894
|
+
items = reversed(cache_sorted_set.items())
|
|
895
|
+
else:
|
|
896
|
+
items = cache_sorted_set.items()
|
|
897
|
+
|
|
898
|
+
if withscores:
|
|
899
|
+
return list(items[start:end])
|
|
900
|
+
return list(x for x, y in items)
|
|
901
|
+
|
|
902
|
+
@assert_tracking
|
|
903
|
+
def _get_cache_dict(self, name):
|
|
904
|
+
cached_dict = self.db_cache.get(name)
|
|
905
|
+
if cached_dict is None:
|
|
906
|
+
cached_dict = self._fill_cache(name, self.TYPE.HASH)
|
|
907
|
+
return cached_dict
|
|
908
|
+
|
|
909
|
+
@assert_tracking
|
|
910
|
+
def _get_cache_key(self, name):
|
|
911
|
+
value = self.db_cache.get(name)
|
|
912
|
+
if value is None and name not in self.db_cache:
|
|
913
|
+
value = self._fill_cache(name, self.TYPE.KEY)
|
|
914
|
+
return value
|
|
915
|
+
|
|
916
|
+
@assert_tracking
|
|
917
|
+
def _get_cache_list(self, name):
|
|
918
|
+
values = self.db_cache.get(name)
|
|
919
|
+
if values is None:
|
|
920
|
+
values = self._fill_cache(name, self.TYPE.QUEUE)
|
|
921
|
+
return values
|
|
922
|
+
|
|
923
|
+
@assert_tracking
|
|
924
|
+
def _get_cache_sorted_set(self, name):
|
|
925
|
+
values = self.db_cache.get(name)
|
|
926
|
+
if values is None:
|
|
927
|
+
# return a list with name and score
|
|
928
|
+
# change it to dict
|
|
929
|
+
values = dict(self._fill_cache(name, self.TYPE.ZSET))
|
|
930
|
+
self.db_cache[name] = values
|
|
931
|
+
return values
|
|
932
|
+
|
|
933
|
+
@assert_tracking
|
|
934
|
+
def _fill_cache(self, name, object_type):
|
|
935
|
+
"""This method gets the value of Redis key "name" from the Redis
|
|
936
|
+
database and caches it. In addition we will fetch all the
|
|
937
|
+
"prefetch" keys which have not been cached yet (or were invalidated).
|
|
938
|
+
|
|
939
|
+
:param str name: Redis key name
|
|
940
|
+
:param TYPE object_type: Redis value type
|
|
941
|
+
:returns: the value of the Redis key
|
|
942
|
+
"""
|
|
943
|
+
cached_settings = {name: object_type}
|
|
944
|
+
fetch_names = {name}
|
|
945
|
+
|
|
946
|
+
# Add prefetch objects that are currently not cached
|
|
947
|
+
cached_settings.update(
|
|
948
|
+
{name: obj_type for name, obj_type in self._cached_settings.values()}
|
|
949
|
+
)
|
|
950
|
+
fetch_names.update(cached_settings.keys() - self.db_cache.keys())
|
|
951
|
+
|
|
952
|
+
# Fetch all values from Redis
|
|
953
|
+
pipeline = self.pipeline()
|
|
954
|
+
for obj_name in fetch_names:
|
|
955
|
+
obj_type = cached_settings[obj_name]
|
|
956
|
+
if obj_type == self.TYPE.HASH:
|
|
957
|
+
pipeline.hgetall(obj_name)
|
|
958
|
+
elif obj_type == self.TYPE.KEY:
|
|
959
|
+
pipeline.get(obj_name)
|
|
960
|
+
elif obj_type == self.TYPE.QUEUE:
|
|
961
|
+
pipeline.lrange(obj_name, 0, -1)
|
|
962
|
+
elif obj_type == self.TYPE.ZSET:
|
|
963
|
+
pipeline.zrange(obj_name, 0, -1, withscores=True)
|
|
964
|
+
pipeline_result = pipeline.execute()
|
|
965
|
+
|
|
966
|
+
# Fill the cache with those values
|
|
967
|
+
for obj_name, result in zip(fetch_names, pipeline_result):
|
|
968
|
+
self.db_cache[obj_name] = result
|
|
969
|
+
|
|
970
|
+
# Return the value of the key we actually asked for
|
|
971
|
+
return self.db_cache[name]
|