beaver-db 0.24.5__tar.gz → 0.25.2__tar.gz
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.
Potentially problematic release.
This version of beaver-db might be problematic. Click here for more details.
- {beaver_db-0.24.5 → beaver_db-0.25.2}/PKG-INFO +1 -1
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/__init__.py +1 -1
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/blobs.py +42 -45
- beaver_db-0.25.2/beaver/cache.py +275 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/core.py +87 -99
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/dicts.py +31 -28
- beaver_db-0.25.2/beaver/lists.py +342 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/logs.py +13 -13
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/manager.py +7 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/queues.py +57 -47
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/types.py +1 -1
- beaver_db-0.25.2/dockerfile +58 -0
- {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/16-add-clear-method-for-all-data-structures.md +1 -1
- {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/17-replace-atomic-operations-with-custom-beaver-lock.md +1 -1
- {beaver_db-0.24.5/issues → beaver_db-0.25.2/issues/closed}/21-implement-dbcache-property-with-wal-invalidation-dummy-fallback-and-performance-stats.md +1 -1
- {beaver_db-0.24.5 → beaver_db-0.25.2}/makefile +9 -2
- {beaver_db-0.24.5 → beaver_db-0.25.2}/pyproject.toml +1 -1
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/conftest.py +17 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/integration/test_cache.py +4 -2
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/integration/test_realtime.py +29 -36
- beaver_db-0.24.5/beaver/cache.py +0 -146
- beaver_db-0.24.5/beaver/lists.py +0 -345
- beaver_db-0.24.5/dockerfile +0 -28
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.dockerignore +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.github/workflows/release.yaml +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.github/workflows/tests.yaml +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.gitignore +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.python-version +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/.vscode/settings.json +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/LICENSE +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/README.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/channels.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/__init__.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/blobs.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/channels.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/collections.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/dicts.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/lists.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/locks.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/logs.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/cli/queues.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/client.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/collections.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/locks.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/server.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/beaver/vectors.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/design.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/.gitignore +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/_quarto.yml +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/cover.png +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-architecture.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-concurrency.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-contributing.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/dev-search.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-collections.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-concurrency.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-deployment.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-dicts-blobs.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-lists-queues.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/guide-realtime.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/index.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/logo.png +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/docs/quickstart.qmd +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/async_pubsub.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/blobs.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/cache.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/fts.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/fuzzy.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/general_test.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/graph.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/kvstore.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/list.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/locks.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/logs.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/pqueue.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/producer_consumer.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/publisher.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/pubsub.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/rerank.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/stress_vectors.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/subscriber.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/textual_chat.css +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/textual_chat.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/type_hints.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/examples/vector.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/13-adopt-pydantic-deprecate-beavermodel-and-refactor-document-to-be-generic.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/15-enhance-cli-with-admin-commands-shell-piping-and-interactivity.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/18-enhanced-dump-and-load-for-etl.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/19-add-comprehensive-unit-integration-and-concurrency-test-suite.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/2-comprehensive-async-wrappers.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/10-expose-dblock-functionality-on-all-high-level-data-managers.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/12-add-dump-method-for-json-export-to-all-data-managers.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/14-deprecate-fire-based-cli-and-build-a-feature-rich-typer-and-rich-cli.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/issues/closed/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/logo.png +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/pytest.ini +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_blob_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_collection_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_dict_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_list_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_log_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/tests/unit/test_queue_manager.py +0 -0
- {beaver_db-0.24.5 → beaver_db-0.25.2}/uv.lock +0 -0
|
@@ -17,36 +17,15 @@ class Blob[M](NamedTuple):
|
|
|
17
17
|
class BlobManager[M: JsonSerializable](ManagerBase[M]):
|
|
18
18
|
"""A wrapper providing a Pythonic interface to a blob store in the database."""
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
Stores or replaces a blob in the store.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
key: The unique string identifier for the blob.
|
|
26
|
-
data: The binary data to store.
|
|
27
|
-
metadata: Optional JSON-serializable dictionary for metadata.
|
|
28
|
-
"""
|
|
29
|
-
if not isinstance(data, bytes):
|
|
30
|
-
raise TypeError("Blob data must be of type bytes.")
|
|
31
|
-
|
|
32
|
-
metadata_json = self._serialize(metadata) if metadata else None
|
|
33
|
-
|
|
34
|
-
with self.connection:
|
|
35
|
-
self.connection.execute(
|
|
36
|
-
"INSERT OR REPLACE INTO beaver_blobs (store_name, key, data, metadata) VALUES (?, ?, ?, ?)",
|
|
37
|
-
(self._name, key, data, metadata_json),
|
|
38
|
-
)
|
|
39
|
-
|
|
20
|
+
@synced
|
|
40
21
|
def get(self, key: str) -> Optional[Blob[M]]:
|
|
41
|
-
"""
|
|
42
|
-
|
|
22
|
+
"""Retrieves a blob from the store."""
|
|
23
|
+
# --- 1. Check cache first ---
|
|
24
|
+
cached_blob = self.cache.get(key)
|
|
25
|
+
if cached_blob is not None:
|
|
26
|
+
return cached_blob # Cache HIT
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
key: The unique string identifier for the blob.
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
A Blob object containing the data and metadata, or None if the key is not found.
|
|
49
|
-
"""
|
|
28
|
+
# --- 2. Cache MISS ---
|
|
50
29
|
cursor = self.connection.cursor()
|
|
51
30
|
cursor.execute(
|
|
52
31
|
"SELECT data, metadata FROM beaver_blobs WHERE store_name = ? AND key = ?",
|
|
@@ -61,23 +40,43 @@ class BlobManager[M: JsonSerializable](ManagerBase[M]):
|
|
|
61
40
|
data, metadata_json = result
|
|
62
41
|
metadata = self._deserialize(metadata_json) if metadata_json else None
|
|
63
42
|
|
|
64
|
-
|
|
43
|
+
# --- 3. Create object and populate cache ---
|
|
44
|
+
blob_obj = Blob(key=key, data=data, metadata=metadata)
|
|
45
|
+
self.cache.set(key, blob_obj)
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
return blob_obj
|
|
48
|
+
|
|
49
|
+
@synced
|
|
50
|
+
def put(self, key: str, data: bytes, metadata: Optional[M] = None):
|
|
51
|
+
"""Stores or replaces a blob in the store."""
|
|
52
|
+
if not isinstance(data, bytes):
|
|
53
|
+
raise TypeError("Blob data must be of type bytes.")
|
|
54
|
+
|
|
55
|
+
metadata_json = self._serialize(metadata) if metadata else None
|
|
69
56
|
|
|
70
|
-
Raises:
|
|
71
|
-
KeyError: If the key does not exist in the store.
|
|
72
|
-
"""
|
|
73
57
|
with self.connection:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
(self._name, key),
|
|
58
|
+
self.connection.execute(
|
|
59
|
+
"INSERT OR REPLACE INTO beaver_blobs (store_name, key, data, metadata) VALUES (?, ?, ?, ?)",
|
|
60
|
+
(self._name, key, data, metadata_json),
|
|
78
61
|
)
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
|
|
63
|
+
# Write-through to cache
|
|
64
|
+
blob_obj = Blob(key=key, data=data, metadata=metadata)
|
|
65
|
+
self.cache.set(key, blob_obj)
|
|
66
|
+
|
|
67
|
+
@synced
|
|
68
|
+
def delete(self, key: str):
|
|
69
|
+
"""Deletes a blob from the store."""
|
|
70
|
+
cursor = self.connection.cursor()
|
|
71
|
+
cursor.execute(
|
|
72
|
+
"DELETE FROM beaver_blobs WHERE store_name = ? AND key = ?",
|
|
73
|
+
(self._name, key),
|
|
74
|
+
)
|
|
75
|
+
if cursor.rowcount == 0:
|
|
76
|
+
raise KeyError(f"Key '{key}' not found in blob store '{self._name}'")
|
|
77
|
+
|
|
78
|
+
# evict from cache
|
|
79
|
+
self.cache.pop(key)
|
|
81
80
|
|
|
82
81
|
def __contains__(self, key: str) -> bool:
|
|
83
82
|
"""
|
|
@@ -183,10 +182,8 @@ class BlobManager[M: JsonSerializable](ManagerBase[M]):
|
|
|
183
182
|
|
|
184
183
|
@synced
|
|
185
184
|
def clear(self):
|
|
186
|
-
"""
|
|
187
|
-
Atomically removes all blobs from this store.
|
|
188
|
-
"""
|
|
185
|
+
"""Atomically removes all blobs from this store."""
|
|
189
186
|
self.connection.execute(
|
|
190
187
|
"DELETE FROM beaver_blobs WHERE store_name = ?",
|
|
191
188
|
(self._name,),
|
|
192
|
-
)
|
|
189
|
+
)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import functools
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional, Any, Protocol, NamedTuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheStats(NamedTuple):
|
|
9
|
+
"""Holds performance metrics for a cache instance."""
|
|
10
|
+
|
|
11
|
+
hits: int
|
|
12
|
+
misses: int
|
|
13
|
+
invalidations: int
|
|
14
|
+
sets: int
|
|
15
|
+
pops: int
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def reads(self) -> int:
|
|
19
|
+
return self.hits + self.misses
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def operations(self) -> int:
|
|
23
|
+
return self.hits + self.misses + self.sets + self.pops
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def hit_rate(self) -> float:
|
|
27
|
+
"""Returns the cache hit rate (0.0 to 1.0)."""
|
|
28
|
+
if self.reads == 0:
|
|
29
|
+
return 0.0
|
|
30
|
+
|
|
31
|
+
return self.hits / self.reads
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def invalidation_rate(self) -> float:
|
|
35
|
+
"""Returns the rate of invalidations per operation (0.0 to 1.0)."""
|
|
36
|
+
if self.reads == 0:
|
|
37
|
+
return 0.0
|
|
38
|
+
|
|
39
|
+
return self.invalidations / self.operations
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ICache(Protocol):
|
|
43
|
+
"""Defines the public interface for all cache objects."""
|
|
44
|
+
|
|
45
|
+
def get(self, key: str) -> Optional[Any]: ...
|
|
46
|
+
def set(self, key: Any, value: Any): ...
|
|
47
|
+
def pop(self, key: str): ...
|
|
48
|
+
def invalidate(self): ...
|
|
49
|
+
def stats(self) -> CacheStats: ...
|
|
50
|
+
def touch(self): ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DummyCache:
|
|
54
|
+
"""A cache object that does nothing. Used when caching is disabled."""
|
|
55
|
+
|
|
56
|
+
_stats = CacheStats(hits=0, misses=0, invalidations=0, sets=0, pops=0)
|
|
57
|
+
|
|
58
|
+
def get(self, key: str) -> Optional[Any]:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def set(self, key: str, value: Any):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def pop(self, key: str):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def invalidate(self):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def stats(self) -> CacheStats:
|
|
71
|
+
return self._stats
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def singleton(cls) -> ICache:
|
|
75
|
+
if not hasattr(cls, "__instance"):
|
|
76
|
+
cls.__instance = cls()
|
|
77
|
+
|
|
78
|
+
return cls.__instance
|
|
79
|
+
|
|
80
|
+
def touch(self):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
class LocalCache:
|
|
84
|
+
"""
|
|
85
|
+
A thread-local cache that invalidates based on a central,
|
|
86
|
+
database-backed version number, checking only once per interval.
|
|
87
|
+
"""
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
db,
|
|
91
|
+
cache_namespace: str,
|
|
92
|
+
check_interval: float
|
|
93
|
+
):
|
|
94
|
+
from .types import IDatabase
|
|
95
|
+
|
|
96
|
+
self._db: IDatabase = db
|
|
97
|
+
self._data: dict[str, Any] = {}
|
|
98
|
+
self._lock = threading.Lock()
|
|
99
|
+
|
|
100
|
+
self._version_key: str = cache_namespace # e.g., "list:tasks"
|
|
101
|
+
self._local_version: int = -1
|
|
102
|
+
self._last_check_time: float = 0.0
|
|
103
|
+
self._min_check_interval: float = check_interval
|
|
104
|
+
|
|
105
|
+
# Statistics
|
|
106
|
+
self._hits = 0
|
|
107
|
+
self._misses = 0
|
|
108
|
+
self._invalidations = 0
|
|
109
|
+
self._sets = 0
|
|
110
|
+
self._pops = 0
|
|
111
|
+
self._clears = 0
|
|
112
|
+
|
|
113
|
+
def _get_global_version(self) -> int:
|
|
114
|
+
"""Reads the 'source of truth' version from the DB."""
|
|
115
|
+
# This is a raw, direct DB call to avoid circular dependencies
|
|
116
|
+
cursor = self._db.connection.cursor()
|
|
117
|
+
cursor.execute(
|
|
118
|
+
"SELECT version FROM beaver_manager_versions WHERE namespace = ?",
|
|
119
|
+
(self._version_key,)
|
|
120
|
+
)
|
|
121
|
+
result = cursor.fetchone()
|
|
122
|
+
return int(result[0]) if result else 0
|
|
123
|
+
|
|
124
|
+
def _check_and_invalidate(self):
|
|
125
|
+
"""
|
|
126
|
+
Checks if the cache is stale, but only hits the DB
|
|
127
|
+
once per check_interval.
|
|
128
|
+
"""
|
|
129
|
+
now = time.time()
|
|
130
|
+
|
|
131
|
+
# --- 1. The Hot Path (Pure In-Memory Check) ---
|
|
132
|
+
if (now - self._last_check_time) < self._min_check_interval:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# --- 2. The "Coalesced" DB Check ---
|
|
136
|
+
with self._lock:
|
|
137
|
+
# Double-check inside lock in case another thread just ran this
|
|
138
|
+
if (time.time() - self._last_check_time) < self._min_check_interval:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
global_version = self._get_global_version()
|
|
142
|
+
self._last_check_time = time.time() # Reset timer
|
|
143
|
+
|
|
144
|
+
if global_version != self._local_version:
|
|
145
|
+
self._data.clear()
|
|
146
|
+
self._local_version = global_version
|
|
147
|
+
self._invalidations += 1
|
|
148
|
+
|
|
149
|
+
def get(self, key: str) -> Optional[Any]:
|
|
150
|
+
# This check is now extremely fast
|
|
151
|
+
self._check_and_invalidate()
|
|
152
|
+
|
|
153
|
+
with self._lock:
|
|
154
|
+
value = self._data.get(key)
|
|
155
|
+
|
|
156
|
+
if value is not None:
|
|
157
|
+
self._hits += 1
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
self._misses += 1
|
|
161
|
+
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def set(self, key: str, value: Any):
|
|
165
|
+
with self._lock:
|
|
166
|
+
self._data[key] = value
|
|
167
|
+
self._sets += 1
|
|
168
|
+
|
|
169
|
+
def pop(self, key: str):
|
|
170
|
+
with self._lock:
|
|
171
|
+
self._data.pop(key, None)
|
|
172
|
+
self._pops += 1
|
|
173
|
+
|
|
174
|
+
def invalidate(self):
|
|
175
|
+
with self._lock:
|
|
176
|
+
self._data.clear()
|
|
177
|
+
self._local_version = 0 # Must force re-check
|
|
178
|
+
self._invalidations += 1
|
|
179
|
+
self._last_check_time = 0.0
|
|
180
|
+
|
|
181
|
+
def touch(self):
|
|
182
|
+
"""
|
|
183
|
+
Atomically increments the cache version in the native SQL table
|
|
184
|
+
and syncs the cache's local version to avoid self-invalidation.
|
|
185
|
+
|
|
186
|
+
Only call this when you make a change that should invalidate
|
|
187
|
+
other caches of the same namespace in other processes,
|
|
188
|
+
but keep this cache valid.
|
|
189
|
+
"""
|
|
190
|
+
with self._lock:
|
|
191
|
+
new_version = 0
|
|
192
|
+
|
|
193
|
+
with self._db.connection:
|
|
194
|
+
# This is a single, atomic, native SQL operation.
|
|
195
|
+
cursor = self._db.connection.execute(
|
|
196
|
+
"""
|
|
197
|
+
INSERT INTO beaver_manager_versions (namespace, version)
|
|
198
|
+
VALUES (?, 1)
|
|
199
|
+
ON CONFLICT(namespace) DO UPDATE SET
|
|
200
|
+
version = version + 1
|
|
201
|
+
RETURNING version;
|
|
202
|
+
""",
|
|
203
|
+
(self._version_key,)
|
|
204
|
+
)
|
|
205
|
+
new_version = cursor.fetchone()[0]
|
|
206
|
+
|
|
207
|
+
# Keep the cache in sync to avoid self-invalidation
|
|
208
|
+
self._last_check_time = time.time()
|
|
209
|
+
self._local_version = new_version
|
|
210
|
+
|
|
211
|
+
def stats(self) -> CacheStats:
|
|
212
|
+
return CacheStats(
|
|
213
|
+
hits=self._hits,
|
|
214
|
+
misses=self._misses,
|
|
215
|
+
invalidations=self._invalidations,
|
|
216
|
+
sets=self._sets,
|
|
217
|
+
pops=self._pops,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def __repr__(self) -> str:
|
|
221
|
+
return f"<LocalCache namespace='{self._version_key}', version={self._local_version}>"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cached(key):
|
|
225
|
+
"""
|
|
226
|
+
Decorator for read methods.
|
|
227
|
+
- Generates a cache key using key on the arguments.
|
|
228
|
+
- If key is None, bypasses cache.
|
|
229
|
+
- If key is in cache, returns cached value.
|
|
230
|
+
- If key is not in cache, runs the decorated function,
|
|
231
|
+
stores the result, and returns it.
|
|
232
|
+
"""
|
|
233
|
+
from .manager import ManagerBase
|
|
234
|
+
|
|
235
|
+
def decorator(func):
|
|
236
|
+
@functools.wraps(func)
|
|
237
|
+
def wrapper(self: ManagerBase, *args, **kwargs):
|
|
238
|
+
cache = self.cache
|
|
239
|
+
cache_key = key(*args, **kwargs)
|
|
240
|
+
|
|
241
|
+
if cache_key is None:
|
|
242
|
+
return func(self, *args, **kwargs)
|
|
243
|
+
|
|
244
|
+
if not self.locked:
|
|
245
|
+
cached_value = cache.get(cache_key)
|
|
246
|
+
|
|
247
|
+
if cached_value is not None:
|
|
248
|
+
return cached_value # HIT
|
|
249
|
+
|
|
250
|
+
result = func(self, *args, **kwargs)
|
|
251
|
+
cache.set(cache_key, result)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
return wrapper
|
|
255
|
+
return decorator
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def invalidates_cache(func):
|
|
259
|
+
"""
|
|
260
|
+
Decorator for write methods that need to invalidate cache.
|
|
261
|
+
- Runs the decorated function.
|
|
262
|
+
- Clears the cache even if there is any exception.
|
|
263
|
+
"""
|
|
264
|
+
from .manager import ManagerBase
|
|
265
|
+
|
|
266
|
+
@functools.wraps(func)
|
|
267
|
+
def wrapper(self: "ManagerBase", *args, **kwargs):
|
|
268
|
+
try:
|
|
269
|
+
result = func(self, *args, **kwargs)
|
|
270
|
+
finally:
|
|
271
|
+
self.cache.invalidate()
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
return wrapper
|