Skip to content
This repository was archived by the owner on Aug 24, 2026. It is now read-only.

feat: concurrent-writer driver with convergence verification (bm#1248) - #42

Closed
phernandez wants to merge 1 commit into
mainfrom
feat/concurrent-writer-driver
Closed

feat: concurrent-writer driver with convergence verification (bm#1248)#42
phernandez wants to merge 1 commit into
mainfrom
feat/concurrent-writer-driver

Conversation

@phernandez

Copy link
Copy Markdown
Member

Adds the concurrent-writer driver required by basicmachines-co/basic-memory#1248 (axes 1 and 4): a bm-bench run concurrent-write command that runs N independent bm mcp client sessions writing and editing notes in ONE shared Basic Memory project — the basic-memory#1214 field-report shape — and then verifies the project converged.

What it does

  • Workload: each writer creates --notes-per-writer notes (observations + relations into shared topic-* / hub-* pools for overlapping-entity contention) and, per --edit-ratio, appends to shared hub notes (max contention) and to its own earlier notes. Deterministic per --seed; --max-seconds optionally caps the phase.
  • Measurement: per-op latency + error rows (per-op.jsonl), per-op-type mean/p50/p95/max, throughput (notes/min, ops/s), error taxonomy (deadlock / sqlite_locked / timeout / write_conflict / other), terminal writer failures, settle time, full-reindex wall time.
  • Convergence verification (the v0.23 claim): after the index settles, checks directly against the on-disk files and the run's isolated SQLite DB:
    • markdown files == hubs + successful creates; entity rows == files
    • no duplicate permalinks
    • no duplicate (entity, category, content) observation tuples (the #1214 duplication metric, plus a redundancy % figure)
    • no duplicate (from, to, type) relation tuples
    • every observation line written by a reported-success op is on disk exactly once — each generated line carries a unique bmk-* marker, so lost appends and doubled appends are both detected
  • Artifacts under benchmarks/runs/<run-id>/: manifest.json (provenance incl. BM SHA), per-op.jsonl, concurrent-write-summary.json, summary.md.
  • Divergence is a valid benchmark result (v0.22.1 is expected to diverge), so the command is report-only by default; --strict makes it exit nonzero when checks fail (used by just bench-write-smoke).

Refactor

_WarmMcpClient, _resolve_bm_command_prefix, and _status_json_is_ready moved from providers/bm_local.py into a shared bm_runtime.py (provider delegates; behavior unchanged) so the driver reuses the same warm-MCP/stdio contract instead of duplicating it.

Fairness / reproducibility

  • Same external bm CLI + MCP contract for every BM version under test (--bm-local-path for checkouts); write_note/edit_note signatures are identical between v0.22.1 and main.
  • Fresh isolated home per run under benchmarks/.bm-homes/ (BASIC_MEMORY_CONFIG_DIR + sandboxed BASIC_MEMORY_HOME so the default project doesn't index operator notes).
  • BASIC_MEMORY_* env passes through, so Redis/semantic axes are toggled the same way as the retrieval scripts.
  • Postgres row-integrity queries are a follow-up; the write workload itself is DB-agnostic.

Validation (small scale, current main)

Run cw-validate-main-r1 — 4 writers x 25 notes (194 ops: 100 creates + 90 appends + 4 hub setups) against basic-memory main @ ca5c54a8, SQLite:

  • 193/194 ops ok; 1 create failed with sqlite3.OperationalError: database is locked (recorded as sqlite_locked, correctly excluded from expected state)
  • 56.5 notes/min, settle 8.5s, full reindex 2.5s; create p50 293ms / p95 1.9s, edit_hub p50 1.3s / p95 7.0s (long tails up to ~65s under 4-way same-note contention)
  • Convergence: 6/7 checks pass — files (103) == entity rows == hubs + successful creates, 0 duplicate permalinks, 0 duplicate observation tuples (0.0% redundancy), 0 duplicate relation tuples, 0 doubled writes
  • 1 check failed, and it looks like a real product signal, not a driver defect: no_lost_writes found 3 of 45 concurrent hub appends that edit_note acknowledged as success but that are absent from the final file AND from every DB table (verified manually). All three are same-hub appends racing other writers, i.e. an acknowledged append lost to a last-writer-wins overwrite across separate bm mcp processes. Whether that is accepted LWW semantics or an edit_note race worth fixing is a #1248/product question — exactly the kind of thing this driver exists to surface.

The strict gate (just bench-write-smoke) exits nonzero on divergence; report-only is the default since divergence is the expected v0.22.1 result.

Unit tests cover plan determinism, marker uniqueness, edit-target validity, latency stats, error classification, and the integrity checks against synthetic SQLite fixtures (clean, duplicated-observation, lost-write, duplicate-permalink cases).

Refs basicmachines-co/basic-memory#1248

🤖 Generated with Claude Code

https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9

Adds 'bm-bench run concurrent-write': N independent bm mcp client sessions
(the basic-memory#1214 field-report shape) create and edit notes in one
shared project with overlapping relation targets and contended hub notes,
capturing per-op latency/errors and throughput, then verifying convergence
against the on-disk files and the run's isolated SQLite index: file/entity
counts, duplicate permalinks, duplicate observation/relation tuples, and
per-line bmk-* markers that detect both lost and doubled writes.

Report-only by default (divergence is a valid benchmark result for
v0.22.1); --strict exits nonzero for gating. just targets:
bench-write-smoke, bench-write-load.

Refactor: _WarmMcpClient, _resolve_bm_command_prefix, and
_status_json_is_ready move from providers/bm_local.py to a shared
bm_runtime.py (provider delegates; behavior unchanged) so the driver and
the retrieval provider share one bm CLI/MCP contract.

Fairness/reproducibility: same external bm contract for every version
under test, fresh isolated home per run (BASIC_MEMORY_CONFIG_DIR plus
sandboxed BASIC_MEMORY_HOME), BASIC_MEMORY_* env passthrough for axis
toggles, full provenance in manifest.json. Postgres row-integrity
queries are a follow-up; the workload itself is DB-agnostic.

Validated end-to-end at 4 writers x 25 notes against basic-memory main
(ca5c54a8): 193/194 ops ok, zero duplicate rows, zero deadlocks; the
marker check surfaced 3/45 concurrent hub appends acknowledged by
edit_note but absent from file and DB (last-writer-wins overwrite
across MCP sessions) - the class of signal the driver exists to catch.

Refs basicmachines-co/basic-memory#1248

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b435ed0036

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +940 to +944
if config.measure_reindex:
console.print("Measuring full reindex wall time...")
reindex_start = time.monotonic()
run_command(prefix + ["reindex", "--search", "-p", config.project_name], env=env)
reindex_seconds = time.monotonic() - reindex_start

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check integrity before running the mutating reindex

With the default --measure-reindex, this command reindexes the project before querying the SQLite database for convergence. Consequently, the reported verdict describes the post-reindex state rather than the state reached when the concurrent workload settled; any rows repaired, removed, or introduced by reindexing can mask the concurrency defect or create a false one. Capture integrity immediately after _settle_index, then run the optional reindex timing separately.

Useful? React with 👍 / 👎.

Comment on lines +119 to +122
if self._thread.is_alive():
self._requests.put(None)
self._thread.join(timeout=self._startup_timeout_seconds)
self._thread = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Terminate timed-out MCP sessions before verification

When an operation exceeds request_timeout_seconds, its MCP call remains in flight, but stop() waits only the 30-second startup timeout and then discards the thread reference even if the thread is still alive. A sufficiently hung write can therefore complete while _settle_index, reindex, or integrity verification is running, making the resulting artifacts race with an untracked writer; stop must cancel/terminate the subprocess or confirm the thread has exited before returning.

AGENTS.md reference: AGENTS.md:L111-L117

Useful? React with 👍 / 👎.

created_at_utc=utc_now_iso(),
benchmark_git_sha=git_sha(Path(".")) or "unknown",
bm_source=config.bm_source,
bm_resolved_sha=git_sha(Path(config.bm_local_path)) if config.bm_local_path else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the resolved SHA for default BM runs

Whenever --bm-local-path is omitted—the documented default and both new just targets—this unconditionally records bm_resolved_sha as null even though the actual executable may be any installed basic-memory>=0.20.2; the manifest simultaneously labels its source as GitHub main. Published results therefore cannot identify or reproduce the BM implementation being compared, so resolve the installed executable's revision or require a pinned source.

AGENTS.md reference: AGENTS.md:L78-L83

Useful? React with 👍 / 👎.

@phernandez

Copy link
Copy Markdown
Member Author

Superseded by basicmachines-co/basic-memory#1304.

The benchmark suite is canonical in Core under /benchmarks, so the concurrent-write driver has been ported there instead of landing in this repository. The Core port also addresses the three open review findings here: it records integrity before reindex, guarantees timed-out MCP sessions stop before verification, and requires an exact local git SHA for provenance.

This repository is being archived.

@phernandez phernandez closed this Aug 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant