You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On a freshly patched install where hnsw:num_threads: 1 is passed via metadata= to get_or_create_collection / create_collection (the fix in #991 and part of #976), the guard still catches sparse link_lists.bin bloat within an hour, and the kernel log records segfaults in chromadb_rust_bindings.abi3.so whenever multiple processes touch the palace concurrently:
kernel: tokio-rt-worker[229244]: segfault at 88 ip ... in chromadb_rust_bindings.abi3.so[32d4...]
kernel: tokio-rt-worker[230410]: segfault at 88 ip ... in chromadb_rust_bindings.abi3.so[32d4...]
Used concurrently by: Claude Code Stop/PreCompact hooks (per turn), a cron health-check (every 30 min), an hourly disk-guard timer, and the MCP server.
Reproduction
Restore a palace from backup SQLite, then run these concurrently (e.g. via threading.Thread spawning subprocess.run):
Each subsequent mine may fail with InternalError: Error constructing hnsw segment reader: Error deserializing pickle file: eval error at offset 0: EOF while parsing (truncated by the guard)
Root causes
chromadb 1.5.x does NOT persist modified configuration_json["hnsw"] across PersistentClient reopens. Setting hnsw:num_threads: 1 at collection-creation time is effectively a one-shot — the next process that opens the palace reads the persisted schema (which lacks the override) and chromadb reverts to the default parallel HNSW insert path. This is the observation that PR fix: HNSW graph corruption, PreCompact deadlock, mine fan-out (closes #974, #965, #955) #976 documents in its _pin_hnsw_threads docstring, and it matches the behavior I see on 3.3.2 after patches 1-3.
Metadata-only retrofit at the SQLite layer isn't enough for the same reason: I attempted a direct INSERT into collection_metadata to record hnsw:num_threads=1, and chromadb read the key back fine (collection.metadata shows it), but HNSW sparse growth still occurred — the runtime config is what governs the actual worker pool, and that lives in memory, not in the persisted schema.
Concurrent-writer races. Even when pinning is applied correctly, having multiple processes open the palace simultaneously still triggers tokio-rt-worker segfaults during chromadb shutdown (visible in kernel log). A palace-wide serialization mechanism is needed on top of the thread pinning.
Palace-wide write lock to serialize concurrent writers. The hook path (mempalace hook run --hook stop) is latency-sensitive and data-sensitive: a non-blocking skip (as in fix: HNSW graph corruption, PreCompact deadlock, mine fan-out (closes #974, #965, #955) #976) means every turn whose hook happens to collide silently drops the conversation save. A blocking queue with per-process timeout keeps data integrity while still failing fast on pathological contention.
Informative logging so operators can see which op is currently holding the lock and how long waiters should expect.
Queue pattern (blocking fcntl.flock with smart wait) instead of non-blocking skip — avoids losing hook-driven saves.
Status file ~/.mempalace/palace/.mine.status.json with {pid, pid_start_jiffies, op, started_at, est_seconds} so waiters can (a) budget their wait, (b) break stale locks if the owner's PID+start-time are gone.
Sleep cap (2s per iteration) so waiters don't over-sleep when the holder finishes earlier than est_seconds.
MEMPAL_LOCK_TIMEOUT env var (default 120s).
Tests covering acquire/release, queue semantics, fast-exit on est > timeout, 3-way serialization, sleep cap, stale-lock break after SIGKILL.
Happy to adjust scope / naming if a maintainer prefers — the core ask is just (a) runtime retrofit and (b) a palace-level write lock with a policy that preserves hook data.
Symptom
On a freshly patched install where
hnsw:num_threads: 1is passed viametadata=toget_or_create_collection/create_collection(the fix in #991 and part of #976), the guard still catches sparselink_lists.binbloat within an hour, and the kernel log records segfaults inchromadb_rust_bindings.abi3.sowhenever multiple processes touch the palace concurrently:Environment
--user --break-system-packages)Reproduction
Restore a palace from backup SQLite, then run these concurrently (e.g. via
threading.Threadspawningsubprocess.run):Observe:
link_lists.bingrows to sparse >100GB apparent size within a few minutesjournalctl -k | grep segfault.*chromadbshows crashesInternalError: Error constructing hnsw segment reader: Error deserializing pickle file: eval error at offset 0: EOF while parsing(truncated by the guard)Root causes
chromadb 1.5.x does NOT persist modified
configuration_json["hnsw"]acrossPersistentClientreopens. Settinghnsw:num_threads: 1at collection-creation time is effectively a one-shot — the next process that opens the palace reads the persisted schema (which lacks the override) and chromadb reverts to the default parallel HNSW insert path. This is the observation that PR fix: HNSW graph corruption, PreCompact deadlock, mine fan-out (closes #974, #965, #955) #976 documents in its_pin_hnsw_threadsdocstring, and it matches the behavior I see on 3.3.2 after patches 1-3.Metadata-only retrofit at the SQLite layer isn't enough for the same reason: I attempted a direct INSERT into
collection_metadatato recordhnsw:num_threads=1, and chromadb read the key back fine (collection.metadatashows it), but HNSW sparse growth still occurred — the runtime config is what governs the actual worker pool, and that lives in memory, not in the persisted schema.Concurrent-writer races. Even when pinning is applied correctly, having multiple processes open the palace simultaneously still triggers tokio-rt-worker segfaults during chromadb shutdown (visible in kernel log). A palace-wide serialization mechanism is needed on top of the thread pinning.
What's needed
num_threads=1on everyget_collection/create_collection, not just creation — covers existing palaces restored from backup. (Same approach as_pin_hnsw_threadsin fix: HNSW graph corruption, PreCompact deadlock, mine fan-out (closes #974, #965, #955) #976.)mempalace hook run --hook stop) is latency-sensitive and data-sensitive: a non-blocking skip (as in fix: HNSW graph corruption, PreCompact deadlock, mine fan-out (closes #974, #965, #955) #976) means every turn whose hook happens to collide silently drops the conversation save. A blocking queue with per-process timeout keeps data integrity while still failing fast on pathological contention.Proposed implementation
Filing a PR shortly. Delta from #976:
fcntl.flockwith smart wait) instead of non-blocking skip — avoids losing hook-driven saves.~/.mempalace/palace/.mine.status.jsonwith{pid, pid_start_jiffies, op, started_at, est_seconds}so waiters can (a) budget their wait, (b) break stale locks if the owner's PID+start-time are gone.est_seconds.MEMPAL_LOCK_TIMEOUTenv var (default 120s).est > timeout, 3-way serialization, sleep cap, stale-lock break after SIGKILL.Related issues / PRs
CONFLICTINGwith mainHappy to adjust scope / naming if a maintainer prefers — the core ask is just (a) runtime retrofit and (b) a palace-level write lock with a policy that preserves hook data.