Skip to content

feat(chroma): opt-in HTTP client/server mode for multi-process palaces - #2005

Open
ahabshamaa wants to merge 4 commits into
MemPalace:developfrom
ahabshamaa:feature/chroma-http-client
Open

feat(chroma): opt-in HTTP client/server mode for multi-process palaces#2005
ahabshamaa wants to merge 4 commits into
MemPalace:developfrom
ahabshamaa:feature/chroma-http-client

Conversation

@ahabshamaa

Copy link
Copy Markdown

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.PersistentClient is 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 manual mempalace 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 standalone chroma run server 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:

  • Health probe — construction hits /api/v2/heartbeat (3 s timeout) and raises PalaceBackendUnreachableError naming host:port instead of hanging.
  • Hard per-operation timeout — every wire call runs under a 15 s ceiling (MEMPALACE_OP_TIMEOUT) via a TimeoutProxy wrapping the client and every collection obtained from it; timeouts raise PalaceOperationTimeoutError naming the operation.
  • One error shape — transport failures mid-call (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. embedded remains the default: zero change and no external service for single-process installs. Config surface: MEMPALACE_CHROMA_MODE, CHROMA_HOST/CHROMA_PORT (default 127.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):

Mining passes still take mine_palace_lock for 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

python -m pytest tests/ -v                      # suite forces embedded mode; 17 new tests in tests/test_palace_client.py
# live smoke test:
chroma run --path /tmp/palace-http --port 8801 &
MEMPALACE_CHROMA_MODE=http mempalace init ~/somewhere && MEMPALACE_CHROMA_MODE=http mempalace mine ~/somewhere
# kill the server mid-session → structured PalaceBackendUnreachableError, no hang

Checklist

  • Tests pass (python -m pytest tests/ -v) — 3284 passed; the 2 failures in test_repair.py (_errors_are_isolated_fts5 string matching) fail identically on clean develop in this environment
  • No hardcoded paths
  • Linter passes (ruff check .)

🤖 Generated with Claude Code

ahabshamaa and others added 4 commits July 14, 2026 12:50
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 fatkobra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@igorls

igorls commented Aug 15, 2026

Copy link
Copy Markdown
Member

Thanks for this contribution, and apologies for the slow turnaround.

develop has moved a fair way since this was opened and the branch no longer merges cleanly. If you're still interested in landing it, could you rebase onto current develop? Once it merges cleanly and CI is green I'll get it reviewed for the 3.8.0 cycle.

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.

@igorls igorls added enhancement New feature or request area/cli CLI commands storage needs-rebase PR has merge conflicts with develop and needs rebase labels Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli CLI commands enhancement New feature or request needs-rebase PR has merge conflicts with develop and needs rebase storage

Projects

None yet

3 participants