nedb-engine 2.6.1__tar.gz → 2.6.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.
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/PKG-INFO +1 -1
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/pyproject.toml +1 -1
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/__init__.py +3 -3
- nedb_engine-2.6.3/python/nedb/_native.abi3.so +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/concurrent.py +16 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/engine.py +102 -0
- nedb_engine-2.6.3/python/nedb/nedbd-v2 +0 -0
- nedb_engine-2.6.3/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- nedb_engine-2.6.3/python/nedb/nedbd-v2-darwin-x64 +0 -0
- nedb_engine-2.6.3/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/server.py +66 -21
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/store.py +10 -0
- nedb_engine-2.6.1/python/nedb/_native.abi3.so +0 -0
- nedb_engine-2.6.1/python/nedb/nedbd-v2 +0 -0
- nedb_engine-2.6.1/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- nedb_engine-2.6.1/python/nedb/nedbd-v2-darwin-x64 +0 -0
- nedb_engine-2.6.1/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/.gitignore +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/LICENSE +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/README.md +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/client/node/README.md +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/client/python/README.md +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/examples/mini-chain/README.md +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/autoindex.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/cascade.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/crypto.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/index.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/log.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/merkle.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/mongo.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/proof.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/query.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/relations.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/resp2.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/snapshot.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/sql.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/python/nedb/wrap_redis.py +0 -0
- {nedb_engine-2.6.1 → nedb_engine-2.6.3}/rust/nedb-v2/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.3
|
|
4
4
|
Summary: NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd).
|
|
5
5
|
Project-URL: Homepage, https://github.com/aiassistsecure/nedb
|
|
6
6
|
Project-URL: Repository, https://github.com/aiassistsecure/nedb
|
|
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "nedb-engine"
|
|
13
|
-
version = "2.6.
|
|
13
|
+
version = "2.6.3"
|
|
14
14
|
description = "NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd)."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.8"
|
|
@@ -16,7 +16,7 @@ as ``nedb._native`` (``nedb.__has_native__`` reports whether it loaded).
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from .engine import NEDB
|
|
19
|
+
from .engine import NEDB, PreconditionFailed
|
|
20
20
|
from .log import Op, OpLog, ReplayError
|
|
21
21
|
from .query import Query, parse_nql
|
|
22
22
|
from .snapshot import save_snapshot, load_snapshot
|
|
@@ -79,7 +79,7 @@ except ImportError: # pure-Python install (sdist / unsupported platform)
|
|
|
79
79
|
del _types, _sys, _NativeStub, _native_stub
|
|
80
80
|
|
|
81
81
|
__all__ = [
|
|
82
|
-
"NEDB", "OpLog", "Op", "ReplayError", "Query", "parse_nql",
|
|
82
|
+
"NEDB", "PreconditionFailed", "OpLog", "Op", "ReplayError", "Query", "parse_nql",
|
|
83
83
|
"save_snapshot", "load_snapshot",
|
|
84
84
|
"sql_exec", "sql_to_nql", "SQLError", "SQLUnsupportedError",
|
|
85
85
|
"RedisCompat", "RedisError", "RedisUnsupportedError",
|
|
@@ -89,4 +89,4 @@ __all__ = [
|
|
|
89
89
|
"verify_proof", "fold_head",
|
|
90
90
|
"_native", "__has_native__",
|
|
91
91
|
]
|
|
92
|
-
__version__ = "2.6.
|
|
92
|
+
__version__ = "2.6.3"
|
|
Binary file
|
|
@@ -90,6 +90,13 @@ class Sequencer:
|
|
|
90
90
|
def put(self, coll: str, id: str, doc: dict, **kw: Any) -> Any:
|
|
91
91
|
return self._submit("put", coll, id, doc, **kw)
|
|
92
92
|
|
|
93
|
+
def tx(self, ops: list, **kw: Any) -> Any:
|
|
94
|
+
"""Atomic multi-op transaction (see NEDB.tx). Submitted as ONE
|
|
95
|
+
intent, so the committer validates preconditions and applies all ops
|
|
96
|
+
with nothing interleaving — the single-writer thread IS the
|
|
97
|
+
serialization point, exactly as Redis's single thread was for Lua."""
|
|
98
|
+
return self._submit("tx", ops, **kw)
|
|
99
|
+
|
|
93
100
|
def delete(self, coll: str, id: str, **kw: Any) -> Any:
|
|
94
101
|
return self._submit("delete", coll, id, **kw)
|
|
95
102
|
|
|
@@ -108,6 +115,11 @@ class Sequencer:
|
|
|
108
115
|
def checkpoint(self) -> Any:
|
|
109
116
|
return self._submit("checkpoint")
|
|
110
117
|
|
|
118
|
+
def sweep(self) -> Any:
|
|
119
|
+
"""TTL sweep as ONE committer intent — sweep appends delete ops, so it
|
|
120
|
+
must ride the single writer, never race it from another thread."""
|
|
121
|
+
return self._submit("sweep")
|
|
122
|
+
|
|
111
123
|
# ── read API: concurrent, snapshot-isolated at committed_seq ───────────────
|
|
112
124
|
def query(self, nql: str) -> List[dict]:
|
|
113
125
|
plan = parse_nql(nql)
|
|
@@ -207,6 +219,10 @@ class Sequencer:
|
|
|
207
219
|
return db.put_file(*intent.args, **intent.kwargs)
|
|
208
220
|
if k == "checkpoint":
|
|
209
221
|
return db.checkpoint()
|
|
222
|
+
if k == "tx":
|
|
223
|
+
return db.tx(*intent.args, **intent.kwargs)
|
|
224
|
+
if k == "sweep":
|
|
225
|
+
return db.sweep()
|
|
210
226
|
raise ValueError(f"unknown write kind: {k}")
|
|
211
227
|
|
|
212
228
|
def close(self) -> None:
|
|
@@ -24,6 +24,23 @@ from .relations import Relations
|
|
|
24
24
|
from .store import MVCCStore
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class PreconditionFailed(Exception):
|
|
28
|
+
"""An `if_seq` compare-and-set precondition did not hold; the transaction
|
|
29
|
+
was rejected as a unit and NOTHING was applied. `.failures` lists every
|
|
30
|
+
failed check as {index, coll, id, expected, actual}."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, failures: List[dict]):
|
|
33
|
+
self.failures = failures
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"{len(failures)} precondition(s) failed: "
|
|
36
|
+
+ "; ".join(
|
|
37
|
+
f"[{f['index']}] {f['coll']}:{f['id']} expected seq "
|
|
38
|
+
f"{f['expected']}, actual {f['actual']}"
|
|
39
|
+
for f in failures
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
def apply_op(store: MVCCStore, relations: Relations, indexes: Indexes, op: Op,
|
|
28
45
|
cause_map: Optional[Dict[int, List[int]]] = None) -> None:
|
|
29
46
|
"""Deterministically fold one op into materialized state."""
|
|
@@ -484,6 +501,91 @@ class NEDB:
|
|
|
484
501
|
return self._check_ttl(coll, id, doc)
|
|
485
502
|
return doc # time-travel reads never trigger lazy expiry
|
|
486
503
|
|
|
504
|
+
def last_seq(self, coll: str, id: str) -> Optional[int]:
|
|
505
|
+
"""Version anchor for compare-and-set: the seq of the most recent op
|
|
506
|
+
(put OR delete) that touched this document, or None if never written."""
|
|
507
|
+
return self.store.last_seq(f"{coll}:{id}")
|
|
508
|
+
|
|
509
|
+
def tx(self, ops: List[dict], default_client: str = "local") -> dict:
|
|
510
|
+
"""Atomic multi-op transaction with optional per-op CAS preconditions.
|
|
511
|
+
|
|
512
|
+
Each op is a dict:
|
|
513
|
+
{"op": "put", "coll", "id", "doc", "if_seq"?, "idem"?, "client"?,
|
|
514
|
+
"caused_by"?, "ttl_s"?}
|
|
515
|
+
{"op": "del", "coll", "id", "if_seq"?, "idem"?, "client"?}
|
|
516
|
+
{"op": "link", "frm", "rel", "to", "client"?}
|
|
517
|
+
|
|
518
|
+
`if_seq` semantics (the Lua-atomics replacement contract):
|
|
519
|
+
if_seq = N -> the doc's last_seq must be exactly N (the caller
|
|
520
|
+
read version N and nothing has touched it since)
|
|
521
|
+
if_seq = -1 -> the doc must NOT currently exist (create-once)
|
|
522
|
+
omitted/None -> unconditional
|
|
523
|
+
|
|
524
|
+
Two phases: ALL preconditions are validated against current state
|
|
525
|
+
first; if any fails, PreconditionFailed is raised and NOTHING is
|
|
526
|
+
applied. Only then are the ops applied in order (each becoming a
|
|
527
|
+
normal chained, hash-sealed op-log entry).
|
|
528
|
+
|
|
529
|
+
Concurrency contract: this method performs no locking of its own.
|
|
530
|
+
It is atomic when executed by a single writer — which is exactly how
|
|
531
|
+
the daemon runs it (one intent on the Sequencer's committer thread,
|
|
532
|
+
the same single-writer property that made Redis Lua scripts atomic).
|
|
533
|
+
Embedded multi-threaded callers must route through Sequencer.tx().
|
|
534
|
+
|
|
535
|
+
Durability: the Sequencer group-commits, so a tx submitted as one
|
|
536
|
+
intent shares a single fsync boundary — a crash before it loses the
|
|
537
|
+
whole tx together, never a prefix of it.
|
|
538
|
+
|
|
539
|
+
Precondition reads use the raw store (no TTL side effects): an
|
|
540
|
+
expired-but-unswept doc still counts as existing for if_seq == -1;
|
|
541
|
+
call sweep() first where create-once interacts with TTLs.
|
|
542
|
+
"""
|
|
543
|
+
failures: List[dict] = []
|
|
544
|
+
for i, op in enumerate(ops):
|
|
545
|
+
want = op.get("if_seq")
|
|
546
|
+
if want is None:
|
|
547
|
+
continue
|
|
548
|
+
kind = str(op.get("op", "put")).lower()
|
|
549
|
+
if kind not in ("put", "del", "delete"):
|
|
550
|
+
continue # links carry no per-doc version
|
|
551
|
+
coll, rid = str(op["coll"]), str(op["id"])
|
|
552
|
+
cur = self.store.last_seq(f"{coll}:{rid}")
|
|
553
|
+
want = int(want)
|
|
554
|
+
if want == -1:
|
|
555
|
+
if self.store.get(f"{coll}:{rid}") is not None:
|
|
556
|
+
failures.append({"index": i, "coll": coll, "id": rid,
|
|
557
|
+
"expected": -1, "actual": cur})
|
|
558
|
+
elif cur != want:
|
|
559
|
+
failures.append({"index": i, "coll": coll, "id": rid,
|
|
560
|
+
"expected": want, "actual": cur})
|
|
561
|
+
if failures:
|
|
562
|
+
raise PreconditionFailed(failures)
|
|
563
|
+
|
|
564
|
+
results: List[dict] = []
|
|
565
|
+
for op in ops:
|
|
566
|
+
kind = str(op.get("op", "put")).lower()
|
|
567
|
+
client = str(op.get("client") or default_client)
|
|
568
|
+
if kind == "put":
|
|
569
|
+
stored = self.put(str(op["coll"]), str(op["id"]),
|
|
570
|
+
dict(op.get("doc") or {}),
|
|
571
|
+
client=client,
|
|
572
|
+
idem=op.get("idem"),
|
|
573
|
+
ttl_s=op.get("ttl_s"),
|
|
574
|
+
caused_by=op.get("caused_by"))
|
|
575
|
+
results.append({"op": "put", "id": op["id"], "seq": self.seq,
|
|
576
|
+
"doc": stored})
|
|
577
|
+
elif kind in ("del", "delete"):
|
|
578
|
+
self.delete(str(op["coll"]), str(op["id"]),
|
|
579
|
+
client=client, idem=op.get("idem"))
|
|
580
|
+
results.append({"op": "del", "id": op["id"], "seq": self.seq})
|
|
581
|
+
elif kind == "link":
|
|
582
|
+
self.link(str(op["frm"]), str(op["rel"]), str(op["to"]),
|
|
583
|
+
client=client)
|
|
584
|
+
results.append({"op": "link", "seq": self.seq})
|
|
585
|
+
else:
|
|
586
|
+
results.append({"op": kind, "error": "unknown op"})
|
|
587
|
+
return {"results": results, "count": len(results), "seq": self.seq}
|
|
588
|
+
|
|
487
589
|
def expire(self, coll: str, id: str, ttl_s: float) -> bool:
|
|
488
590
|
"""Set or update the TTL on an existing document. Returns False if not found."""
|
|
489
591
|
doc = self.store.get(f"{coll}:{id}")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -12,6 +12,7 @@ Config (env):
|
|
|
12
12
|
NEDBD_PORT bind port (default 7070)
|
|
13
13
|
NEDBD_DATA data root directory (default ./nedb-data)
|
|
14
14
|
NEDBD_TOKEN bearer token (optional; if set, every /v1 route requires it)
|
|
15
|
+
NEDBD_SWEEP_S TTL sweep interval s (default 30; 0 disables the sweeper)
|
|
15
16
|
|
|
16
17
|
Run:
|
|
17
18
|
nedbd # console script (pip install nedb-engine)
|
|
@@ -24,7 +25,7 @@ HTTP API (all JSON):
|
|
|
24
25
|
GET /v1/databases/<name>
|
|
25
26
|
DELETE /v1/databases/<name>
|
|
26
27
|
POST /v1/databases/<name>/query {nql}
|
|
27
|
-
POST /v1/databases/<name>/put {coll, id, doc, client?, nonce?, idem?}
|
|
28
|
+
POST /v1/databases/<name>/put {coll, id, doc, client?, nonce?, idem?, ttl_s?}
|
|
28
29
|
POST /v1/databases/<name>/index {coll, field, kind}
|
|
29
30
|
POST /v1/databases/<name>/link {frm, rel, to}
|
|
30
31
|
DELETE /v1/databases/<name>/rows/<coll>/<id>
|
|
@@ -35,7 +36,13 @@ HTTP API (all JSON):
|
|
|
35
36
|
GET /v1/databases/<name>/files/<filename>/root?version=N&tier=warm — Merkle root (anchorable)
|
|
36
37
|
POST /v1/databases/<name>/proof {hash} — Merkle inclusion proof (verifiable offline)
|
|
37
38
|
POST /v1/databases/<name>/checkpoint — on-demand checkpoint
|
|
38
|
-
|
|
39
|
+
POST /v1/databases/<name>/sweep — delete TTL-expired docs now (also runs
|
|
40
|
+
automatically every NEDBD_SWEEP_S, default 30)
|
|
41
|
+
POST /v1/databases/<name>/batch — ATOMIC multi-op tx: {ops: [{op, coll, id,
|
|
42
|
+
doc?, if_seq?, idem?, client?}], client?}
|
|
43
|
+
all-or-nothing; if_seq CAS preconditions
|
|
44
|
+
(N = exact version, -1 = must-not-exist);
|
|
45
|
+
409 {error: "precondition_failed", failures}
|
|
39
46
|
"""
|
|
40
47
|
from __future__ import annotations
|
|
41
48
|
|
|
@@ -52,6 +59,7 @@ from . import __version__
|
|
|
52
59
|
from .engine import NEDB
|
|
53
60
|
from .concurrent import Sequencer
|
|
54
61
|
from .log import ReplayError
|
|
62
|
+
from .engine import PreconditionFailed
|
|
55
63
|
|
|
56
64
|
NAME_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}")
|
|
57
65
|
|
|
@@ -322,7 +330,7 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
322
330
|
if not coll or rid is None or not isinstance(doc, dict):
|
|
323
331
|
raise HttpError(400, "coll, id, and doc are required")
|
|
324
332
|
_scalar = ("client", "nonce", "idem", "evidence", "confidence",
|
|
325
|
-
"valid_from", "valid_to")
|
|
333
|
+
"valid_from", "valid_to", "ttl_s")
|
|
326
334
|
kw = {k: b[k] for k in _scalar if b.get(k) is not None}
|
|
327
335
|
# caused_by may live at the top level of the request body
|
|
328
336
|
# OR inside doc (natural for clients embedding it in the document).
|
|
@@ -364,6 +372,10 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
364
372
|
head = db.checkpoint()
|
|
365
373
|
self._send(200, {"ok": True, "head": head, "seq": db.seq})
|
|
366
374
|
return
|
|
375
|
+
if method == "POST" and action == "sweep":
|
|
376
|
+
swept = db.sweep()
|
|
377
|
+
self._send(200, {"ok": True, "swept": swept, "seq": db.seq})
|
|
378
|
+
return
|
|
367
379
|
if method == "GET" and action == "log":
|
|
368
380
|
limit = int(query.get("limit", ["50"])[0])
|
|
369
381
|
ops = [o.to_dict() for o in db.log.ops[-limit:]][::-1]
|
|
@@ -525,30 +537,43 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
525
537
|
"size": len(data), "tier": tier})
|
|
526
538
|
return
|
|
527
539
|
|
|
528
|
-
# POST /v1/databases/<name>/batch —
|
|
529
|
-
# Body: {ops: [{op:"put"|"del"|"link", coll, id, doc?, frm?, rel?,
|
|
540
|
+
# POST /v1/databases/<name>/batch — ATOMIC multi-op transaction.
|
|
541
|
+
# Body: {ops: [{op:"put"|"del"|"link", coll, id, doc?, frm?, rel?,
|
|
542
|
+
# to?, if_seq?, idem?, client?, caused_by?, ttl_s?}, ...],
|
|
543
|
+
# client?: default client for ops that don't set one}
|
|
544
|
+
#
|
|
545
|
+
# All-or-nothing: every `if_seq` precondition is validated against
|
|
546
|
+
# current state FIRST (under the Sequencer's single committer
|
|
547
|
+
# thread — nothing can interleave); if any fails, HTTP 409 with
|
|
548
|
+
# the structured failure list and NOTHING is applied.
|
|
549
|
+
#
|
|
550
|
+
# `if_seq` contract (the Lua-atomics replacement):
|
|
551
|
+
# if_seq = N doc's last_seq must be exactly N (CAS)
|
|
552
|
+
# if_seq = -1 doc must not exist (create-once)
|
|
553
|
+
# omitted unconditional
|
|
554
|
+
#
|
|
555
|
+
# Per-op `idem` and `client` are forwarded to the op log (idem
|
|
556
|
+
# keys dedupe engine-side; client attributes provenance).
|
|
530
557
|
if method == "POST" and len(parts) == 4 and parts[:2] == ["v1", "databases"] and parts[3] == "batch":
|
|
531
558
|
db = manager.require(parts[2])
|
|
532
559
|
b = self._body()
|
|
533
560
|
ops_list = b.get("ops") or []
|
|
534
561
|
if not isinstance(ops_list, list) or not ops_list:
|
|
535
562
|
raise HttpError(400, "ops array is required")
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
self._send(200, {"results": results, "count": len(results),
|
|
551
|
-
"seq": db.seq, "head": db.head})
|
|
563
|
+
default_client = str(b.get("client") or "http")
|
|
564
|
+
try:
|
|
565
|
+
out = db.tx(ops_list, default_client=default_client)
|
|
566
|
+
except PreconditionFailed as e:
|
|
567
|
+
self._send(409, {"error": "precondition_failed",
|
|
568
|
+
"failures": e.failures,
|
|
569
|
+
"seq": db.seq})
|
|
570
|
+
return
|
|
571
|
+
except ReplayError as e:
|
|
572
|
+
raise HttpError(409, str(e))
|
|
573
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
574
|
+
raise HttpError(400, f"bad op: {e}")
|
|
575
|
+
self._send(200, {"results": out["results"], "count": out["count"],
|
|
576
|
+
"atomic": True, "seq": db.seq, "head": db.head})
|
|
552
577
|
return
|
|
553
578
|
|
|
554
579
|
raise HttpError(404, "no such route")
|
|
@@ -895,6 +920,26 @@ def main() -> None:
|
|
|
895
920
|
token = args.token
|
|
896
921
|
resp2_port = args.resp2_port
|
|
897
922
|
manager = Manager(data)
|
|
923
|
+
|
|
924
|
+
# TTL sweeper: engine expiry is lazy (as_of=None gets) but daemon reads are
|
|
925
|
+
# snapshot-pinned, so expired docs would linger forever without an active
|
|
926
|
+
# sweep. Runs every NEDBD_SWEEP_S seconds (default 30; 0 disables) through
|
|
927
|
+
# each Sequencer's committer intent — single-writer discipline preserved.
|
|
928
|
+
sweep_s = float(os.environ.get("NEDBD_SWEEP_S", "30"))
|
|
929
|
+
if sweep_s > 0:
|
|
930
|
+
def _sweeper():
|
|
931
|
+
import time as _t
|
|
932
|
+
while True:
|
|
933
|
+
_t.sleep(sweep_s)
|
|
934
|
+
for _name in list(manager._open.keys()):
|
|
935
|
+
try:
|
|
936
|
+
n = manager._open[_name].sweep()
|
|
937
|
+
if n:
|
|
938
|
+
_log(f" [nedbd] ttl sweep [{_name}]: {n} expired", level=1)
|
|
939
|
+
except Exception as _e: # noqa: BLE001
|
|
940
|
+
_log(f" [nedbd] ttl sweep [{_name}] error: {_e}", level=1)
|
|
941
|
+
threading.Thread(target=_sweeper, name="nedbd-ttl-sweeper", daemon=True).start()
|
|
942
|
+
|
|
898
943
|
httpd = ThreadingHTTPServer((host, port), make_handler(manager, token))
|
|
899
944
|
auth = "on" if token else "off"
|
|
900
945
|
BANNER = f"""\
|
|
@@ -40,6 +40,16 @@ class MVCCStore:
|
|
|
40
40
|
_, val = chain[i]
|
|
41
41
|
return None if val is TOMB else val
|
|
42
42
|
|
|
43
|
+
def last_seq(self, key: str) -> Optional[int]:
|
|
44
|
+
"""Seq of the most recent op that touched this key (including a
|
|
45
|
+
delete's tombstone), or None if the key has never been written.
|
|
46
|
+
|
|
47
|
+
This is the version anchor for compare-and-set (`if_seq`): a CAS
|
|
48
|
+
taken against version N correctly fails after ANY later write or
|
|
49
|
+
delete, because that op advanced the key's last_seq past N."""
|
|
50
|
+
seqs = self._seqs.get(key)
|
|
51
|
+
return seqs[-1] if seqs else None
|
|
52
|
+
|
|
43
53
|
def keys(self, prefix: str = "", as_of: Optional[int] = None) -> List[str]:
|
|
44
54
|
# Lock-free read: take an atomic snapshot of the key set, then resolve
|
|
45
55
|
# versions by `as_of`. Under the concurrent Sequencer a single committer
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|