beaver-db 0.19.2__tar.gz → 0.19.3__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.19.2 → beaver_db-0.19.3}/PKG-INFO +1 -1
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/__init__.py +1 -1
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/blobs.py +36 -1
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/collections.py +39 -3
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/dicts.py +36 -3
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/lists.py +37 -3
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/locks.py +20 -7
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/queues.py +38 -3
- beaver_db-0.19.3/issues/10-expose-dblock-functionality-on-all-high-level-data-managers.md +86 -0
- beaver_db-0.19.3/issues/closed/11-a-new-issue-for-gabriel.md +8 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/pyproject.toml +1 -1
- {beaver_db-0.19.2 → beaver_db-0.19.3}/.dockerignore +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/.gitignore +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/.python-version +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/LICENSE +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/README.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/channels.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/cli.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/core.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/logs.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/server.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/types.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/beaver/vectors.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/design.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/dockerfile +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/async_pubsub.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/blobs.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/cache.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/fts.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/fuzzy.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/general_test.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/graph.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/kvstore.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/list.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/locks.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/logs.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/pqueue.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/producer_consumer.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/publisher.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/pubsub.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/rerank.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/stress_vectors.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/subscriber.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/textual_chat.css +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/textual_chat.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/type_hints.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/examples/vector.py +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/2-comprehensive-async-wrappers.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/6-drop-in-replacement-for-beaver-rest-server-client.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/7-replace-faiss-with-simpler-linear-numpy-vectorial-search.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/9-type-safe-wrappers-based-on-pydantic-compatible-models.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/closed/1-refactor-vector-store-to-use-faiss.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/closed/5-add-dblock-for-inter-process-synchronization.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/issues/closed/8-first-class-synchronization-primitive.md +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/logo.png +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/makefile +0 -0
- {beaver_db-0.19.2 → beaver_db-0.19.3}/uv.lock +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
3
|
from typing import Any, Dict, Iterator, NamedTuple, Optional, Type, TypeVar
|
|
4
|
-
|
|
5
4
|
from .types import JsonSerializable, IDatabase
|
|
5
|
+
from .locks import LockManager
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Blob[M](NamedTuple):
|
|
@@ -16,10 +16,13 @@ class Blob[M](NamedTuple):
|
|
|
16
16
|
class BlobManager[M]:
|
|
17
17
|
"""A wrapper providing a Pythonic interface to a blob store in the database."""
|
|
18
18
|
|
|
19
|
+
# In beaver/blobs.py, inside class BlobManager[M]:
|
|
19
20
|
def __init__(self, name: str, db: IDatabase, model: Type[M] | None = None):
|
|
20
21
|
self._name = name
|
|
21
22
|
self._db = db
|
|
22
23
|
self._model = model
|
|
24
|
+
lock_name = f"__lock__blob__{name}"
|
|
25
|
+
self._lock = LockManager(db, lock_name)
|
|
23
26
|
|
|
24
27
|
def _serialize(self, value: M) -> str | None:
|
|
25
28
|
"""Serializes the given value to a JSON string."""
|
|
@@ -135,3 +138,35 @@ class BlobManager[M]:
|
|
|
135
138
|
|
|
136
139
|
def __repr__(self) -> str:
|
|
137
140
|
return f"BlobManager(name='{self._name}')"
|
|
141
|
+
|
|
142
|
+
def acquire(
|
|
143
|
+
self,
|
|
144
|
+
timeout: Optional[float] = None,
|
|
145
|
+
lock_ttl: Optional[float] = None,
|
|
146
|
+
poll_interval: Optional[float] = None,
|
|
147
|
+
) -> "BlobManager[M]":
|
|
148
|
+
"""
|
|
149
|
+
Acquires an inter-process lock on this blob store, blocking until acquired.
|
|
150
|
+
|
|
151
|
+
Parameters override the default settings of the underlying LockManager.
|
|
152
|
+
"""
|
|
153
|
+
self._lock.acquire(
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
lock_ttl=lock_ttl,
|
|
156
|
+
poll_interval=poll_interval
|
|
157
|
+
)
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def release(self):
|
|
161
|
+
"""
|
|
162
|
+
Releases the inter-process lock on this blob store.
|
|
163
|
+
"""
|
|
164
|
+
self._lock.release()
|
|
165
|
+
|
|
166
|
+
def __enter__(self) -> "BlobManager[M]":
|
|
167
|
+
"""Acquires the lock upon entering a 'with' statement."""
|
|
168
|
+
return self.acquire()
|
|
169
|
+
|
|
170
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
171
|
+
"""Releases the lock when exiting a 'with' statement."""
|
|
172
|
+
self.release()
|
|
@@ -3,15 +3,16 @@ import sqlite3
|
|
|
3
3
|
import threading
|
|
4
4
|
import uuid
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Any, Iterator, List, Literal, Tuple, Type, TypeVar
|
|
6
|
+
from typing import Any, Iterator, List, Literal, Tuple, Type, TypeVar, Optional
|
|
7
7
|
from .types import Model, stub, IDatabase
|
|
8
|
+
from .locks import LockManager
|
|
8
9
|
|
|
9
10
|
try:
|
|
10
11
|
import numpy as np
|
|
11
12
|
from .vectors import VectorIndex
|
|
12
13
|
except ImportError:
|
|
13
|
-
np = stub("This feature requires to install beaver-db[
|
|
14
|
-
VectorIndex = stub("This feature requires to install beaver-db[
|
|
14
|
+
np = stub("This feature requires to install beaver-db[vector]")()
|
|
15
|
+
VectorIndex = stub("This feature requires to install beaver-db[vector]")
|
|
15
16
|
|
|
16
17
|
# --- Fuzzy Search Helper Functions ---
|
|
17
18
|
|
|
@@ -135,6 +136,9 @@ class CollectionManager[D: Document]:
|
|
|
135
136
|
self._compaction_lock = threading.Lock()
|
|
136
137
|
self._compaction_thread: threading.Thread | None = None
|
|
137
138
|
|
|
139
|
+
lock_name = f"__lock__collection__{name}"
|
|
140
|
+
self._lock = LockManager(db, lock_name)
|
|
141
|
+
|
|
138
142
|
def _flatten_metadata(self, metadata: dict, prefix: str = "") -> dict[str, Any]:
|
|
139
143
|
"""Flattens a nested dictionary for indexing."""
|
|
140
144
|
flat_dict = {}
|
|
@@ -643,6 +647,38 @@ class CollectionManager[D: Document]:
|
|
|
643
647
|
cursor.close()
|
|
644
648
|
return count
|
|
645
649
|
|
|
650
|
+
def acquire(
|
|
651
|
+
self,
|
|
652
|
+
timeout: Optional[float] = None,
|
|
653
|
+
lock_ttl: Optional[float] = None,
|
|
654
|
+
poll_interval: Optional[float] = None,
|
|
655
|
+
) -> "CollectionManager[D]":
|
|
656
|
+
"""
|
|
657
|
+
Acquires an inter-process lock on this collection, blocking until acquired.
|
|
658
|
+
This guarantees exclusive access for multi-step atomic operations
|
|
659
|
+
(e.g., index + connect).
|
|
660
|
+
|
|
661
|
+
Parameters override the default settings of the underlying LockManager.
|
|
662
|
+
"""
|
|
663
|
+
self._lock.acquire(
|
|
664
|
+
timeout=timeout, lock_ttl=lock_ttl, poll_interval=poll_interval
|
|
665
|
+
)
|
|
666
|
+
return self
|
|
667
|
+
|
|
668
|
+
def release(self):
|
|
669
|
+
"""
|
|
670
|
+
Releases the inter-process lock on this collection.
|
|
671
|
+
"""
|
|
672
|
+
self._lock.release()
|
|
673
|
+
|
|
674
|
+
def __enter__(self) -> "CollectionManager[D]":
|
|
675
|
+
"""Acquires the lock upon entering a 'with' statement."""
|
|
676
|
+
return self.acquire()
|
|
677
|
+
|
|
678
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
679
|
+
"""Releases the lock when exiting a 'with' statement."""
|
|
680
|
+
self.release()
|
|
681
|
+
|
|
646
682
|
|
|
647
683
|
def rerank[D: Document](
|
|
648
684
|
*results: list[D], weights: list[float] | None = None, k: int = 60
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
3
|
import time
|
|
4
|
-
from typing import Any, Iterator, Tuple, Type
|
|
5
|
-
|
|
4
|
+
from typing import Any, Iterator, Tuple, Type, Optional
|
|
6
5
|
from .types import JsonSerializable, IDatabase
|
|
6
|
+
from .locks import LockManager
|
|
7
7
|
|
|
8
8
|
class DictManager[T]:
|
|
9
9
|
"""A wrapper providing a Pythonic interface to a dictionary in the database."""
|
|
@@ -12,6 +12,8 @@ class DictManager[T]:
|
|
|
12
12
|
self._name = name
|
|
13
13
|
self._db = db
|
|
14
14
|
self._model = model
|
|
15
|
+
lock_name = f"__lock__dict__{name}"
|
|
16
|
+
self._lock = LockManager(db, lock_name)
|
|
15
17
|
|
|
16
18
|
def _serialize(self, value: T) -> str:
|
|
17
19
|
"""Serializes the given value to a JSON string."""
|
|
@@ -147,4 +149,35 @@ class DictManager[T]:
|
|
|
147
149
|
cursor.close()
|
|
148
150
|
|
|
149
151
|
def __repr__(self) -> str:
|
|
150
|
-
return f"DictManager(name='{self._name}')"
|
|
152
|
+
return f"DictManager(name='{self._name}')"
|
|
153
|
+
|
|
154
|
+
def acquire(
|
|
155
|
+
self,
|
|
156
|
+
timeout: Optional[float] = None,
|
|
157
|
+
lock_ttl: Optional[float] = None,
|
|
158
|
+
poll_interval: Optional[float] = None,
|
|
159
|
+
) -> "DictManager[T]":
|
|
160
|
+
"""
|
|
161
|
+
Acquires an inter-process lock on this dictionary, blocking until acquired.
|
|
162
|
+
Parameters override the default settings of the underlying LockManager.
|
|
163
|
+
"""
|
|
164
|
+
self._lock.acquire(
|
|
165
|
+
timeout=timeout,
|
|
166
|
+
lock_ttl=lock_ttl,
|
|
167
|
+
poll_interval=poll_interval
|
|
168
|
+
)
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def release(self):
|
|
172
|
+
"""
|
|
173
|
+
Releases the inter-process lock on this dictionary.
|
|
174
|
+
"""
|
|
175
|
+
self._lock.release()
|
|
176
|
+
|
|
177
|
+
def __enter__(self) -> "DictManager[T]":
|
|
178
|
+
"""Acquires the lock upon entering a 'with' statement."""
|
|
179
|
+
return self.acquire()
|
|
180
|
+
|
|
181
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
182
|
+
"""Releases the lock when exiting a 'with' statement."""
|
|
183
|
+
self.release()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sqlite3
|
|
3
|
-
from typing import Any, Iterator, Type, Union
|
|
4
|
-
|
|
3
|
+
from typing import Any, Iterator, Type, Union, Optional
|
|
5
4
|
from .types import JsonSerializable, IDatabase
|
|
5
|
+
from .locks import LockManager
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ListManager[T]:
|
|
@@ -12,6 +12,8 @@ class ListManager[T]:
|
|
|
12
12
|
self._name = name
|
|
13
13
|
self._db = db
|
|
14
14
|
self._model = model
|
|
15
|
+
lock_name = f"__lock__list__{name}"
|
|
16
|
+
self._lock = LockManager(db, lock_name)
|
|
15
17
|
|
|
16
18
|
def _serialize(self, value: T) -> str:
|
|
17
19
|
"""Serializes the given value to a JSON string."""
|
|
@@ -261,4 +263,36 @@ class ListManager[T]:
|
|
|
261
263
|
cursor.execute(
|
|
262
264
|
"DELETE FROM beaver_lists WHERE rowid = ?", (rowid_to_delete,)
|
|
263
265
|
)
|
|
264
|
-
return self._deserialize(value_to_return)
|
|
266
|
+
return self._deserialize(value_to_return)
|
|
267
|
+
|
|
268
|
+
def acquire(
|
|
269
|
+
self,
|
|
270
|
+
timeout: Optional[float] = None,
|
|
271
|
+
lock_ttl: Optional[float] = None,
|
|
272
|
+
poll_interval: Optional[float] = None,
|
|
273
|
+
) -> "ListManager[T]":
|
|
274
|
+
"""
|
|
275
|
+
Acquires an inter-process lock on this list, blocking until acquired.
|
|
276
|
+
|
|
277
|
+
Parameters override the default settings of the underlying LockManager.
|
|
278
|
+
"""
|
|
279
|
+
self._lock.acquire(
|
|
280
|
+
timeout=timeout,
|
|
281
|
+
lock_ttl=lock_ttl,
|
|
282
|
+
poll_interval=poll_interval
|
|
283
|
+
)
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def release(self):
|
|
287
|
+
"""
|
|
288
|
+
Releases the inter-process lock on this list, allowing the next waiting process access.
|
|
289
|
+
"""
|
|
290
|
+
self._lock.release()
|
|
291
|
+
|
|
292
|
+
def __enter__(self) -> "ListManager[T]":
|
|
293
|
+
"""Acquires the lock upon entering a 'with' statement."""
|
|
294
|
+
return self.acquire()
|
|
295
|
+
|
|
296
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
297
|
+
"""Releases the lock when exiting a 'with' statement."""
|
|
298
|
+
self.release()
|
|
@@ -55,7 +55,11 @@ class LockManager:
|
|
|
55
55
|
self._waiter_id = f"pid:{os.getpid()}:id:{uuid.uuid4()}"
|
|
56
56
|
self._acquired = False # State to track if this instance holds the lock
|
|
57
57
|
|
|
58
|
-
def acquire(self
|
|
58
|
+
def acquire(self,
|
|
59
|
+
timeout: float|None = None,
|
|
60
|
+
lock_ttl: float |None = None,
|
|
61
|
+
poll_interval: float |None = None,
|
|
62
|
+
) -> "LockManager":
|
|
59
63
|
"""
|
|
60
64
|
Blocks until the lock is acquired or the timeout expires.
|
|
61
65
|
|
|
@@ -66,9 +70,18 @@ class LockManager:
|
|
|
66
70
|
# This instance already holds the lock
|
|
67
71
|
return self
|
|
68
72
|
|
|
73
|
+
if timeout is None:
|
|
74
|
+
timeout = self._timeout
|
|
75
|
+
|
|
76
|
+
if lock_ttl is None:
|
|
77
|
+
lock_ttl = self._lock_ttl
|
|
78
|
+
|
|
79
|
+
if poll_interval is None:
|
|
80
|
+
poll_interval = self._poll_interval
|
|
81
|
+
|
|
69
82
|
start_time = time.time()
|
|
70
83
|
requested_at = time.time()
|
|
71
|
-
expires_at = requested_at +
|
|
84
|
+
expires_at = requested_at + lock_ttl
|
|
72
85
|
|
|
73
86
|
conn = self._db.connection
|
|
74
87
|
|
|
@@ -113,19 +126,19 @@ class LockManager:
|
|
|
113
126
|
return self
|
|
114
127
|
|
|
115
128
|
# 5. Check for timeout
|
|
116
|
-
if
|
|
117
|
-
if (time.time() - start_time) >
|
|
129
|
+
if timeout is not None:
|
|
130
|
+
if (time.time() - start_time) > timeout:
|
|
118
131
|
# We timed out. Remove ourselves from the queue and raise.
|
|
119
132
|
self._release_from_queue()
|
|
120
133
|
raise TimeoutError(
|
|
121
|
-
f"Failed to acquire lock '{self._lock_name}' within {
|
|
134
|
+
f"Failed to acquire lock '{self._lock_name}' within {timeout}s."
|
|
122
135
|
)
|
|
123
136
|
|
|
124
137
|
# 6. Wait politely before polling again
|
|
125
138
|
# Add +/- 10% jitter to the poll interval to avoid thundering herd
|
|
126
|
-
jitter =
|
|
139
|
+
jitter = poll_interval * 0.1
|
|
127
140
|
sleep_time = random.uniform(
|
|
128
|
-
|
|
141
|
+
poll_interval - jitter, poll_interval + jitter
|
|
129
142
|
)
|
|
130
143
|
time.sleep(sleep_time)
|
|
131
144
|
|
|
@@ -2,10 +2,9 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
import sqlite3
|
|
4
4
|
import time
|
|
5
|
-
from typing import Any, Literal, NamedTuple, Type, overload
|
|
6
|
-
|
|
5
|
+
from typing import Any, Literal, NamedTuple, Type, overload, Optional
|
|
7
6
|
from .types import JsonSerializable, IDatabase
|
|
8
|
-
|
|
7
|
+
from .locks import LockManager
|
|
9
8
|
|
|
10
9
|
class QueueItem[T](NamedTuple):
|
|
11
10
|
"""A data class representing a single item retrieved from the queue."""
|
|
@@ -54,6 +53,8 @@ class QueueManager[T]:
|
|
|
54
53
|
self._name = name
|
|
55
54
|
self._db = db
|
|
56
55
|
self._model = model
|
|
56
|
+
lock_name = f"__lock__queue__{name}"
|
|
57
|
+
self._lock = LockManager(db, lock_name)
|
|
57
58
|
|
|
58
59
|
def _serialize(self, value: T) -> str:
|
|
59
60
|
"""Serializes the given value to a JSON string."""
|
|
@@ -184,3 +185,37 @@ class QueueManager[T]:
|
|
|
184
185
|
|
|
185
186
|
def __repr__(self) -> str:
|
|
186
187
|
return f"QueueManager(name='{self._name}')"
|
|
188
|
+
|
|
189
|
+
def acquire(
|
|
190
|
+
self,
|
|
191
|
+
timeout: Optional[float] = None,
|
|
192
|
+
lock_ttl: Optional[float] = None,
|
|
193
|
+
poll_interval: Optional[float] = None,
|
|
194
|
+
) -> "QueueManager[T]":
|
|
195
|
+
"""
|
|
196
|
+
Acquires an inter-process lock on this queue, blocking until acquired.
|
|
197
|
+
This ensures that a sequence of operations (e.g., batch-getting tasks)
|
|
198
|
+
is performed atomically without interruption from other processes.
|
|
199
|
+
|
|
200
|
+
Parameters override the default settings of the underlying LockManager.
|
|
201
|
+
"""
|
|
202
|
+
self._lock.acquire(
|
|
203
|
+
timeout=timeout,
|
|
204
|
+
lock_ttl=lock_ttl,
|
|
205
|
+
poll_interval=poll_interval
|
|
206
|
+
)
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def release(self):
|
|
210
|
+
"""
|
|
211
|
+
Releases the inter-process lock on this queue.
|
|
212
|
+
"""
|
|
213
|
+
self._lock.release()
|
|
214
|
+
|
|
215
|
+
def __enter__(self) -> "QueueManager[T]":
|
|
216
|
+
"""Acquires the lock upon entering a 'with' statement."""
|
|
217
|
+
return self.acquire()
|
|
218
|
+
|
|
219
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
220
|
+
"""Releases the lock when exiting a 'with' statement."""
|
|
221
|
+
self.release()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
number: 10
|
|
3
|
+
title: "Expose `db.lock()` functionality on all high-level data managers"
|
|
4
|
+
state: open
|
|
5
|
+
labels:
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
### 1. Concept
|
|
9
|
+
|
|
10
|
+
This feature proposes adding explicit, inter-process synchronization to all core state-mutating data managers: `DictManager`, `ListManager`, `CollectionManager`, **`QueueManager`**, and `BlobManager`.
|
|
11
|
+
|
|
12
|
+
This will be achieved by:
|
|
13
|
+
|
|
14
|
+
1. Making each manager class a **Python context manager** (implementing `__enter__` and `__exit__`).
|
|
15
|
+
2. Adding public `acquire()` and `release()` methods to each manager.
|
|
16
|
+
3. Internally, these methods will be thin wrappers around the existing `db.lock()` primitive, using a reserved naming convention (e.g., `__lock__dict__<name>`).
|
|
17
|
+
|
|
18
|
+
This will allow users to perform complex, multi-step operations on a single data structure as a process-safe, atomic unit.
|
|
19
|
+
|
|
20
|
+
### 2. Use Cases
|
|
21
|
+
|
|
22
|
+
* **Atomic Read-Modify-Write (Dicts/Blobs):** Guarantee that a process can safely read, modify, and write a value back without another process interfering.
|
|
23
|
+
* **Batch Task Processing (Queues):** Allow a worker to **atomically retrieve a batch of multiple tasks** from the queue (e.g., calling `q.get()` five times) without another worker jumping in and stealing tasks in the middle of the batch operation.
|
|
24
|
+
* **List Manipulation (Lists):** Safely perform multi-step modifications like checking list length, popping the last item, and prepending a new item without race conditions.
|
|
25
|
+
* **Consistent State for Collections (Collections):** A process could lock a collection to perform a series of related operations (e.g., `index`, `drop`, and `connect`) and ensure that other processes only see the final, consistent state.
|
|
26
|
+
|
|
27
|
+
### 3. Proposed API
|
|
28
|
+
|
|
29
|
+
The primary change would be enabling a `with` statement directly on the manager object.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
db = BeaverDB("my.db")
|
|
33
|
+
|
|
34
|
+
# Example for a Dictionary (Atomic Read-Modify-Write)
|
|
35
|
+
try:
|
|
36
|
+
with db.dict("config", timeout=5) as config:
|
|
37
|
+
count = config.get("counter", 0)
|
|
38
|
+
count += 1
|
|
39
|
+
config["counter"] = count
|
|
40
|
+
except TimeoutError:
|
|
41
|
+
print("Could not acquire lock for config.")
|
|
42
|
+
|
|
43
|
+
# Example for a Queue (Atomic Batch Processing)
|
|
44
|
+
try:
|
|
45
|
+
with db.queue("tasks", timeout=10) as q:
|
|
46
|
+
task1 = q.get()
|
|
47
|
+
task2 = q.get()
|
|
48
|
+
print(f"Retrieved two tasks atomically: {task1} and {task2}")
|
|
49
|
+
except IndexError:
|
|
50
|
+
print("Queue did not have two tasks.")
|
|
51
|
+
except TimeoutError:
|
|
52
|
+
print("Could not acquire lock for queue.")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 4. Implementation Design (High-Level)
|
|
56
|
+
|
|
57
|
+
**Manager `__init__` Modification:**
|
|
58
|
+
|
|
59
|
+
* The `__init__` method for all targeted managers (`DictManager`, `ListManager`, `CollectionManager`, **`QueueManager`**, and `BlobManager`) will be updated.
|
|
60
|
+
* Each manager will initialize and store an internal `LockManager` instance, which is provided by `db.lock()`.
|
|
61
|
+
* The lock name will use a standardized internal format based on the manager type and the manager's name, e.g., `f"__lock__queue__{self._name}"`.
|
|
62
|
+
|
|
63
|
+
**Lock Methods Implementation:**
|
|
64
|
+
|
|
65
|
+
* `acquire(self, timeout=None, ...)`: Calls `self._lock.acquire(timeout=timeout, ...)` internally.
|
|
66
|
+
* `release(self)`: Calls `self._lock.release()` internally.
|
|
67
|
+
* `__enter__(self)`: Calls `self.acquire()`.
|
|
68
|
+
* `__exit__(self, exc_type, exc_val, exc_tb)`: Calls `self.release()`.
|
|
69
|
+
|
|
70
|
+
**Targeted Manager Classes:**
|
|
71
|
+
|
|
72
|
+
* `DictManager` (`beaver/dicts.py`)
|
|
73
|
+
* `ListManager` (`beaver/lists.py`)
|
|
74
|
+
* `QueueManager` (`beaver/queues.py`)
|
|
75
|
+
* `CollectionManager` (`beaver/collections.py`)
|
|
76
|
+
* `BlobManager` (`beaver/blobs.py`)
|
|
77
|
+
|
|
78
|
+
**Exclusions:** `ChannelManager` and `LogManager` will **not** be modified, as their core write operations are already atomic, and imposing a lock would create an unnecessary performance bottleneck for their high-concurrency design.
|
|
79
|
+
|
|
80
|
+
### 5. Alignment with Philosophy
|
|
81
|
+
|
|
82
|
+
This feature aligns with `beaver-db`'s core principles:
|
|
83
|
+
|
|
84
|
+
* **Simplicity and Pythonic API:** Provides the most intuitive `with` statement syntax for concurrency control.
|
|
85
|
+
* **Developer Experience:** Solves complex multi-process problems (like batch race conditions) with a simple, high-level primitive.
|
|
86
|
+
* **Synchronous Core:** Builds entirely on the existing, dependency-free `db.lock()` primitive, maintaining the project's architectural integrity.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beaver_db-0.19.2 → beaver_db-0.19.3}/issues/6-drop-in-replacement-for-beaver-rest-server-client.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beaver_db-0.19.2 → beaver_db-0.19.3}/issues/closed/8-first-class-synchronization-primitive.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|