Skip to content

Commit 0045529

Browse files
committed
Introduced configurable write durability for Mnemos databases
1 parent 9c89129 commit 0045529

3 files changed

Lines changed: 45 additions & 24 deletions

File tree

crates/mnemos-py/mnemos/client.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,11 @@ def __init__(
132132
path: str,
133133
dimension: t.Optional[int],
134134
embedder: t.Optional[Embedder] = None,
135+
sync: str = "strict",
135136
):
136137
self._embedder = embedder
137138
try:
138-
self._inner = _mnemos.Mnemos.open(path, dimension=dimension)
139+
self._inner = _mnemos.Mnemos.open(path, dimension=dimension, sync=sync)
139140
except Exception as e:
140141
raise MnemosError(str(e))
141142

@@ -146,6 +147,7 @@ def open(
146147
*,
147148
dimension: t.Optional[int] = None,
148149
embedder: t.Optional[Embedder] = None,
150+
sync: str = "strict",
149151
) -> "Mnemos":
150152
"""
151153
Open or create a Mnemos database.
@@ -158,6 +160,15 @@ def open(
158160
pre-computed embeddings.
159161
embedder: An :class:`~mnemos.Embedder` instance. The dimension is
160162
inferred from ``embedder.dimension`` automatically.
163+
sync: Write durability policy. One of:
164+
165+
* ``"strict"`` *(default)* — every write is fsynced immediately.
166+
Safe against crashes, slightly lower throughput.
167+
* ``"async"`` — writes are fsynced every 10 ms in the
168+
background. Higher throughput but may lose the last few
169+
writes on unclean shutdown.
170+
* ``"batch"`` — fsync every 64 writes or 50 ms, whichever
171+
comes first. A middle ground.
161172
162173
Raises:
163174
MnemosError: If neither or both of *dimension* and *embedder* are
@@ -173,7 +184,7 @@ def open(
173184
)
174185

175186
dim = embedder.dimension if embedder is not None else dimension
176-
return cls(path, dimension=dim, embedder=embedder)
187+
return cls(path, dimension=dim, embedder=embedder, sync=sync)
177188

178189
# ------------------------------------------------------------------
179190
# Internal helpers

crates/mnemos-py/src/lib.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,28 @@ impl PyMnemos {
167167
/// MnemosError: If the database cannot be opened or the dimension
168168
/// mismatches an existing database.
169169
#[staticmethod]
170-
#[pyo3(text_signature = "(path, *, dimension)")]
171-
fn open(path: &str, dimension: usize) -> PyResult<Self> {
170+
#[pyo3(
171+
text_signature = "(path, *, dimension, sync='strict')",
172+
signature = (path, *, dimension, sync="strict".to_string())
173+
)]
174+
fn open(path: &str, dimension: usize, sync: String) -> PyResult<Self> {
172175
if dimension == 0 {
173176
return Err(MnemosError::new_err("dimension must be > 0"));
174177
}
175178

179+
let sync_policy = match sync.to_lowercase().as_str() {
180+
"strict" => SyncPolicy::Strict,
181+
"async" => SyncPolicy::Async { interval_ms: 10 },
182+
"batch" => SyncPolicy::Batch { max_ops: 64, max_delay_ms: 50 },
183+
other => return Err(MnemosError::new_err(format!(
184+
"unknown sync policy '{}'. Valid values: 'strict', 'async', 'batch'",
185+
other,
186+
))),
187+
};
188+
176189
let config = facade::MnemosConfig {
177190
vector_dimension: dimension,
178-
sync_policy: SyncPolicy::Async {
179-
interval_ms: 10,
180-
},
191+
sync_policy,
181192
checkpoint_policy: CheckpointPolicy::Periodic {
182193
every_ops: 1000,
183194
every_ms: 30_000,

crates/mnemos-py/test_stress.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,39 @@ def clean_db_path(request):
1414
shutil.rmtree(db_path)
1515

1616
def test_replay_safety(clean_db_path):
17-
print("\n--- Test 1: Replay Safety (50k inserts) ---")
18-
19-
with Mnemos.open(clean_db_path, dimension=2) as db:
17+
print("\n--- Test 1: Replay Safety (5000 inserts) ---")
18+
19+
# Use strict sync so every write is fsynced — guarantees full replay on reopen.
20+
with Mnemos.open(clean_db_path, dimension=2, sync="strict") as db:
2021
start_time = time.time()
2122
for i in range(5000):
2223
db.remember(f"Entry {i}", embedding=[0.5, 0.5])
23-
24+
2425
print(f"Inserted 5,000 memories in {time.time() - start_time:.2f}s")
2526
assert len(db) == 5000
2627

27-
# Simulate closing / crash then reopen
28+
# Simulate closing then reopen — strict policy guarantees no data loss.
2829
print("Reopening...")
29-
with Mnemos.open(clean_db_path, dimension=2) as db2:
30-
# With Async sync policy a few trailing entries may not be flushed
31-
# before the context manager drops the handle — allow up to 20 missing.
32-
assert len(db2) >= 4980, f"Expected ~5000 entries after reopen, got {len(db2)}"
33-
30+
with Mnemos.open(clean_db_path, dimension=2, sync="strict") as db2:
31+
assert len(db2) == 5000, f"Expected 5000 entries after reopen, got {len(db2)}"
32+
3433
print("Test 1 PASS")
3534

3635
def test_compaction_integrity(clean_db_path):
3736
print("\n--- Test 3: WAL Compaction Integrity ---")
38-
39-
with Mnemos.open(clean_db_path, dimension=2) as db:
40-
# Insert 100 entries
37+
38+
# Use strict sync so compact sees all 100 entries in the WAL.
39+
with Mnemos.open(clean_db_path, dimension=2, sync="strict") as db:
4140
for _ in range(100):
4241
db.remember("Stress entry", embedding=[0.1, 0.9])
43-
42+
4443
assert len(db) == 100
4544

4645
print("Compacting...")
4746
db.compact()
4847

4948
print("Reopening...")
50-
with Mnemos.open(clean_db_path, dimension=2) as db2:
51-
assert len(db2) >= 90, f"Expected ~100 entries after compact + reopen, got {len(db2)}"
52-
49+
with Mnemos.open(clean_db_path, dimension=2, sync="strict") as db2:
50+
assert len(db2) == 100, f"Expected 100 entries after compact + reopen, got {len(db2)}"
51+
5352
print("Test 3 PASS")

0 commit comments

Comments
 (0)