feat(chroma): opt-in HTTP client/server mode for multi-process palaces - #2005
feat(chroma): opt-in HTTP client/server mode for multi-process palaces#2005ahabshamaa wants to merge 4 commits into
Conversation
Embedded PersistentClient is not process-safe: multiple MemPalace processes (MCP servers, stop-hook mines, manual CLI runs) writing the same palace contest the sqlite file lock and can corrupt the HNSW index (MemPalace#1963), and a blocked writer waits with no timeout, hanging the MCP server. With MEMPALACE_CHROMA_MODE=http, one standalone `chroma run` server exclusively owns the persist directory and every MemPalace process is a thin HTTP client. The default stays embedded — zero change for single-process installs. - New mempalace/palace_client.py: the single construction point for the Chroma client. CHROMA_HOST/CHROMA_PORT (default 127.0.0.1:8801). - Health probe: construction hits /api/v2/heartbeat with a 3s timeout and raises PalaceBackendUnreachableError naming host:port instead of hanging. - Hard per-op timeout: TimeoutProxy wraps the client and every collection obtained from it; each wire call runs under a 15s ceiling (MEMPALACE_OP_TIMEOUT) and raises PalaceOperationTimeoutError naming the operation. - ChromaBackend._client/make_client route through palace_client in HTTP mode: cache keyed on server address (inode/mtime freshness is meaningless over HTTP), embedded pre-open repair pass skipped (client-side repair against a live server's files is a corruption risk). - mcp_server: startup health probe (non-fatal), HTTP-mode client cached for process lifetime, HNSW capacity probe gated off (embedded-only segfault guard, MemPalace#1222), unreachable/timeout surfaced as structured tool errors. - tests/conftest.py pins embedded mode so the suite's temp-dir palaces never touch a real server; tests/test_palace_client.py covers env resolution, unreachable-server errors, and the timeout wrapper. Refs MemPalace#832, MemPalace#1096, MemPalace#1963 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two locks exist solely to guard embedded ChromaDB's in-process state and re-create the multi-writer failure HTTP mode removes: - ChromaCollection._write_lock took the non-blocking mine_palace_lock flock, which guarded embedded ChromaDB's multi-threaded HNSW corruption (MemPalace#974/MemPalace#965). Over HTTP the server serializes writes; kept on, two concurrent writers from separate processes fail hard with MineAlreadyRunning instead of both succeeding. - The MCP peer-writer lease (MemPalace#1818) holds mine_palace_lock for the process lifetime to protect peers from each other's stale in-memory HNSW/FTS state. In HTTP mode that hazard cannot occur, and the lease would turn peer MCP servers read-only — the same incompatibility reported for daemon topologies in MemPalace#1888. Whole mining passes still take mine_palace_lock for their own application-level atomicity in both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…able error An in-flight call on an existing client whose server dies mid-request surfaced raw httpx.ConnectError — bounded and clean, but not the structured error the health probe gives at construction time. run_with_timeout now translates httpx.TransportError / builtin ConnectionError into PalaceBackendUnreachableError naming the operation, so consumers see one error shape whether the server died before or during the call. Non-transport exceptions still propagate unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds docs/chroma-client-server.md covering the opt-in client/server topology, the config surface, the structured error contract, launchd and systemd service examples, heartbeat/backup/vacuum operations, and the list of embedded-only mechanisms gated off in HTTP mode. Links it from the README's storage backends section. Refs MemPalace#1096 (which asked for exactly this deployment documentation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fatkobra
left a comment
There was a problem hiding this comment.
Blocking: this is a timeout on the caller's wait, not a timeout or cancellation of the Chroma operation. The implementation explicitly leaves a timed-out call running and then reports failure. For add, upsert, update, or delete, the caller can retry while the original request later commits, producing duplicate, reordered, or overlapping effects.
Repeated hangs also occupy all eight executor workers. Subsequent healthy calls can remain queued and time out before they ever start, making the failure self-sustaining.
Please use transport-level connect/read/write/pool timeouts enforced by the HTTP client. An ambiguous write timeout must be reported as "outcome unknown" and reconciled with an idempotency key or status check rather than treated as a clean failed operation. Add tests for commit-after-timeout and executor saturation.
|
Thanks for this contribution, and apologies for the slow turnaround.
If you'd rather not pick it back up, no problem at all — just say so and I'll close it out, and thanks either way for taking the time to send it. |
What does this PR do?
Adds an opt-in HTTP client/server mode for the ChromaDB backend, as requested in #832 and #1096 — and implements the "ChromaDB HTTP server mode" option named in the architectural decision epic #1963.
Problem. Embedded
chromadb.PersistentClientis not process-safe. When more than one process writes the same palace — multiple MCP server instances, stop-hook mines firing alongside an interactive session, a manualmempalace mine— writers contest the same sqlite/HNSW files and can corrupt the index (the #1963 cluster). Worse, a blocked writer waits on the file lock with no timeout, so palace calls can hang the MCP server indefinitely.Fix. With
MEMPALACE_CHROMA_MODE=http, one standalonechroma runserver exclusively owns the palace directory and every MemPalace process is a thin HTTP client constructed in one place (mempalace/palace_client.py). The server serializes writes where ChromaDB actually supports concurrency; clients get:/api/v2/heartbeat(3 s timeout) and raisesPalaceBackendUnreachableErrornaming host:port instead of hanging.MEMPALACE_OP_TIMEOUT) via aTimeoutProxywrapping the client and every collection obtained from it; timeouts raisePalaceOperationTimeoutErrornaming the operation.httpx.TransportError,ConnectionError) are translated into the same structured unreachable error the probe gives; the MCP server returns these as structured tool errors, never hangs, never retries (the bound already happened).Default unchanged.
embeddedremains the default: zero change and no external service for single-process installs. Config surface:MEMPALACE_CHROMA_MODE,CHROMA_HOST/CHROMA_PORT(default127.0.0.1:8801),MEMPALACE_OP_TIMEOUT.Embedded-only mechanisms gated off in HTTP mode (each is correct for a process that owns the files, wrong for a thin client):
_write_lockflock — SIGSEGV in HNSW parallel inserts — missing hnsw:num_threads on collection creation #974/Critical Data write loop, HNSW compaction bug #965 guarded embedded multi-threaded corruption; kept on it turns legitimate concurrent writers intoMineAlreadyRunningerrors;Mining passes still take
mine_palace_lockfor application-level atomicity in both modes.Docs:
docs/chroma-client-server.md— topology, config, error contract, launchd + systemd service examples, heartbeat/backup/vacuum runbook (#1096 asked for exactly this).Relationship to the daemon+bridge direction (#1270/#1976): complementary, not competing — this is opt-in, changes nothing by default, and gives multi-process users a supported deployment today at the layer where ChromaDB itself supports concurrency.
Closes #832. Closes #1096. Related to #1963, #1581, #1799, #1888.
How to test
Checklist
python -m pytest tests/ -v) — 3284 passed; the 2 failures intest_repair.py(_errors_are_isolated_fts5string matching) fail identically on cleandevelopin this environmentruff check .)🤖 Generated with Claude Code