gpdatacached 0.1.0__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.
- gpdatacached/__init__.py +79 -0
- gpdatacached/builtins/__init__.py +3 -0
- gpdatacached/builtins/bool_object.py +25 -0
- gpdatacached/builtins/datetime_object.py +72 -0
- gpdatacached/builtins/dict_object.py +195 -0
- gpdatacached/builtins/float_object.py +26 -0
- gpdatacached/builtins/int_object.py +26 -0
- gpdatacached/builtins/list_object.py +232 -0
- gpdatacached/builtins/none_object.py +25 -0
- gpdatacached/builtins/registry.py +110 -0
- gpdatacached/builtins/set_object.py +161 -0
- gpdatacached/builtins/str_object.py +23 -0
- gpdatacached/builtins/tuple_object.py +49 -0
- gpdatacached/codec.py +47 -0
- gpdatacached/config.py +101 -0
- gpdatacached/container.py +205 -0
- gpdatacached/domain.py +362 -0
- gpdatacached/exceptions.py +112 -0
- gpdatacached/extensions/__init__.py +0 -0
- gpdatacached/extensions/pandas_support.py +1635 -0
- gpdatacached/extensions/pydantic_support.py +169 -0
- gpdatacached/gc.py +282 -0
- gpdatacached/keys.py +150 -0
- gpdatacached/lifecycle.py +171 -0
- gpdatacached/locking.py +163 -0
- gpdatacached/meta.py +126 -0
- gpdatacached/namespace.py +602 -0
- gpdatacached/object.py +318 -0
- gpdatacached/refset.py +54 -0
- gpdatacached/scalar.py +56 -0
- gpdatacached/types.py +296 -0
- gpdatacached-0.1.0.dist-info/METADATA +171 -0
- gpdatacached-0.1.0.dist-info/RECORD +35 -0
- gpdatacached-0.1.0.dist-info/WHEEL +4 -0
- gpdatacached-0.1.0.dist-info/licenses/LICENSE +21 -0
gpdatacached/domain.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# src/gpdatacached/domain.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import redis
|
|
9
|
+
from gpconfig import GPConfigurable
|
|
10
|
+
|
|
11
|
+
from .config import GPDCDomainConfig
|
|
12
|
+
from .keys import (
|
|
13
|
+
GLOBAL_META_KEY,
|
|
14
|
+
GLOBAL_DOMAINS_KEY,
|
|
15
|
+
make_domain_meta_key,
|
|
16
|
+
make_domain_namespaces_key,
|
|
17
|
+
make_namespace_meta_key,
|
|
18
|
+
validate_domain_name,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .config import GPDCDomainConfig
|
|
23
|
+
from .namespace import GPDCNamespace
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Wire protocol version (Major.Minor), changes only when Redis data layout
|
|
28
|
+
# becomes incompatible. Do not confuse with the library __version__.
|
|
29
|
+
GPDC_PROTOCOL_VERSION = "1.0"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GPDCDomain(GPConfigurable):
|
|
33
|
+
"""GPDC domain: a Redis connection pool and the configuration root."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, name: str, config: GPDCDomainConfig) -> None:
|
|
36
|
+
super().__init__(config)
|
|
37
|
+
validate_domain_name(name)
|
|
38
|
+
self._name = name
|
|
39
|
+
self._namespaces: dict[str, GPDCNamespace] = {}
|
|
40
|
+
self._gc_thread: threading.Thread | None = None
|
|
41
|
+
self._gc_stop_event = threading.Event()
|
|
42
|
+
self._closed = False
|
|
43
|
+
self._redis: redis.Redis = None # type: ignore[assignment]
|
|
44
|
+
self._connect()
|
|
45
|
+
self._start_gc()
|
|
46
|
+
|
|
47
|
+
def _connect(self) -> None:
|
|
48
|
+
cfg = self._config
|
|
49
|
+
pool = redis.ConnectionPool(
|
|
50
|
+
host=cfg.redis_host,
|
|
51
|
+
port=cfg.redis_port,
|
|
52
|
+
db=cfg.redis_db,
|
|
53
|
+
password=cfg.redis_password or None,
|
|
54
|
+
decode_responses=True,
|
|
55
|
+
)
|
|
56
|
+
self._redis = redis.Redis(connection_pool=pool)
|
|
57
|
+
self._init_global_meta()
|
|
58
|
+
self._init_domain_meta()
|
|
59
|
+
self._init_predefined_namespaces()
|
|
60
|
+
|
|
61
|
+
def _init_global_meta(self) -> None:
|
|
62
|
+
"""Initialize or verify global GPDC metadata."""
|
|
63
|
+
existing = self._redis.hget(GLOBAL_META_KEY, "version")
|
|
64
|
+
if existing is None:
|
|
65
|
+
self._redis.hset(GLOBAL_META_KEY, "version", GPDC_PROTOCOL_VERSION)
|
|
66
|
+
elif existing != GPDC_PROTOCOL_VERSION:
|
|
67
|
+
raise RuntimeError(f"Incompatible GPDC version in Redis: {existing!r} (expected {GPDC_PROTOCOL_VERSION!r})")
|
|
68
|
+
|
|
69
|
+
def _init_domain_meta(self) -> None:
|
|
70
|
+
"""Initialize or verify domain metadata in Redis.
|
|
71
|
+
|
|
72
|
+
Legacy fields (``default_cache_mode``, ``default_ttl_seconds``) keep
|
|
73
|
+
their existing silent write-once behaviour: written when the domain is
|
|
74
|
+
new, never compared against config thereafter.
|
|
75
|
+
|
|
76
|
+
Lock fields (``default_lock_ttl_seconds``, etc.) follow the
|
|
77
|
+
``_init_global_meta`` write-or-verify pattern: written when new,
|
|
78
|
+
backfilled if missing, raise ``GPDCLockConfigInconsistentError`` on
|
|
79
|
+
mismatch.
|
|
80
|
+
"""
|
|
81
|
+
cfg = self._config
|
|
82
|
+
meta_key = make_domain_meta_key(self._name)
|
|
83
|
+
existing = self._redis.hgetall(meta_key)
|
|
84
|
+
|
|
85
|
+
lock_fields = {
|
|
86
|
+
"default_lock_ttl_seconds": str(cfg.default_lock_ttl_seconds),
|
|
87
|
+
"default_lock_acquire_timeout_seconds": str(cfg.default_lock_acquire_timeout_seconds),
|
|
88
|
+
"gc_lock_ttl_seconds": str(cfg.gc_lock_ttl_seconds),
|
|
89
|
+
"gc_lock_wait_seconds": str(cfg.gc_lock_wait_seconds),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if not existing:
|
|
93
|
+
# New domain: write legacy + lock fields in one shot.
|
|
94
|
+
self._redis.hset(
|
|
95
|
+
meta_key,
|
|
96
|
+
mapping={
|
|
97
|
+
"default_cache_mode": cfg.default_cache_mode,
|
|
98
|
+
"default_ttl_seconds": str(cfg.default_ttl_seconds),
|
|
99
|
+
**lock_fields,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
# Existing domain.
|
|
104
|
+
# Legacy fields: untouched (silent write-once, matches prior behaviour).
|
|
105
|
+
# Lock fields: write-or-verify.
|
|
106
|
+
to_write: dict[str, str] = {}
|
|
107
|
+
conflicts: dict[str, tuple[str, str]] = {}
|
|
108
|
+
for field, expected in lock_fields.items():
|
|
109
|
+
actual = existing.get(field)
|
|
110
|
+
if actual is None:
|
|
111
|
+
to_write[field] = expected
|
|
112
|
+
elif actual != expected:
|
|
113
|
+
conflicts[field] = (actual, expected)
|
|
114
|
+
|
|
115
|
+
if conflicts:
|
|
116
|
+
from .exceptions import GPDCLockConfigInconsistentError
|
|
117
|
+
|
|
118
|
+
legal_values = {k: existing[k] for k in lock_fields if k in existing}
|
|
119
|
+
raise GPDCLockConfigInconsistentError(
|
|
120
|
+
domain=self._name,
|
|
121
|
+
conflicts=conflicts,
|
|
122
|
+
legal_values=legal_values,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if to_write:
|
|
126
|
+
self._redis.hset(meta_key, mapping=to_write)
|
|
127
|
+
|
|
128
|
+
# Always register this domain in the global domains set.
|
|
129
|
+
self._redis.sadd(GLOBAL_DOMAINS_KEY, self._name)
|
|
130
|
+
|
|
131
|
+
def _init_predefined_namespaces(self) -> None:
|
|
132
|
+
"""Create predefined namespaces declared in config, applying per-namespace settings.
|
|
133
|
+
|
|
134
|
+
Delegates config-priority logic to GPDCNamespace.__init__: if the
|
|
135
|
+
namespace does not yet exist in Redis the per-namespace config is used;
|
|
136
|
+
if it already exists the Redis metadata is left untouched.
|
|
137
|
+
"""
|
|
138
|
+
from .namespace import GPDCNamespace
|
|
139
|
+
|
|
140
|
+
for ns_name, ns_config in self._config.namespaces.items():
|
|
141
|
+
if ns_name not in self._namespaces:
|
|
142
|
+
self._namespaces[ns_name] = GPDCNamespace(ns_name, self, config=ns_config)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def name(self) -> str:
|
|
146
|
+
return self._name
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
return f"GPDCDomain({self._name!r})"
|
|
150
|
+
|
|
151
|
+
def _check_open(self) -> None:
|
|
152
|
+
if self._closed:
|
|
153
|
+
raise RuntimeError(f"GPDCDomain {self._name!r} is closed")
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def redis(self) -> redis.Redis:
|
|
157
|
+
self._check_open()
|
|
158
|
+
return self._redis
|
|
159
|
+
|
|
160
|
+
def ensure_namespace(self, name: str):
|
|
161
|
+
self._check_open()
|
|
162
|
+
from .namespace import GPDCNamespace
|
|
163
|
+
|
|
164
|
+
if name not in self._namespaces:
|
|
165
|
+
self._namespaces[name] = GPDCNamespace(name, self)
|
|
166
|
+
return self._namespaces[name]
|
|
167
|
+
|
|
168
|
+
def get_namespace(self, name: str):
|
|
169
|
+
return self[name]
|
|
170
|
+
|
|
171
|
+
def __contains__(self, name: object) -> bool:
|
|
172
|
+
self._check_open()
|
|
173
|
+
if not isinstance(name, str):
|
|
174
|
+
return False
|
|
175
|
+
return self._redis.sismember(make_domain_namespaces_key(self._name), name)
|
|
176
|
+
|
|
177
|
+
def __iter__(self):
|
|
178
|
+
self._check_open()
|
|
179
|
+
return iter(self._redis.smembers(make_domain_namespaces_key(self._name)))
|
|
180
|
+
|
|
181
|
+
def __len__(self) -> int:
|
|
182
|
+
self._check_open()
|
|
183
|
+
return self._redis.scard(make_domain_namespaces_key(self._name))
|
|
184
|
+
|
|
185
|
+
def __getitem__(self, name: str):
|
|
186
|
+
self._check_open()
|
|
187
|
+
if not self._redis.sismember(make_domain_namespaces_key(self._name), name):
|
|
188
|
+
raise KeyError(name)
|
|
189
|
+
if name not in self._namespaces:
|
|
190
|
+
from .namespace import GPDCNamespace
|
|
191
|
+
|
|
192
|
+
self._namespaces[name] = GPDCNamespace(name, self)
|
|
193
|
+
return self._namespaces[name]
|
|
194
|
+
|
|
195
|
+
def get_default_cache_mode(self) -> str:
|
|
196
|
+
"""Effective default_cache_mode for this domain."""
|
|
197
|
+
meta_key = make_domain_meta_key(self._name)
|
|
198
|
+
val = self._redis.hget(meta_key, "default_cache_mode")
|
|
199
|
+
if val:
|
|
200
|
+
return val
|
|
201
|
+
return self._config.default_cache_mode
|
|
202
|
+
|
|
203
|
+
def get_default_ttl_seconds(self) -> int:
|
|
204
|
+
"""Effective default_ttl_seconds for this domain."""
|
|
205
|
+
meta_key = make_domain_meta_key(self._name)
|
|
206
|
+
raw = self._redis.hget(meta_key, "default_ttl_seconds")
|
|
207
|
+
if raw:
|
|
208
|
+
return int(raw)
|
|
209
|
+
return self._config.default_ttl_seconds
|
|
210
|
+
|
|
211
|
+
def get_default_lock_ttl_seconds(self) -> int:
|
|
212
|
+
"""Effective ``default_lock_ttl_seconds`` for this domain."""
|
|
213
|
+
meta_key = make_domain_meta_key(self._name)
|
|
214
|
+
raw = self._redis.hget(meta_key, "default_lock_ttl_seconds")
|
|
215
|
+
return int(raw) if raw else self._config.default_lock_ttl_seconds
|
|
216
|
+
|
|
217
|
+
def get_default_lock_acquire_timeout_seconds(self) -> int:
|
|
218
|
+
"""Effective ``default_lock_acquire_timeout_seconds`` for this domain."""
|
|
219
|
+
meta_key = make_domain_meta_key(self._name)
|
|
220
|
+
raw = self._redis.hget(meta_key, "default_lock_acquire_timeout_seconds")
|
|
221
|
+
return int(raw) if raw else self._config.default_lock_acquire_timeout_seconds
|
|
222
|
+
|
|
223
|
+
def get_gc_lock_ttl_seconds(self) -> int:
|
|
224
|
+
"""Effective ``gc_lock_ttl_seconds`` for this domain."""
|
|
225
|
+
meta_key = make_domain_meta_key(self._name)
|
|
226
|
+
raw = self._redis.hget(meta_key, "gc_lock_ttl_seconds")
|
|
227
|
+
return int(raw) if raw else self._config.gc_lock_ttl_seconds
|
|
228
|
+
|
|
229
|
+
def get_gc_lock_wait_seconds(self) -> int:
|
|
230
|
+
"""Effective ``gc_lock_wait_seconds`` for this domain."""
|
|
231
|
+
meta_key = make_domain_meta_key(self._name)
|
|
232
|
+
raw = self._redis.hget(meta_key, "gc_lock_wait_seconds")
|
|
233
|
+
return int(raw) if raw else self._config.gc_lock_wait_seconds
|
|
234
|
+
|
|
235
|
+
def _start_gc(self) -> None:
|
|
236
|
+
cfg = self._config
|
|
237
|
+
if not cfg.gc_enabled:
|
|
238
|
+
return
|
|
239
|
+
self._gc_thread = threading.Thread(target=self._gc_loop, daemon=True, name=f"gpdc-gc-{self._name}")
|
|
240
|
+
self._gc_thread.start()
|
|
241
|
+
|
|
242
|
+
def _gc_loop(self) -> None:
|
|
243
|
+
from .gc import run_gc_for_namespace
|
|
244
|
+
|
|
245
|
+
interval = self._config.gc_interval_seconds
|
|
246
|
+
while not self._gc_stop_event.wait(timeout=interval):
|
|
247
|
+
try:
|
|
248
|
+
for ns_name in self:
|
|
249
|
+
run_gc_for_namespace(self._redis, self._name, ns_name)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning("GPDC GC error in domain %s: %s", self._name, e)
|
|
252
|
+
|
|
253
|
+
def run_gc(self, namespace: str | None = None) -> int:
|
|
254
|
+
"""Run GC manually. If namespace is None, run for all namespaces."""
|
|
255
|
+
self._check_open()
|
|
256
|
+
from .gc import run_gc_for_namespace
|
|
257
|
+
|
|
258
|
+
total = 0
|
|
259
|
+
if namespace:
|
|
260
|
+
total += run_gc_for_namespace(self._redis, self._name, namespace)
|
|
261
|
+
else:
|
|
262
|
+
for ns_name in self:
|
|
263
|
+
total += run_gc_for_namespace(self._redis, self._name, ns_name)
|
|
264
|
+
return total
|
|
265
|
+
|
|
266
|
+
def delete_namespace(self, name: str) -> None:
|
|
267
|
+
self._check_open()
|
|
268
|
+
ns_key = make_domain_namespaces_key(self._name)
|
|
269
|
+
if not self._redis.sismember(ns_key, name):
|
|
270
|
+
raise KeyError(name)
|
|
271
|
+
pattern = f"gpdc:{self._name}:{name}:*"
|
|
272
|
+
cursor = 0
|
|
273
|
+
while True:
|
|
274
|
+
cursor, keys = self._redis.scan(cursor=cursor, match=pattern, count=100)
|
|
275
|
+
if keys:
|
|
276
|
+
self._redis.delete(*keys)
|
|
277
|
+
if cursor == 0:
|
|
278
|
+
break
|
|
279
|
+
self._redis.delete(make_namespace_meta_key(self._name, name))
|
|
280
|
+
self._redis.srem(ns_key, name)
|
|
281
|
+
self._namespaces.pop(name, None)
|
|
282
|
+
logger.info("Deleted namespace %r from domain %r", name, self._name)
|
|
283
|
+
|
|
284
|
+
def destroy(self) -> None:
|
|
285
|
+
self._check_open()
|
|
286
|
+
ns_key = make_domain_namespaces_key(self._name)
|
|
287
|
+
for ns_name in list(self._redis.smembers(ns_key)):
|
|
288
|
+
self.delete_namespace(ns_name)
|
|
289
|
+
self._redis.delete(make_domain_meta_key(self._name))
|
|
290
|
+
self._redis.delete(ns_key)
|
|
291
|
+
self._redis.srem(GLOBAL_DOMAINS_KEY, self._name)
|
|
292
|
+
self._namespaces.clear()
|
|
293
|
+
logger.info("Destroyed domain %r", self._name)
|
|
294
|
+
self.close()
|
|
295
|
+
|
|
296
|
+
def close(self) -> None:
|
|
297
|
+
if self._closed:
|
|
298
|
+
return
|
|
299
|
+
self._closed = True
|
|
300
|
+
self._gc_stop_event.set()
|
|
301
|
+
if self._gc_thread and self._gc_thread.is_alive():
|
|
302
|
+
self._gc_thread.join(timeout=5)
|
|
303
|
+
self._gc_thread = None
|
|
304
|
+
if self._redis is not None:
|
|
305
|
+
self._redis.close()
|
|
306
|
+
self._redis = None
|
|
307
|
+
|
|
308
|
+
def __enter__(self):
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
312
|
+
self.close()
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def __del__(self):
|
|
316
|
+
# __del__ must never raise: during interpreter shutdown module globals
|
|
317
|
+
# (incl. `logger`) may have been nulled, and an exception here only adds
|
|
318
|
+
# "Exception ignored" noise. Swallow defensively so auto-close stays silent.
|
|
319
|
+
try:
|
|
320
|
+
if not getattr(self, "_closed", True):
|
|
321
|
+
logger.warning("GPDCDomain %r was not explicitly closed; auto-closing in __del__", self._name)
|
|
322
|
+
self.close()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def reset_domain_config(name: str, config: "GPDCDomainConfig") -> None:
|
|
328
|
+
"""Delete the four lock-config fields from the domain's meta hash in Redis.
|
|
329
|
+
|
|
330
|
+
Intended to be called BEFORE constructing ``GPDCDomain`` when the user
|
|
331
|
+
wants to change lock config on an existing domain:
|
|
332
|
+
|
|
333
|
+
reset_domain_config("mydomain", old_config)
|
|
334
|
+
domain = GPDCDomain("mydomain", new_config)
|
|
335
|
+
|
|
336
|
+
No-op if the meta hash has no lock fields (HDEL on missing fields).
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
name: Domain name whose meta should be cleared.
|
|
340
|
+
config: ``GPDCDomainConfig`` carrying Redis connection parameters.
|
|
341
|
+
Only connection fields are read; lock fields on this config are
|
|
342
|
+
ignored.
|
|
343
|
+
"""
|
|
344
|
+
pool = redis.ConnectionPool(
|
|
345
|
+
host=config.redis_host,
|
|
346
|
+
port=config.redis_port,
|
|
347
|
+
db=config.redis_db,
|
|
348
|
+
password=config.redis_password or None,
|
|
349
|
+
decode_responses=True,
|
|
350
|
+
)
|
|
351
|
+
r = redis.Redis(connection_pool=pool)
|
|
352
|
+
try:
|
|
353
|
+
meta_key = make_domain_meta_key(name)
|
|
354
|
+
r.hdel(
|
|
355
|
+
meta_key,
|
|
356
|
+
"default_lock_ttl_seconds",
|
|
357
|
+
"default_lock_acquire_timeout_seconds",
|
|
358
|
+
"gc_lock_ttl_seconds",
|
|
359
|
+
"gc_lock_wait_seconds",
|
|
360
|
+
)
|
|
361
|
+
finally:
|
|
362
|
+
r.close()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GPDCError(Exception):
|
|
5
|
+
"""Base exception for all gpdatacached errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ObjectGoneError(GPDCError):
|
|
9
|
+
def __init__(self, object_uri: str) -> None:
|
|
10
|
+
self.object_uri = object_uri
|
|
11
|
+
super().__init__(f"Object {object_uri!r} no longer exists in Redis")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CanonicalObjectReferencedError(GPDCError):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
canonical_name: str,
|
|
18
|
+
canonical_uri: str,
|
|
19
|
+
referrers: list[str],
|
|
20
|
+
) -> None:
|
|
21
|
+
self.canonical_name = canonical_name
|
|
22
|
+
self.canonical_uri = canonical_uri
|
|
23
|
+
self.referrers = referrers
|
|
24
|
+
names = ", ".join(repr(r) for r in referrers)
|
|
25
|
+
super().__init__(
|
|
26
|
+
f"Cannot delete canonical object {canonical_name!r} ({canonical_uri}) "
|
|
27
|
+
f"because it is still referenced by: [{names}]"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LifecyclePermissionError(GPDCError):
|
|
32
|
+
"""Raised when attempting to modify lifecycle of an object not owned by the caller."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, canonical_name: str, owner_name: str) -> None:
|
|
35
|
+
self.canonical_name = canonical_name
|
|
36
|
+
self.owner_name = owner_name
|
|
37
|
+
super().__init__(
|
|
38
|
+
f"Cannot modify lifecycle of {canonical_name!r}: "
|
|
39
|
+
f"owned by {owner_name!r}. "
|
|
40
|
+
f"Call take_ownership() first to gain lifecycle management rights."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CyclicReferenceError(GPDCError):
|
|
45
|
+
"""Raised when a cyclic container reference is detected during encoding."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, type_name: str) -> None:
|
|
48
|
+
self.type_name = type_name
|
|
49
|
+
super().__init__(
|
|
50
|
+
f"Cannot encode cyclic reference involving {type_name!r}: "
|
|
51
|
+
f"a container references itself directly or transitively. "
|
|
52
|
+
f"Break the cycle before storing, or store the shared container "
|
|
53
|
+
f"separately and reference it by name."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GPDCLockError(GPDCError):
|
|
58
|
+
"""Base class for all lock-acquisition/ownership errors."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LockAcquisitionTimeout(GPDCLockError):
|
|
62
|
+
"""Raised when ``obj.lock()`` cannot acquire the lock within ``timeout``."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, lock_key: str, timeout: float) -> None:
|
|
65
|
+
self.lock_key = lock_key
|
|
66
|
+
self.timeout = timeout
|
|
67
|
+
super().__init__(f"Acquiring lock {lock_key!r} timed out after {timeout}s")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LockOwnershipError(GPDCLockError):
|
|
71
|
+
"""Raised when releasing a lock whose Redis token no longer matches.
|
|
72
|
+
|
|
73
|
+
Internal: ``HeldLock.release()`` swallows this case (logs a warning) instead
|
|
74
|
+
of raising, because the caller has no recovery path. The exception class is
|
|
75
|
+
kept for callers that wrap release logic themselves.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class GPDCLockConfigInconsistentError(GPDCError):
|
|
80
|
+
"""Raised at GPDCDomain construction when the lock config in the explicit
|
|
81
|
+
``GPDCDomainConfig`` conflicts with values already stored in Redis domain
|
|
82
|
+
meta.
|
|
83
|
+
|
|
84
|
+
Note: extends ``GPDCError`` directly (not ``GPDCLockError``) because this
|
|
85
|
+
is a configuration error surfaced before any lock acquisition is attempted.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
domain: str,
|
|
91
|
+
conflicts: dict[str, tuple[str, str]],
|
|
92
|
+
legal_values: dict[str, str],
|
|
93
|
+
) -> None:
|
|
94
|
+
self.domain = domain
|
|
95
|
+
self.conflicts = conflicts
|
|
96
|
+
self.legal_values = legal_values
|
|
97
|
+
|
|
98
|
+
lines = [f"Lock config for domain {domain!r} inconsistent with Redis:"]
|
|
99
|
+
for field, (redis_val, config_val) in conflicts.items():
|
|
100
|
+
lines.append(f" conflict: {field}: redis={redis_val!r}, your config={config_val!r}")
|
|
101
|
+
lines.append("Legal values currently in Redis domain meta:")
|
|
102
|
+
if legal_values:
|
|
103
|
+
for field, val in legal_values.items():
|
|
104
|
+
lines.append(f" {field} = {val}")
|
|
105
|
+
else:
|
|
106
|
+
lines.append(" (none)")
|
|
107
|
+
lines.append(
|
|
108
|
+
"To change these values, call "
|
|
109
|
+
"gpdatacached.reset_domain_config(name, config) "
|
|
110
|
+
"before constructing GPDCDomain."
|
|
111
|
+
)
|
|
112
|
+
super().__init__("\n".join(lines))
|
|
File without changes
|