fix: fall back to FTS5 lexical search when chromadb HNSW segment segfaults on open - #1948
Conversation
|
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. |
…aults on open A corrupt HNSW segment segfaults chromadb on open; SIGSEGV is uncatchable and kills the CLI. Probe the open in a subprocess and, when unsafe, serve read-only FTS5 lexical results instead. Healthy palaces keep the vector path unchanged.
fb57cb3 to
5940882
Compare
|
Rebased onto current develop — merges cleanly now. A few notes on how the conflicts were resolved, since develop grew its own HNSW-divergence fallback (
Local verification: full suite green (4307 passed, 31 skipped), plus |
|
Thanks for rebasing this, and for the notes on how you resolved against develop's own HNSW-divergence fallback — that made the diff much easier to follow. The core reasoning here is right, and I want to say so before the concern: probing in a throwaway subprocess because SIGSEGV can't be caught in-process is the correct call, and the returncode handling (negative means killed by signal, so unsafe; positive means an ordinary exception the in-process path can surface properly) is exactly the right distinction. That part I'd merge as-is. The problem is the call site. You already identified the fix in the comment you left: # ponytail: re-probes chromadb every search. Cache on the HNSW segment's
# mtime/size if search latency on a healthy palace ever matters.It does matter, so I'd like that cache before this lands. Keying on the HNSW segment's Two smaller things while you're in there:
Happy to look again as soon as the probe is cached. |
The probe spawns a fresh interpreter that imports chromadb on every search, which does not fit the latency budget on a healthy palace. Cache the verdict per (palace_path, collection_name), keyed on a new hnsw_segment_signature() helper: (inode, mtime_ns, size) over every file in the VECTOR segment directory, resolved fresh each call. The crash verdict can only change when the segment changes underneath it, so a cached verdict is fresh exactly until the signature says otherwise — repair or a re-mine invalidates immediately. Same shape as the capacity cache (MemPalace#1471): before/after snapshots so a mid-probe write falls through uncached, a 10s max-age backstop for coarse-timestamp filesystems, and no caching when the segment cannot be resolved. Also drop the probe timeout from 120s to 15s: the probe only answers "does opening this segfault", and a timeout is already treated as unsafe, so a wedged open can no longer stall a single search for two minutes.
|
All three points addressed in fe0a747, pushed just now. Probe is cached on the segment fingerprint. New I also carried over the capacity cache's safety properties, since they apply verbatim here: before/after snapshots so an external write landing mid-probe falls through uncached rather than pinning a verdict the disk no longer supports, a 10s max-age backstop for filesystems with coarse timestamps (the signature is the freshness mechanism, the ceiling never fires on ext4/APFS), and no caching when the segment can't be resolved — a One honest scoping note: the cache is in-process, so it eliminates the probe entirely for the long-lived MCP server. A one-shot Timeout is 15s now. You're right that 120s allowed a two-minute stall on a single search. The probe only needs to import chromadb and open the collection — seconds even cold — and a timeout is already treated as unsafe, so a wedged open now degrades to lexical quickly instead of hanging. The Eleven new tests: cache hit, invalidation on segment change, unsafe-verdict caching, no-signature passthrough, TTL age-out, and |
The reasoning lives in the PR conversation; the code stays bare.
|
Follow-up in d00b6ca: stripped the explanatory comments from the probe cache — the reasoning lives here in the conversation and in the commit message, the code stays bare. Docstrings are one-liners now, including on the new Nothing behavioural changed; 167 tests across the touched files still pass, ruff clean. |
Problem
When a palace's HNSW vector segment is corrupt, chromadb segfaults while loading the segment on
get_collection().SIGSEGVcannot be caught withtry/except(it kills the interpreter), somempalace searchdies with exit 139 and returns nothing, even though the FTS5/sqlite lexical index inchroma.sqlite3is intact and independent of the vector segment.Fix
search()now probes the collection open in a throwaway subprocess (_chromadb_open_crashes) before touching chromadb in-process. The child runsget_collection(...).count(); a non-zero / signal exit or a timeout means opening in-process is unsafe._bm25_only_via_sqlitepath, rendered by a new_print_lexical_resultswith a clearlexical mode — vector index unavailablebanner. The CLI stays up and answers the query.The subprocess is the crux:
SIGSEGVis uncatchable in-process, so the only safe way to know whether an open will crash is to let a disposable child try it and inspect its exit status.Before / after
mempalace search "..."on a corrupt HNSW palace → core dump, exit 139, no output.Tests
tests/test_lexical_fallback_on_segfault.py(hermetic, no corrupt palace needed):-11/SIGSEGV → unsafe) and treats a timeout as unsaferuff check/ruff format --checkclean; existingtest_hybrid_search.pyandtest_empty_chromadb_results.pystill pass.