Skip to content

feat: resolve ingest conflicts and complete storage, Docker, and API fixes - #169

Merged
n0nuser merged 2 commits into
mainfrom
fix/160-ingest-lock
Aug 6, 2026
Merged

feat: resolve ingest conflicts and complete storage, Docker, and API fixes#169
n0nuser merged 2 commits into
mainfrom
fix/160-ingest-lock

Conversation

@n0nuser

@n0nuser n0nuser commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes #160
Closes #168
Closes #158
Closes #155

Summary

This feature PR rebases and resolves the concurrent-ingest conflict from the
original PR, then completes the related storage, Docker, and API follow-ups.

Changes

  • Preserve ingest progress reporting while enforcing the cross-process ingest
    lock for every write entry point.
  • Remove the exact persisted HNSW segment directory when deleting a Chroma
    collection, and invalidate collection-bound retrieval caches.
  • Stamp Docker images with LOCALRAG_BUILD_SHA, expose authenticated
    /build-info, and add task docker-check for stale-stack detection.
  • Add optional request-scoped collection selection to JSON, contexts, and SSE
    query paths while retaining the configured collection as the default.
  • Record the collection namespace trust boundary in ADR 040.

Verification

  • 476 unit/non-integration tests passed.
  • 13 Compose integration tests passed; 14 environment-dependent corpus tests
    were skipped by their existing prerequisites.
  • Real Chroma HNSW deletion test passed after exceeding the 1,000-item sync
    threshold.
  • Ruff lint and format, mypy, and Bandit passed.
  • Rebuilt the image before integration testing; build-info matched the Git
    revision.

n0nuser added 2 commits August 6, 2026 15:51
… path

Two `localrag ingest` processes writing the same `CHROMA_PERSIST_PATH` silently
lost writes. Chroma's embedded `PersistentClient` keeps HNSW segments in
per-process memory with no cross-process invalidation, so the loser's upsert
failed inside Chroma ("Error creating hnsw segment reader: Nothing found on
disk") while the CLI still reported `status=ok` and exit code 0.

`VectorStore._write_lock` is a `threading.RLock` and only ever serialized
writers inside one process. This adds the missing cross-process boundary:
`localrag/storage/persist_lock.py::ingest_lock` takes an advisory, non-blocking
`flock` on `<persist_path>/.ingest.lock`. This enforces the contract ADR 035
already states ("multi-process writers require an external ownership/locking
boundary and are not supported by this contract") rather than setting a new one,
so no new ADR accompanies it.

The lock sits at the ingest use-case boundary (`IngestionService.ingest_paths`
and `rebuild_collection`), not in `VectorStore`. `api/dependencies.py` and
`application/runtime.py` each cache a `VectorStore` for the whole process
lifetime, so a store-scoped lock would let a running API hold it forever and
deadlock every CLI ingest. Held only for the duration of a write; read and query
paths stay unlocked. Rebuild takes it too because it deletes and re-embeds every
source.

Acquisition fails fast instead of queueing — an ingest can run for minutes, so a
second caller is better told to retry than left hanging. Contention surfaces as
`ConcurrentIngestError`, mapped to `409 Conflict` over HTTP and to a stderr
message plus exit code 1 on the CLI. Nested acquisition is counted in-process
because `flock` is per-fd and an inner release would otherwise drop the outer
lock. An unwritable persist directory or a filesystem without `flock` degrades
to a logged warning rather than blocking ingest.

Closes #160

Claude-Session: https://claude.ai/code/session_01BXupLJw7GHrgJg3eVaAraq
@n0nuser
n0nuser force-pushed the fix/160-ingest-lock branch from 7303c48 to c7fa124 Compare August 6, 2026 14:24
@n0nuser n0nuser changed the title fix(storage): reject a second concurrent ingest on one Chroma persist path feat: resolve ingest conflicts and complete storage, Docker, and API fixes Aug 6, 2026
@n0nuser
n0nuser merged commit 9dda815 into main Aug 6, 2026
12 of 13 checks passed
@n0nuser
n0nuser deleted the fix/160-ingest-lock branch August 6, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment