Phase 0.9 — Spike S14 (cocoindex concurrency under LMDB single-writer)
Phase 0.9 — Spike S14 (cocoindex concurrency under LMDB single-writer)
Section titled “Phase 0.9 — Spike S14 (cocoindex concurrency under LMDB single-writer)”Audit date: 2026-05-11 (KH session S230)
Branch: content-items-investigation (worktree-agent-a8ffb1adaede42c93)
Spike spec: 0.9-spike-plan.md §S14 (NEW S229; added after S2 surfaced LMDB swap)
Cocoindex version installed: 1.0.3 (PyPI; identical to S2 install)
Python version: 3.14.2 (Homebrew); venv at .venv-spike-s14/ (worktree-local; not committed)
Test corpus: docs/client-documentation-base/ (35 files — same as S2)
Spike status: PASSED — S2’s “single-writer constrains multi-worker Cloud Run topology” framing is REVISED. Cocoindex 1.0.3 over LMDB does NOT hard-block concurrent processes; LMDB’s writer-lock is held in tiny windows per ops-DB write, and multiple cocoindex processes can run App.update() concurrently against the same LMDB without lock errors, data corruption, or measurable serialisation overhead. Recommended v1 Cloud Run topology = (single-orchestrator-instance) for efficiency-of-work reasons (avoid 10× duplicate @coco.fn execution), NOT for correctness. Reversibility excellent — topology can be revisited without schema or data migration.
1. Executive summary
Section titled “1. Executive summary”| Question | Answer |
|---|---|
| Does LMDB single-writer hard-reject concurrent process-level writers? | No. 10 concurrent processes against the same LMDB ops-DB all completed cleanly (10/10 success, 0 lock errors, no corruption). LMDB’s writer-lock IS single-writer at the transaction level, but cocoindex’s per-row ops-DB writes are short — concurrent processes interleave via the lock without contention-induced failure. |
| Does cocoindex 1.0.3 coordinate work across concurrent processes? | No. 10 concurrent processes each computed 34-35 of the 35 @coco.fn invocations (343 total observe invocations vs ~35 if perfectly deduped). The second-to-cache process for any given row gets the cache value and skips invocation, but there’s no cross-process work-stealing or job-claim primitive. Result: N concurrent processes do ~N× the @coco.fn work — that’s the cost shape, not lock contention. |
| Does cocoindex’s memo cache stay coherent under concurrent writers? | Yes. After 10 concurrent processes against one LMDB, a follow-up App.update() observes 0 invocations (full memo hit). The cache converges to the correct (deduplicated) state. |
| Do read-only consumers block when a writer holds the LMDB? | No. With a slow writer running (HOLD_MS=200, full_reprocess=True), 3 concurrent reader processes each opened the LMDB env and completed their 2s sleep cleanly. LMDB’s documented MVCC (“Multiple readers can hold the lock concurrently, but writers have exclusive access” — verbatim from Rust binary strings) holds. |
| What’s the v1 Cloud Run recommendation? | Topology (d-new) — single-orchestrator-instance with isolated per-instance LMDB. Each Cloud Run container has its own filesystem; LMDB-per-container is the default. Topology (a) (concurrent writers, shared LMDB) is technically correctness-safe but wastes N× compute via duplicate @coco.fn execution. Topology (b) (queue-based) is unnecessary at v1 scale. Topology (c) (read-only-worker fan-out) is viable for query-side workers; not needed for ingest. |
| Does cocoindex 1.0.3 support a Postgres-backed ops-DB (no LMDB)? | No. Postgres connector is target-only (write path for materialised views). The ops-DB backend is LMDB; the Rust binary contains no SQLite/Postgres-as-state-store code paths. (“Migrating legacy LMDB files from…” strings confirm the SQLite-to-LMDB swap is complete; there is no reverse path.) |
| Does cocoindex survive a writer crash mid-transaction? | Yes. Kill -9 a writer at 3s of a 17.5s run (5/35 rows written), then re-run another writer: recovery completes with 30 remaining invocations + 0 errors. LMDB’s MVCC + cocoindex’s per-row commit make this safe. |
G14 decision-gate verdict: v1 Cloud Run topology = single-orchestrator-instance with isolated per-instance LMDB. Cloud Run autoscaling can drive multiple cocoindex instances safely (no shared state needed for correctness), but should be tuned so only one instance handles ingest at a time to avoid duplicate work. Migrate to topology (b) queue-based or (c) read-only-worker only if v1 traffic exceeds single-instance throughput (~50-200 concurrent rows/sec is empirically feasible per S2 + this spike).
2. Method (executed)
Section titled “2. Method (executed)”2.1 Setup
Section titled “2.1 Setup”Per 0.9-spike-plan.md S14 + CLAUDE.md sandbox gotcha for cocoindex installs:
python3 -m venv .venv-spike-s14PIP_USER=0 PIP_TARGET="" .venv-spike-s14/bin/pip install cocoindex# Confirmed: cocoindex 1.0.3, Python 3.14.2, identical to S2 venvAll Python invocations + LMDB-engine startup run with dangerouslyDisableSandbox: true per S2’s §4.5 gotcha.
2.2 Probe harness
Section titled “2.2 Probe harness”Spike-local scripts at .spike-s14/ (gitignored):
| Script | Purpose |
|---|---|
.spike-s14/concurrency_probe.py | Reusable per-process harness. Single App.update() against docs/client-documentation-base/. Honours COCOINDEX_DB env var for LMDB path, HOLD_MS for per-row sleep inside @cocoindex.fn, FULL_REPROCESS=1 to bypass memo, `mode = update |
.spike-s14/run_topology_a.sh | Topology (a) — 2 concurrent writers, same LMDB. |
.spike-s14/run_topology_a_n4.sh | 4 concurrent writers, same LMDB. |
.spike-s14/run_topology_a_n10.sh | 10 concurrent writers, same LMDB (stress). |
.spike-s14/run_topology_b.sh | Topology (b) — N serialised invocations against same LMDB. |
.spike-s14/run_topology_c.sh | Topology (c) — writer with FULL_REPROCESS=1 + N read-only consumers. |
.spike-s14/run_isolated_lmdb.sh | Topology (d-new) — N concurrent writers, isolated per-process LMDB dirs. |
.spike-s14/run_crash_recovery.sh | Crash probe — kill -9 a writer mid-transaction, then re-run. |
2.3 The reusable probe
Section titled “2.3 The reusable probe”@cocoindex.fn(memo=True)def _observe(path: str, content: bytes) -> dict: """Trivial observer; emits invocation count + size.""" if _HOLD_MS: time.sleep(_HOLD_MS / 1000.0) # widen transaction window sys.stderr.write(f" observe: {path} (size={len(content)})\n") return {"path": path, "size": len(content)}
async def _main() -> None: files = walk_dir(CORPUS, recursive=True) async def index_one(item): data = await item.read() return _observe(str(item.file_path), data) await cocoindex.mount_each(index_one, files.items())
def run_update() -> None: app = cocoindex.App("s14_probe", _main) full_reprocess = os.environ.get("FULL_REPROCESS", "") == "1" app.update_blocking(report_to_stdout=False, full_reprocess=full_reprocess)Baseline single-process run with HOLD_MS=200 against fresh LMDB: 7.18s wall-clock; 35 @coco.fn invocations. This is our reference.
2.4 Topology (a) — concurrent writers, shared LMDB
Section titled “2.4 Topology (a) — concurrent writers, shared LMDB”Probes: do two/four/ten concurrent processes against the same LMDB block, fail, or corrupt?
| Configuration | N | HOLD_MS | Wall (s) | All exit=0? | Observe per proc (range) | Total observes | Post-run memo coherent? |
|---|---|---|---|---|---|---|---|
| Fresh LMDB | 2 | 200 | 7.20 | yes | 34-35 | 69 | yes (0 on re-run) |
| Fresh LMDB | 4 | 200 | 8 | yes | 34-35 | 138 | yes |
| Fresh LMDB | 10 | 300 | 11 | yes (10/10) | 34-35 | 343 | yes (0 on re-run) |
Interpretation:
- All processes succeed. No
MDB_BUSY, noMDB_INVALID, no “lock held” rejection. Concurrent process-level writers ARE supported (despite LMDB’s documented single-writer model — this works because the writer lock is held in tiny per-row windows, not across the whole engine update). - Wall-clock barely scales with N. Going from 1 → 10 concurrent processes increases wall-clock from 7.2s → 11s (1.5×). LMDB writer-lock contention is real but small.
- Cross-process work-coordination is absent. 343 observe invocations across 10 processes vs ~35 if perfectly coordinated. Each process re-fingerprints the corpus independently; the “winner” for any given row commits its result first, and subsequent processes see the cache. 34-35 invocations per process means at most 1 row per process saw the cache early enough to skip — most rows are still processed by 9/10 processes.
- Final state is coherent. Re-running
App.update()after the concurrent run observes 0 invocations. Whichever process committed last for each row is in the cache. No corruption observed across any topology.
2.5 Topology (b) — queue-based serialisation
Section titled “2.5 Topology (b) — queue-based serialisation”Probes: do N serialised invocations against the same LMDB run cleanly start-to-finish?
== Topology (b) — 4 serialised writers, same LMDB == slot 1: OK pid=81182 elapsed=7.20s (cold; 35 observe) slot 2: OK pid=81291 elapsed=0.02s (full memo; 0 observe) slot 3: OK pid=81295 elapsed=0.03s (full memo; 0 observe) slot 4: OK pid=81297 elapsed=0.03s (full memo; 0 observe)WALL-CLOCK: 8s for 4 serial runsThe first run does all the work (7.20s); subsequent runs hit a fully-memoised cache and complete in ~30ms. Cocoindex’s incremental Δ semantics work perfectly across process restarts — exactly what S2’s §4.2 promised.
Implication: if the corpus is changing (new files arriving), serialised invocations are the most efficient pattern. Each run picks up only the new/changed files; previously-processed files stay in the cache. This is the natural fit for a “ingest job triggered by webhook / cron / Cloud Tasks” pattern.
2.6 Topology (c) — read-only-worker fan-out
Section titled “2.6 Topology (c) — read-only-worker fan-out”Probes: do read-only consumer processes block when a writer holds the LMDB?
Setup: writer runs with FULL_REPROCESS=1 + HOLD_MS=200 against a pre-seeded LMDB → ~7s of active writes. While writer is running, spawn 3 read-only consumer processes (each opens the env via cocoindex.start_blocking() then sleeps 2s).
== Topology (c) — 1 writer + 3 read-only consumers ==-- writer exit=0 --OK pid=82518 elapsed=7.18s-- reader 1 --READ-OK pid=82521 elapsed=2.00s-- reader 2 --READ-OK pid=82522 elapsed=2.00s-- reader 3 --READ-OK pid=82523 elapsed=2.00sAll readers completed without blocking. Cross-process MVCC reads work as advertised — verbatim from cocoindex Rust binary: “Multiple readers can hold the lock concurrently, but writers have exclusive access.”
Implication for v1: if KH ever needs to expose the LMDB ops-DB to read-side workers (e.g. fan-out a query-only API tier sharing the orchestrator’s LMDB via Cloud Filestore), the cross-process read story works. Note: KH’s actual query path is Postgres (the target), not LMDB (the ops-DB), so this is a contingency rather than a v1 design driver.
2.7 Topology (d-new) — isolated per-process LMDB
Section titled “2.7 Topology (d-new) — isolated per-process LMDB”This isn’t in the original spike plan but emerges naturally as the Cloud Run default: each container has its own filesystem; LMDB-per-container is the natural shape.
== Isolated-LMDB-per-process topology == slot 1 observe count: 35 (full work; OK pid=85360 elapsed=7.21s) slot 2 observe count: 35 (full work; OK pid=85361 elapsed=7.20s) slot 1 re-run observe count: 0 (memo coherent in own LMDB) slot 2 re-run observe count: 0 (memo coherent in own LMDB)Two isolated LMDBs each fully process the corpus and converge to their own coherent memo state. This is what Cloud Run autoscaling looks like by default — and it’s correctness-safe at the cost of N× work per N concurrent containers.
2.8 Crash recovery
Section titled “2.8 Crash recovery”Probes: does cocoindex survive a SIGKILL’d writer mid-transaction?
== Crash-recovery probe: kill writer mid-transaction == killing pid=84623 (SIGKILL) after 3s of work [partial write ~5/35 rows]== Recovery: run another writer against the crashed LMDB == exit=0 observe count: 30 [pid=84669] update OK in 0.05sLMDB is crash-safe by design (append-only data + lockfile recovery), and cocoindex inherits this. Critical for Cloud Run — pod termination mid-update doesn’t corrupt the ops-DB; the next invocation picks up where the crashed one stopped.
2.9 Verification of Postgres-as-ops-DB option
Section titled “2.9 Verification of Postgres-as-ops-DB option”S2 §3 documented the SQLite-to-LMDB swap but left open whether a Postgres ops-DB option exists (which would dissolve the LMDB question entirely for KH, since the Postgres staging branch is already running).
Probe: inspect cocoindex._internal.setting.Settings + the Rust binary symbol table for any postgres_url/backend/metadata_store settings.
Result: No Postgres-as-ops-DB option exists in 1.0.3. The Rust binary contains:
db_path(LMDB path)lmdb_max_dbs,lmdb_map_size- “Migrating legacy LMDB files from…” (SQLite→LMDB migration code)
- No Postgres / SQLite ops-DB code paths
Postgres is target-only (writes the materialised view of cocoindex’s flow output). The ops-DB / state store is LMDB-only. If LMDB becomes a v1 blocker (it isn’t, per this spike), the workaround is not “switch to Postgres ops-DB”; it’s either accept isolated-LMDB-per-instance, or wait for upstream cocoindex to add a multi-writer backend.
3. Per-spike-question findings
Section titled “3. Per-spike-question findings”3.1 LMDB locking semantics — empirically verified
Section titled “3.1 LMDB locking semantics — empirically verified”S2 §4.5 stated: “LMDB single-process semantics. LMDB is single-writer; multi-process concurrent writes are blocked.” — partially incorrect. Refined finding:
- LMDB’s writer transaction is single-writer (one writer can hold the write-txn at a time).
- LMDB’s writer lock is process-coordinated via a memory-mapped lockfile (
lock.mdb). - Concurrent processes do not hard-fail; they queue at the writer-lock and take turns. Per-row commits are short (microseconds), so wall-clock impact is minimal.
- Within a single process, the Rust core uses a
tokio::sync::RwLockto coordinate concurrent flow components — seecocoindex_py::rwlock::RWLockReadGuard/RWLockWriteGuardin the binary symbol table.
Correct framing: LMDB enforces transaction-level write-serialisation; cocoindex doesn’t add additional cross-process coordination beyond LMDB’s built-in lockfile. Concurrent processes work, but waste N× compute on uncoordinated @coco.fn execution.
3.2 Queue-based serialisation pattern (Topology b) — works as intended
Section titled “3.2 Queue-based serialisation pattern (Topology b) — works as intended”Serialised invocations against a stable LMDB demonstrate the incremental Δ pattern: first run does full work; subsequent runs hit the memo and complete near-instantly. This is the natural pattern for Cloud Run + Cloud Tasks (or any work-queue) wiring:
[file change webhook] → [Cloud Tasks queue] → [Cloud Run job: cocoindex App.update()] ↓ [LMDB on persistent volume] ↓ [Postgres target writes]The cocoindex job runs to completion against the current corpus state; the cache means repeated runs are cheap.
3.3 Read-only-worker pattern (Topology c) — works
Section titled “3.3 Read-only-worker pattern (Topology c) — works”If KH needed to fan out read-side query workers sharing the LMDB ops-DB, cross-process MVCC reads succeed alongside an active writer. However, this isn’t relevant for v1 KH topology because:
- KH’s query path goes through Postgres (the cocoindex target), not the LMDB ops-DB.
- The Postgres target IS multi-reader-multi-writer (it’s a real database).
- LMDB is just the operations / state store; nobody outside cocoindex’s flow engine queries it.
So Topology (c) is filed as a “useful capability if a future v1.1 or v2 feature needs it” — not a v1 design driver.
3.4 Cloud Run mapping
Section titled “3.4 Cloud Run mapping”KH’s Cloud Run projects (kh-prod-494815 + kh-staging-494815) currently host the Python pipeline jobs. The v1 architecture absorbs cocoindex’s orchestrator role into this tier.
Container filesystem semantics:
- Cloud Run instances each have their own ephemeral filesystem (
/tmp/is writable;/is read-only after image build). - Without a shared volume mount (Cloud Filestore / GCS Fuse / Cloud Storage volume), each container has its own LMDB → topology (d-new) by default.
- With a shared volume mount (Cloud Filestore), multiple containers can share an LMDB → topology (a) or (c).
Recommended v1 deployment:
| Component | Cloud Run topology | Why |
|---|---|---|
| cocoindex orchestrator | Single-instance via min_instances=1, max_instances=1 OR scheduled Cloud Run job (one-shot per invocation) | Avoid the N× duplicate-work cost of Topology (a). LMDB lives on Cloud Run job container fs + persists to Cloud Storage volume mount OR is rebuilt from scratch each invocation (full re-fingerprint = ~7s for a 35-file corpus per this spike; scales linearly). |
| KH API tier (Next.js) | Vercel; cocoindex-irrelevant | Reads Postgres directly; no LMDB dependency. |
| KH MCP server | Vercel; cocoindex-irrelevant | Reads Postgres directly. |
| Query / search workers (if added) | Cloud Run multi-instance; reads Postgres | No LMDB share needed. |
LMDB persistence choice (open question for Phase 2):
- Option α — ephemeral LMDB, rebuild each run. Simplest. ~7s overhead per invocation for 35 files; scales to ~minutes for full client corpus. Cost: re-fingerprinting overhead. Win: stateless orchestrator, no volume management.
- Option β — persisted LMDB on Cloud Storage volume mount. Lower latency on repeated invocations (memo hits). Cost: volume management, eventual-consistency concerns if mount semantics aren’t strong. Cloud Run’s gcsfuse + Cloud Storage volume is the documented path; latency is higher than local disk but acceptable for cocoindex’s ops-DB writes (small + infrequent).
Recommend Option α for v1 (ephemeral LMDB). Re-fingerprinting at job start is bounded; the simplicity is worth it. Promote to Option β only if profiling shows full-corpus re-fingerprint exceeds the job invocation budget.
3.5 Reversibility
Section titled “3.5 Reversibility”Topology choice is fully reversible without schema or data migration:
- If v1 ships single-orchestrator + ephemeral LMDB and v1.1 needs multi-writer: just add the volume mount. Cocoindex’s flow definitions, schema, and target bindings are unchanged.
- If upstream cocoindex adds a multi-writer ops-DB backend (e.g. Postgres-backed): swap the backend via Settings. Flow code unchanged.
- If KH outgrows Cloud Run and needs Kubernetes: cocoindex’s container model is platform-agnostic; the same LMDB volume mount story applies.
No lock-in. This is the rare architectural decision where deferring optimisation is risk-free.
4. Decision gate G14 outcome
Section titled “4. Decision gate G14 outcome”G14: cocoindex concurrency under LMDB single-writer
| Sub-decision | Verdict |
|---|---|
| LMDB hard-blocks concurrent writers? | No — empirically refuted. 10 concurrent writers OK. |
| v1 Cloud Run topology | Single-orchestrator-instance with isolated per-instance LMDB. Use min_instances=1, max_instances=1 for the cocoindex job, OR scheduled Cloud Run job invocations. |
| LMDB persistence | Ephemeral (Option α) for v1. Re-fingerprint cost bounded; simplicity wins. Promote to volume-mounted (Option β) if profiling demands. |
| Queue infra needed? | No additional queue infra needed for v1. Cloud Run’s built-in trigger mechanisms (Cloud Tasks if needed, but optional) are sufficient. |
| Read-only-worker capability | Available but not used for v1. Filed for future query-side workers if needed. |
| Reversibility to multi-writer if v1.1 demands | Excellent — schema, data, target bindings unchanged. Just add volume mount + raise max_instances. |
G14 status: RESOLVED. Phase 2 architecture commits to single-orchestrator-instance cocoindex on Cloud Run with ephemeral LMDB. No new infra required beyond what’s already running for the Python pipeline.
Phase 2 architecture-impl cost delta vs S229 estimate: zero. Topology decision is configuration, not code.
5. Surprises and residual questions
Section titled “5. Surprises and residual questions”5.1 Surprises
Section titled “5.1 Surprises”- S2’s “single-writer constrains multi-worker topology” framing was overcautious. LMDB’s writer-lock is real but cocoindex’s per-row writes are short — concurrent processes interleave at the lock without erroring. The actual constraint is “wasted compute” (N× work), not “lock contention failure”. Correction note for downstream docs.
- Cocoindex doesn’t coordinate work across concurrent processes. Each process re-fingerprints the full corpus; only the LMDB memo cache deduplicates results post-hoc. If KH ever wants true multi-process work-sharing, it needs an external coordinator (e.g. a row-claim primitive in a separate queue) — but that’s a v1.1+ scaling concern, not v1.
- The migration tooling in the binary (“Migrating legacy LMDB files from…”) suggests cocoindex’s own developers were once using SQLite ops-DBs. The swap to LMDB is recent. Watch for upstream churn — a Postgres-backed ops-DB option could land in a future version and dissolve the v1.1 multi-writer question.
- Crash recovery is built-in. SIGKILL’d writers leave the LMDB intact + the next invocation resumes. This is exactly what Cloud Run needs given pod-termination semantics.
- The “max_inflight_components” knob on
AppConfigis the intra-process concurrency control — distinct from the inter-process question this spike addressed. Worth tuning in Phase 2 if profiling shows the orchestrator’s @coco.fn fan-out is throughput-bound.
5.2 Residual questions
Section titled “5.2 Residual questions”| Q | For |
|---|---|
| Q-S14-1 | Full-corpus re-fingerprint cost at production scale. Spike used 35 files (~7s with HOLD_MS=200). KH’s first-client corpus is ~500-2000 files; binary docx/pdf files add IO. Expected: 30s-3min per cold run. Acceptable if Cloud Run job timeout is 10min+ — confirm before committing to Option α (ephemeral LMDB). |
| Q-S14-2 | Cocoindex flow-restart vs partial-restart semantics. If a Cloud Run job is killed mid-flight, does the next invocation start with the LMDB state at kill-time OR does it re-run the entire flow from scratch? This spike confirmed crash-safety; the partial-progress question wasn’t directly tested. Mid-spike observation: killed writer at 5/35 → recovery processed remaining 30. Suggests partial resume works as expected. Confirm at full-corpus scale. |
| Q-S14-3 | Cloud Storage volume mount (gcsfuse) latency profile on LMDB writes. If KH later wants Option β (persisted LMDB), need to confirm volume-mount latency doesn’t cause LMDB writer-lock starvation. Plan: latency-probe pre-Phase-3-deploy if Option β is chosen. |
| Q-S14-4 | Upstream cocoindex roadmap on multi-writer / Postgres-backed ops-DB. Could materially affect v1.1 topology. Track upstream releases; re-evaluate annually. |
| Q-S14-5 | Cloud Run min_instances=1 cost vs scheduled-job cost. Scheduled cocoindex jobs (Cloud Scheduler → Cloud Run jobs) avoid the always-on instance cost but have cold-start latency. v1 budget impact: marginal either way at v1 scale; revisit if cost-conscious. |
6. Recommendations for downstream artefacts
Section titled “6. Recommendations for downstream artefacts”6.1 Updates to 0.9-intended-architecture.md
Section titled “6.1 Updates to 0.9-intended-architecture.md”§10 (Tool stack composition) — cocoindex pipeline (orchestrator) block should specify:
Single-instance orchestrator on Cloud Run (
min_instances=1, max_instances=1) with ephemeral per-instance LMDB ops-DB. Cocoindex’s incremental Δ semantics + LMDB crash-safety mean re-runs after kill are cheap and safe. No queue infrastructure required for v1.
§11 (UI surface) — no changes (this is back-end-only).
§12 (Dedupe / data-fix) — note that cocoindex’s per-row dedup is single-process; cross-process dedup (UC8) still needs S10’s separate substrate evaluation.
6.2 Updates to 0.9-spike-S2-cocoindex-folder-binding.md
Section titled “6.2 Updates to 0.9-spike-S2-cocoindex-folder-binding.md”§4.5 (Operational concerns) item 2 — current text:
LMDB single-process semantics. LMDB is single-writer; multi-process concurrent writes are blocked. For KH’s planned multi-worker Cloud Run topology, the ops-DB needs partitioning (one LMDB per worker shard) OR the engine must run as a singleton orchestrator with workers as
@coco.fnrunners only. Architecture revision needed if multi-worker concurrent ingest is mandatory for v1. Confirm with S1 + a separate “cocoindex concurrency” probe (not in current spike-plan; recommend adding as S14).
Recommended revision:
LMDB transaction-level single-writer. LMDB’s writer-lock serialises transactions; concurrent processes are NOT hard-blocked (S14 empirically verified 10/10 success with 10 concurrent writers). However, cocoindex doesn’t coordinate work across concurrent processes — N concurrent writers do ~N× the @coco.fn work, with the memo cache deduplicating results post-hoc. v1 recommendation: single-orchestrator-instance on Cloud Run with ephemeral per-instance LMDB. See S14 for full analysis. The architecture revision risk flagged in S2 is dissolved.
6.3 Updates to 0.9-spike-plan.md
Section titled “6.3 Updates to 0.9-spike-plan.md”§S14 status: NEW S229 → ✅ COMPLETE S230. PASSED — single-orchestrator topology confirmed.
§7 Risk register — remove the “Cocoindex concurrency unclear / Cloud Run topology” entry.
6.4 New CLAUDE.md gotcha (cocoindex install)
Section titled “6.4 New CLAUDE.md gotcha (cocoindex install)”Add under Gotchas → General:
cocoindex installs require sandbox bypass. PyPI install (
pip install cocoindex) AND Rust-engine LMDB startup both fail under macOS sandbox profile with “Operation not permitted”. UsedangerouslyDisableSandbox: truefor all cocoindex-related Bash calls including subprocess invocations. Recorded across spikes S2 + S14.
(Both S2 and S14 hit this; codifying in CLAUDE.md prevents re-surfacing in future spikes / Phase 2 work.)
7. Effort revision (S229 baseline → S230 actual)
Section titled “7. Effort revision (S229 baseline → S230 actual)”| Estimate | S229 spike-plan §1 budget | S230 actual |
|---|---|---|
| S14 effort | 1 day | ~3.5 hours main-session worktree sub-agent (incl. probe scripting + Rust binary inspection + 4 topology tests + crash test + docs) |
| Effort delta on Phase 2 architecture-impl | possibly +2-5 days if multi-writer needed | 0 days — single-orchestrator is config-only |
| Cloud Run infra delta | possibly +Cloud Tasks queue / Pub-Sub | 0 — uses existing Cloud Run jobs |
S14 was 1d planned, ~0.5d actual. The risk it was sized to investigate dissolves cleanly with the empirical observation.
8. Reproducibility — to re-run this spike
Section titled “8. Reproducibility — to re-run this spike”# From worktree or main:cd <repo>
# 1. Venv (identical to S2)python3 -m venv .venv-spike-s14PIP_USER=0 PIP_TARGET="" .venv-spike-s14/bin/pip install cocoindex.venv-spike-s14/bin/python3 -c "import cocoindex; print(cocoindex.__version__)"# Expected: 1.0.3
# 2. Topology probesHOLD_MS=200 bash .spike-s14/run_topology_a.sh # 2 concurrent writersHOLD_MS=200 bash .spike-s14/run_topology_a_n4.sh # 4 concurrent writersHOLD_MS=300 bash .spike-s14/run_topology_a_n10.sh # 10 concurrent writers (stress)HOLD_MS=200 N=4 bash .spike-s14/run_topology_b.sh # 4 serialised writersHOLD_MS=200 N=3 bash .spike-s14/run_topology_c.sh # writer + 3 readersbash .spike-s14/run_isolated_lmdb.sh # isolated-LMDB-per-processbash .spike-s14/run_crash_recovery.sh # SIGKILL recoveryAll scripts live under .spike-s14/ (gitignored). Re-create from the inline excerpts in §2 above + the canonical corpus at docs/client-documentation-base/ if needed.
9. Confidence assessment
Section titled “9. Confidence assessment”S14 verdict confidence: 92%.
Evidence quality:
- Empirical multi-process verification on the canonical corpus.
- Rust binary symbol inspection confirms LMDB-only ops-DB backend (no hidden alternative).
- LMDB’s MVCC semantics are well-documented; cocoindex inherits them without surprises.
- Crash-recovery test confirms Cloud Run pod-termination resilience.
Remaining 8% drag:
- Cloud Run + Cloud Storage volume mount latency not directly tested (only on local fs).
- Production-scale corpus (~500-2000 files) not tested; behaviour should extrapolate cleanly but unverified.
- Upstream cocoindex multi-writer story may change in future versions.
Phase 2 commit confidence improves accordingly — the “cocoindex multi-worker topology blocker” hypothetical from S2 is empirically dissolved.
10. Decision recommendation (binding for G14)
Section titled “10. Decision recommendation (binding for G14)”Recommendation: v1 Cloud Run cocoindex topology = single-orchestrator-instance with isolated per-instance LMDB (ephemeral).
- Configure cocoindex job with
min_instances=1, max_instances=1OR as a scheduled Cloud Run job triggered by Cloud Scheduler. - LMDB lives on the instance’s ephemeral filesystem; rebuilt from scratch each cold-start.
- Re-fingerprint cost (~7s per 35 files) acceptable at v1 scale.
- No queue infrastructure required.
- No data-share volume mount required.
- Topology reversible to multi-writer or volume-mounted if v1.1 demands.
This dissolves the S2 §4.5 architecture-revision concern. Phase 2 architecture-impl can commit cocoindex as the v1 orchestrator without re-thinking Cloud Run topology.
End of S14 spike record. G14: RESOLVED (single-orchestrator-instance + ephemeral LMDB for v1).