cachu 0.2.4__py3-none-any.whl → 0.2.5__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.
- cachu/__init__.py +4 -5
- cachu/backends/__init__.py +38 -21
- cachu/backends/memory.py +137 -135
- cachu/backends/redis.py +86 -65
- cachu/backends/sqlite.py +163 -123
- cachu/config.py +6 -0
- cachu/decorator.py +257 -275
- cachu/mutex.py +247 -0
- cachu/operations.py +27 -28
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/METADATA +1 -2
- cachu-0.2.5.dist-info/RECORD +15 -0
- cachu/async_decorator.py +0 -262
- cachu/async_operations.py +0 -178
- cachu/backends/async_base.py +0 -50
- cachu/backends/async_memory.py +0 -111
- cachu/backends/async_redis.py +0 -141
- cachu/backends/async_sqlite.py +0 -256
- cachu/backends/file.py +0 -10
- cachu-0.2.4.dist-info/RECORD +0 -21
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/WHEEL +0 -0
- {cachu-0.2.4.dist-info → cachu-0.2.5.dist-info}/top_level.txt +0 -0
cachu/backends/sqlite.py
CHANGED
|
@@ -8,7 +8,8 @@ import time
|
|
|
8
8
|
from collections.abc import AsyncIterator, Iterator
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from ..mutex import AsyncCacheMutex, AsyncioMutex, CacheMutex, ThreadingMutex
|
|
12
|
+
from . import NO_VALUE, Backend
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
import aiosqlite
|
|
@@ -28,18 +29,36 @@ def _get_aiosqlite_module() -> Any:
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class SqliteBackend(Backend):
|
|
31
|
-
"""SQLite file-based cache backend.
|
|
32
|
+
"""Unified SQLite file-based cache backend with both sync and async interfaces.
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
35
|
def __init__(self, filepath: str) -> None:
|
|
35
36
|
self._filepath = filepath
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
37
|
+
self._sync_lock = threading.RLock()
|
|
38
|
+
self._async_lock: asyncio.Lock | None = None
|
|
39
|
+
self._async_write_lock: asyncio.Lock | None = None
|
|
40
|
+
self._async_connection: aiosqlite.Connection | None = None
|
|
41
|
+
self._async_initialized = False
|
|
42
|
+
self._init_sync_db()
|
|
43
|
+
|
|
44
|
+
def _get_async_lock(self) -> asyncio.Lock:
|
|
45
|
+
"""Lazy-create async init lock (must be called from async context).
|
|
46
|
+
"""
|
|
47
|
+
if self._async_lock is None:
|
|
48
|
+
self._async_lock = asyncio.Lock()
|
|
49
|
+
return self._async_lock
|
|
50
|
+
|
|
51
|
+
def _get_async_write_lock(self) -> asyncio.Lock:
|
|
52
|
+
"""Lazy-create async write lock (must be called from async context).
|
|
53
|
+
"""
|
|
54
|
+
if self._async_write_lock is None:
|
|
55
|
+
self._async_write_lock = asyncio.Lock()
|
|
56
|
+
return self._async_write_lock
|
|
38
57
|
|
|
39
|
-
def
|
|
40
|
-
"""Initialize database schema.
|
|
58
|
+
def _init_sync_db(self) -> None:
|
|
59
|
+
"""Initialize sync database schema.
|
|
41
60
|
"""
|
|
42
|
-
with self.
|
|
61
|
+
with self._sync_lock:
|
|
43
62
|
conn = sqlite3.connect(self._filepath)
|
|
44
63
|
try:
|
|
45
64
|
conn.execute("""
|
|
@@ -58,16 +77,65 @@ class SqliteBackend(Backend):
|
|
|
58
77
|
finally:
|
|
59
78
|
conn.close()
|
|
60
79
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
80
|
+
async def _ensure_async_initialized(self) -> 'aiosqlite.Connection':
|
|
81
|
+
"""Ensure async database is initialized and return connection.
|
|
82
|
+
"""
|
|
83
|
+
async with self._get_async_lock():
|
|
84
|
+
if self._async_connection is None:
|
|
85
|
+
aiosqlite = _get_aiosqlite_module()
|
|
86
|
+
self._async_connection = await aiosqlite.connect(self._filepath)
|
|
87
|
+
await self._async_connection.execute('PRAGMA journal_mode=WAL')
|
|
88
|
+
await self._async_connection.execute('PRAGMA busy_timeout=5000')
|
|
89
|
+
|
|
90
|
+
if not self._async_initialized:
|
|
91
|
+
await self._async_connection.execute("""
|
|
92
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
93
|
+
key TEXT PRIMARY KEY,
|
|
94
|
+
value BLOB NOT NULL,
|
|
95
|
+
created_at REAL NOT NULL,
|
|
96
|
+
expires_at REAL NOT NULL
|
|
97
|
+
)
|
|
98
|
+
""")
|
|
99
|
+
await self._async_connection.execute("""
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_cache_expires
|
|
101
|
+
ON cache(expires_at)
|
|
102
|
+
""")
|
|
103
|
+
await self._async_connection.commit()
|
|
104
|
+
self._async_initialized = True
|
|
105
|
+
|
|
106
|
+
return self._async_connection
|
|
107
|
+
|
|
108
|
+
def _get_sync_connection(self) -> sqlite3.Connection:
|
|
109
|
+
"""Get a sync database connection.
|
|
63
110
|
"""
|
|
64
111
|
return sqlite3.connect(self._filepath)
|
|
65
112
|
|
|
113
|
+
def _fnmatch_to_glob(self, pattern: str) -> str:
|
|
114
|
+
"""Convert fnmatch pattern to SQLite GLOB pattern.
|
|
115
|
+
"""
|
|
116
|
+
return pattern
|
|
117
|
+
|
|
118
|
+
def _schedule_async_delete(self, key: str) -> None:
|
|
119
|
+
"""Schedule a background deletion task (fire-and-forget).
|
|
120
|
+
"""
|
|
121
|
+
async def _delete() -> None:
|
|
122
|
+
try:
|
|
123
|
+
async with self._get_async_write_lock():
|
|
124
|
+
conn = await self._ensure_async_initialized()
|
|
125
|
+
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
126
|
+
await conn.commit()
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
asyncio.create_task(_delete())
|
|
131
|
+
|
|
132
|
+
# ===== Sync interface =====
|
|
133
|
+
|
|
66
134
|
def get(self, key: str) -> Any:
|
|
67
135
|
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
68
136
|
"""
|
|
69
|
-
with self.
|
|
70
|
-
conn = self.
|
|
137
|
+
with self._sync_lock:
|
|
138
|
+
conn = self._get_sync_connection()
|
|
71
139
|
try:
|
|
72
140
|
cursor = conn.execute(
|
|
73
141
|
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
@@ -93,8 +161,8 @@ class SqliteBackend(Backend):
|
|
|
93
161
|
def get_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
94
162
|
"""Get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
95
163
|
"""
|
|
96
|
-
with self.
|
|
97
|
-
conn = self.
|
|
164
|
+
with self._sync_lock:
|
|
165
|
+
conn = self._get_sync_connection()
|
|
98
166
|
try:
|
|
99
167
|
cursor = conn.execute(
|
|
100
168
|
'SELECT value, created_at, expires_at FROM cache WHERE key = ?',
|
|
@@ -123,8 +191,8 @@ class SqliteBackend(Backend):
|
|
|
123
191
|
now = time.time()
|
|
124
192
|
value_blob = pickle.dumps(value)
|
|
125
193
|
|
|
126
|
-
with self.
|
|
127
|
-
conn = self.
|
|
194
|
+
with self._sync_lock:
|
|
195
|
+
conn = self._get_sync_connection()
|
|
128
196
|
try:
|
|
129
197
|
conn.execute(
|
|
130
198
|
"""INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
|
|
@@ -138,8 +206,8 @@ class SqliteBackend(Backend):
|
|
|
138
206
|
def delete(self, key: str) -> None:
|
|
139
207
|
"""Delete value by key.
|
|
140
208
|
"""
|
|
141
|
-
with self.
|
|
142
|
-
conn = self.
|
|
209
|
+
with self._sync_lock:
|
|
210
|
+
conn = self._get_sync_connection()
|
|
143
211
|
try:
|
|
144
212
|
conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
145
213
|
conn.commit()
|
|
@@ -151,8 +219,8 @@ class SqliteBackend(Backend):
|
|
|
151
219
|
def clear(self, pattern: str | None = None) -> int:
|
|
152
220
|
"""Clear entries matching pattern. Returns count of cleared entries.
|
|
153
221
|
"""
|
|
154
|
-
with self.
|
|
155
|
-
conn = self.
|
|
222
|
+
with self._sync_lock:
|
|
223
|
+
conn = self._get_sync_connection()
|
|
156
224
|
try:
|
|
157
225
|
if pattern is None:
|
|
158
226
|
cursor = conn.execute('SELECT COUNT(*) FROM cache')
|
|
@@ -180,8 +248,8 @@ class SqliteBackend(Backend):
|
|
|
180
248
|
"""
|
|
181
249
|
now = time.time()
|
|
182
250
|
|
|
183
|
-
with self.
|
|
184
|
-
conn = self.
|
|
251
|
+
with self._sync_lock:
|
|
252
|
+
conn = self._get_sync_connection()
|
|
185
253
|
try:
|
|
186
254
|
if pattern is None:
|
|
187
255
|
cursor = conn.execute(
|
|
@@ -206,8 +274,8 @@ class SqliteBackend(Backend):
|
|
|
206
274
|
"""
|
|
207
275
|
now = time.time()
|
|
208
276
|
|
|
209
|
-
with self.
|
|
210
|
-
conn = self.
|
|
277
|
+
with self._sync_lock:
|
|
278
|
+
conn = self._get_sync_connection()
|
|
211
279
|
try:
|
|
212
280
|
if pattern is None:
|
|
213
281
|
cursor = conn.execute(
|
|
@@ -227,21 +295,18 @@ class SqliteBackend(Backend):
|
|
|
227
295
|
finally:
|
|
228
296
|
conn.close()
|
|
229
297
|
|
|
230
|
-
def
|
|
231
|
-
"""
|
|
232
|
-
|
|
233
|
-
fnmatch uses * and ? which are the same as SQLite GLOB.
|
|
234
|
-
The main difference is character classes [...] which we don't use.
|
|
298
|
+
def get_mutex(self, key: str) -> CacheMutex:
|
|
299
|
+
"""Get a mutex for dogpile prevention on the given key.
|
|
235
300
|
"""
|
|
236
|
-
return
|
|
301
|
+
return ThreadingMutex(f'sqlite:{self._filepath}:{key}')
|
|
237
302
|
|
|
238
303
|
def cleanup_expired(self) -> int:
|
|
239
304
|
"""Remove expired entries. Returns count of removed entries.
|
|
240
305
|
"""
|
|
241
306
|
now = time.time()
|
|
242
307
|
|
|
243
|
-
with self.
|
|
244
|
-
conn = self.
|
|
308
|
+
with self._sync_lock:
|
|
309
|
+
conn = self._get_sync_connection()
|
|
245
310
|
try:
|
|
246
311
|
cursor = conn.execute(
|
|
247
312
|
'SELECT COUNT(*) FROM cache WHERE expires_at <= ?',
|
|
@@ -254,65 +319,13 @@ class SqliteBackend(Backend):
|
|
|
254
319
|
finally:
|
|
255
320
|
conn.close()
|
|
256
321
|
|
|
322
|
+
# ===== Async interface =====
|
|
257
323
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"""
|
|
261
|
-
|
|
262
|
-
def __init__(self, filepath: str) -> None:
|
|
263
|
-
self._filepath = filepath
|
|
264
|
-
self._connection: aiosqlite.Connection | None = None
|
|
265
|
-
self._init_lock = asyncio.Lock()
|
|
266
|
-
self._write_lock = asyncio.Lock()
|
|
267
|
-
self._initialized = False
|
|
268
|
-
|
|
269
|
-
async def _ensure_initialized(self) -> 'aiosqlite.Connection':
|
|
270
|
-
"""Ensure database is initialized and return connection.
|
|
271
|
-
"""
|
|
272
|
-
async with self._init_lock:
|
|
273
|
-
if self._connection is None:
|
|
274
|
-
aiosqlite = _get_aiosqlite_module()
|
|
275
|
-
self._connection = await aiosqlite.connect(self._filepath)
|
|
276
|
-
await self._connection.execute('PRAGMA journal_mode=WAL')
|
|
277
|
-
await self._connection.execute('PRAGMA busy_timeout=5000')
|
|
278
|
-
|
|
279
|
-
if not self._initialized:
|
|
280
|
-
await self._connection.execute("""
|
|
281
|
-
CREATE TABLE IF NOT EXISTS cache (
|
|
282
|
-
key TEXT PRIMARY KEY,
|
|
283
|
-
value BLOB NOT NULL,
|
|
284
|
-
created_at REAL NOT NULL,
|
|
285
|
-
expires_at REAL NOT NULL
|
|
286
|
-
)
|
|
287
|
-
""")
|
|
288
|
-
await self._connection.execute("""
|
|
289
|
-
CREATE INDEX IF NOT EXISTS idx_cache_expires
|
|
290
|
-
ON cache(expires_at)
|
|
291
|
-
""")
|
|
292
|
-
await self._connection.commit()
|
|
293
|
-
self._initialized = True
|
|
294
|
-
|
|
295
|
-
return self._connection
|
|
296
|
-
|
|
297
|
-
def _schedule_delete(self, key: str) -> None:
|
|
298
|
-
"""Schedule a background deletion task (fire-and-forget).
|
|
299
|
-
"""
|
|
300
|
-
async def _delete() -> None:
|
|
301
|
-
try:
|
|
302
|
-
async with self._write_lock:
|
|
303
|
-
conn = await self._ensure_initialized()
|
|
304
|
-
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
305
|
-
await conn.commit()
|
|
306
|
-
except Exception:
|
|
307
|
-
pass
|
|
308
|
-
|
|
309
|
-
asyncio.create_task(_delete())
|
|
310
|
-
|
|
311
|
-
async def get(self, key: str) -> Any:
|
|
312
|
-
"""Get value by key. Returns NO_VALUE if not found or expired.
|
|
324
|
+
async def aget(self, key: str) -> Any:
|
|
325
|
+
"""Async get value by key. Returns NO_VALUE if not found or expired.
|
|
313
326
|
"""
|
|
314
327
|
try:
|
|
315
|
-
conn = await self.
|
|
328
|
+
conn = await self._ensure_async_initialized()
|
|
316
329
|
cursor = await conn.execute(
|
|
317
330
|
'SELECT value, expires_at FROM cache WHERE key = ?',
|
|
318
331
|
(key,),
|
|
@@ -324,18 +337,18 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
324
337
|
|
|
325
338
|
value_blob, expires_at = row
|
|
326
339
|
if time.time() > expires_at:
|
|
327
|
-
self.
|
|
340
|
+
self._schedule_async_delete(key)
|
|
328
341
|
return NO_VALUE
|
|
329
342
|
|
|
330
343
|
return pickle.loads(value_blob)
|
|
331
344
|
except Exception:
|
|
332
345
|
return NO_VALUE
|
|
333
346
|
|
|
334
|
-
async def
|
|
335
|
-
"""
|
|
347
|
+
async def aget_with_metadata(self, key: str) -> tuple[Any, float | None]:
|
|
348
|
+
"""Async get value and creation timestamp. Returns (NO_VALUE, None) if not found.
|
|
336
349
|
"""
|
|
337
350
|
try:
|
|
338
|
-
conn = await self.
|
|
351
|
+
conn = await self._ensure_async_initialized()
|
|
339
352
|
cursor = await conn.execute(
|
|
340
353
|
'SELECT value, created_at, expires_at FROM cache WHERE key = ?',
|
|
341
354
|
(key,),
|
|
@@ -347,21 +360,21 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
347
360
|
|
|
348
361
|
value_blob, created_at, expires_at = row
|
|
349
362
|
if time.time() > expires_at:
|
|
350
|
-
self.
|
|
363
|
+
self._schedule_async_delete(key)
|
|
351
364
|
return NO_VALUE, None
|
|
352
365
|
|
|
353
366
|
return pickle.loads(value_blob), created_at
|
|
354
367
|
except Exception:
|
|
355
368
|
return NO_VALUE, None
|
|
356
369
|
|
|
357
|
-
async def
|
|
358
|
-
"""
|
|
370
|
+
async def aset(self, key: str, value: Any, ttl: int) -> None:
|
|
371
|
+
"""Async set value with TTL in seconds.
|
|
359
372
|
"""
|
|
360
373
|
now = time.time()
|
|
361
374
|
value_blob = pickle.dumps(value)
|
|
362
375
|
|
|
363
|
-
async with self.
|
|
364
|
-
conn = await self.
|
|
376
|
+
async with self._get_async_write_lock():
|
|
377
|
+
conn = await self._ensure_async_initialized()
|
|
365
378
|
await conn.execute(
|
|
366
379
|
"""INSERT OR REPLACE INTO cache (key, value, created_at, expires_at)
|
|
367
380
|
VALUES (?, ?, ?, ?)""",
|
|
@@ -369,23 +382,23 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
369
382
|
)
|
|
370
383
|
await conn.commit()
|
|
371
384
|
|
|
372
|
-
async def
|
|
373
|
-
"""
|
|
385
|
+
async def adelete(self, key: str) -> None:
|
|
386
|
+
"""Async delete value by key.
|
|
374
387
|
"""
|
|
375
|
-
async with self.
|
|
388
|
+
async with self._get_async_write_lock():
|
|
376
389
|
try:
|
|
377
|
-
conn = await self.
|
|
390
|
+
conn = await self._ensure_async_initialized()
|
|
378
391
|
await conn.execute('DELETE FROM cache WHERE key = ?', (key,))
|
|
379
392
|
await conn.commit()
|
|
380
393
|
except Exception:
|
|
381
394
|
pass
|
|
382
395
|
|
|
383
|
-
async def
|
|
384
|
-
"""
|
|
396
|
+
async def aclear(self, pattern: str | None = None) -> int:
|
|
397
|
+
"""Async clear entries matching pattern. Returns count of cleared entries.
|
|
385
398
|
"""
|
|
386
|
-
async with self.
|
|
399
|
+
async with self._get_async_write_lock():
|
|
387
400
|
try:
|
|
388
|
-
conn = await self.
|
|
401
|
+
conn = await self._ensure_async_initialized()
|
|
389
402
|
if pattern is None:
|
|
390
403
|
cursor = await conn.execute('SELECT COUNT(*) FROM cache')
|
|
391
404
|
row = await cursor.fetchone()
|
|
@@ -407,11 +420,11 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
407
420
|
except Exception:
|
|
408
421
|
return 0
|
|
409
422
|
|
|
410
|
-
async def
|
|
411
|
-
"""
|
|
423
|
+
async def akeys(self, pattern: str | None = None) -> AsyncIterator[str]:
|
|
424
|
+
"""Async iterate over keys matching pattern.
|
|
412
425
|
"""
|
|
413
426
|
now = time.time()
|
|
414
|
-
conn = await self.
|
|
427
|
+
conn = await self._ensure_async_initialized()
|
|
415
428
|
|
|
416
429
|
if pattern is None:
|
|
417
430
|
cursor = await conn.execute(
|
|
@@ -430,13 +443,13 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
430
443
|
for key in all_keys:
|
|
431
444
|
yield key
|
|
432
445
|
|
|
433
|
-
async def
|
|
434
|
-
"""
|
|
446
|
+
async def acount(self, pattern: str | None = None) -> int:
|
|
447
|
+
"""Async count keys matching pattern.
|
|
435
448
|
"""
|
|
436
449
|
now = time.time()
|
|
437
450
|
|
|
438
451
|
try:
|
|
439
|
-
conn = await self.
|
|
452
|
+
conn = await self._ensure_async_initialized()
|
|
440
453
|
if pattern is None:
|
|
441
454
|
cursor = await conn.execute(
|
|
442
455
|
'SELECT COUNT(*) FROM cache WHERE expires_at > ?',
|
|
@@ -454,18 +467,18 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
454
467
|
except Exception:
|
|
455
468
|
return 0
|
|
456
469
|
|
|
457
|
-
def
|
|
458
|
-
"""
|
|
470
|
+
def get_async_mutex(self, key: str) -> AsyncCacheMutex:
|
|
471
|
+
"""Get an async mutex for dogpile prevention on the given key.
|
|
459
472
|
"""
|
|
460
|
-
return
|
|
473
|
+
return AsyncioMutex(f'sqlite:{self._filepath}:{key}')
|
|
461
474
|
|
|
462
|
-
async def
|
|
463
|
-
"""
|
|
475
|
+
async def acleanup_expired(self) -> int:
|
|
476
|
+
"""Async remove expired entries. Returns count of removed entries.
|
|
464
477
|
"""
|
|
465
478
|
now = time.time()
|
|
466
479
|
|
|
467
|
-
async with self.
|
|
468
|
-
conn = await self.
|
|
480
|
+
async with self._get_async_write_lock():
|
|
481
|
+
conn = await self._ensure_async_initialized()
|
|
469
482
|
cursor = await conn.execute(
|
|
470
483
|
'SELECT COUNT(*) FROM cache WHERE expires_at <= ?',
|
|
471
484
|
(now,),
|
|
@@ -476,10 +489,37 @@ class AsyncSqliteBackend(AsyncBackend):
|
|
|
476
489
|
await conn.commit()
|
|
477
490
|
return count
|
|
478
491
|
|
|
479
|
-
|
|
480
|
-
|
|
492
|
+
# ===== Lifecycle =====
|
|
493
|
+
|
|
494
|
+
def _close_async_connection_sync(self) -> None:
|
|
495
|
+
"""Forcefully close async connection from sync context.
|
|
496
|
+
|
|
497
|
+
This accesses aiosqlite internals as there's no public sync close API.
|
|
498
|
+
"""
|
|
499
|
+
if self._async_connection is None:
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
conn = self._async_connection
|
|
503
|
+
self._async_connection = None
|
|
504
|
+
self._async_initialized = False
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
conn._running = False
|
|
508
|
+
if hasattr(conn, '_connection') and conn._connection:
|
|
509
|
+
conn._connection.close()
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
def close(self) -> None:
|
|
514
|
+
"""Close all backend resources from sync context.
|
|
515
|
+
"""
|
|
516
|
+
self._close_async_connection_sync()
|
|
517
|
+
|
|
518
|
+
async def aclose(self) -> None:
|
|
519
|
+
"""Close all backend resources from async context.
|
|
481
520
|
"""
|
|
482
|
-
if self.
|
|
483
|
-
|
|
484
|
-
self.
|
|
485
|
-
self.
|
|
521
|
+
if self._async_connection is not None:
|
|
522
|
+
conn = self._async_connection
|
|
523
|
+
self._async_connection = None
|
|
524
|
+
self._async_initialized = False
|
|
525
|
+
await conn.close()
|
cachu/config.py
CHANGED
|
@@ -58,6 +58,7 @@ class CacheConfig:
|
|
|
58
58
|
key_prefix: str = ''
|
|
59
59
|
file_dir: str = '/tmp'
|
|
60
60
|
redis_url: str = 'redis://localhost:6379/0'
|
|
61
|
+
lock_timeout: float = 10.0
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
class ConfigRegistry:
|
|
@@ -79,6 +80,7 @@ class ConfigRegistry:
|
|
|
79
80
|
key_prefix: str | None = None,
|
|
80
81
|
file_dir: str | None = None,
|
|
81
82
|
redis_url: str | None = None,
|
|
83
|
+
lock_timeout: float | None = None,
|
|
82
84
|
) -> CacheConfig:
|
|
83
85
|
"""Configure cache for a specific package.
|
|
84
86
|
"""
|
|
@@ -90,6 +92,7 @@ class ConfigRegistry:
|
|
|
90
92
|
'key_prefix': key_prefix,
|
|
91
93
|
'file_dir': str(file_dir) if file_dir else None,
|
|
92
94
|
'redis_url': redis_url,
|
|
95
|
+
'lock_timeout': lock_timeout,
|
|
93
96
|
}
|
|
94
97
|
updates = {k: v for k, v in updates.items() if v is not None}
|
|
95
98
|
|
|
@@ -152,6 +155,7 @@ def configure(
|
|
|
152
155
|
key_prefix: str | None = None,
|
|
153
156
|
file_dir: str | None = None,
|
|
154
157
|
redis_url: str | None = None,
|
|
158
|
+
lock_timeout: float | None = None,
|
|
155
159
|
) -> CacheConfig:
|
|
156
160
|
"""Configure cache settings for the caller's package.
|
|
157
161
|
|
|
@@ -163,12 +167,14 @@ def configure(
|
|
|
163
167
|
key_prefix: Prefix for all cache keys (for versioning/debugging)
|
|
164
168
|
file_dir: Directory for file-based caches
|
|
165
169
|
redis_url: Redis connection URL (e.g., 'redis://localhost:6379/0')
|
|
170
|
+
lock_timeout: Timeout for distributed locks in seconds (default: 10.0)
|
|
166
171
|
"""
|
|
167
172
|
return _registry.configure(
|
|
168
173
|
backend=backend,
|
|
169
174
|
key_prefix=key_prefix,
|
|
170
175
|
file_dir=str(file_dir) if file_dir else None,
|
|
171
176
|
redis_url=redis_url,
|
|
177
|
+
lock_timeout=lock_timeout,
|
|
172
178
|
)
|
|
173
179
|
|
|
174
180
|
|