sec: admin auth, embed-backfill durability, and embed-queue hardening (M23, L20, L21, L22)#37
Merged
Merged
Conversation
…md units internal/index/hnsw_writer.go implements PassageWriter against a Pebble store + in-memory HNSW graph, with periodic checkpointing and a manual Flush for shutdown. Covered by hnsw_writer_test.go (bridges passages into the graph, and verifies the auto-flush-at-threshold contract). Not yet wired into cmd_crawl/cmd_serve — those already use their own hnswPassageWriter in cmd/cosift; this is an alternate, independently tested implementation living in internal/index for review. Also adds the systemd unit/timer files used to run cosift-crawl, cosift-serve, cosift-snapshot, and the ollama service(s) in production. No secrets included — cosift-snapshot.service loads its admin token via EnvironmentFile from /etc/cosift/snapshot.env, not inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M23 — embed-backfill only added vectors to the in-memory HNSW graph, so a restart discarded everything the backfill produced. Snapshot the graph to the store when the handler finishes and report persist status in the response. L20 — the pebble HTTP path compared the peer/admin bearer token with a plain string comparison. Route all 24 admin gates through a shared peerTokenOK helper backed by subtle.ConstantTimeCompare. L21 — the zombie-reclaim diagnostic counter was a plain int read and written from every crawl worker. Make it an atomic.Int64. L22 — the decoupled embed queue dropped a document's passages immediately whenever the buffer was momentarily full, and its workers ran on the crawl context, which the terminator cancels as soon as the frontier drains, so queued jobs died with a cancelled context. Producers now wait a bounded interval for buffer space; embed workers run on a context detached from the crawl cancellation, with a per-job timeout and a post-crawl drain deadline so the shutdown wall clock stays bounded. Tests cover each: backfill persistence round-trips through the store, the token helper's accept/reject matrix plus a source guard, a race-detector test for the diagnostic counter, and an end-to-end test that all queued embed jobs complete after the crawl context is cancelled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the open
cosiftrows from the findings register: M23, L20, L21, L22.Each change has a test that fails on
mainand passes here.M23 —
embed-backfillnever persisted its vectorshandleEmbedBackfillcalleds.hnsw.AddPassagefor every chunk it embedded, which only mutates the in-memory graph. Nothing wrote the graph back to Pebble, so a restart discarded the entire backfill.Snapshot the graph via
s.hnsw.Persistonce the worker pool drains, on a detached context so a client disconnect can't abort the write. The response now carriespersisted(andpersist_errorwhen it fails) instead of silently reporting success.Test:
TestEmbedBackfillPersistsHNSWruns the handler against an empty graph and reloads viaindex.LoadHNSW— onmainthe store holds no HNSW meta at all.L20 — non-constant-time peer/admin token compare
The pebble HTTP path compared the request's bearer credential against
cfg.Cluster.PeerAuthTokenwith!=, which short-circuits on the first differing byte.internal/server/http.goalready usedsubtle.ConstantTimeCompare; this path diverged, in 24 places.Added
peerTokenOK/bearerTokeninserve_helpers.goand routed every gate through it, including the two odd variants infeedback.goandquerylog.go. The empty-token single-node default is preserved.Tests:
TestPeerTokenOK(accept/reject matrix incl. prefix and suffix cases),TestRequireAdminGate, andTestAdminGatesUseConstantTimeCompare— a source guard that fails if a raw comparison reappears.L21 — data race on
zombieDebugLoggedThe bounded diagnostic counter behind
COSIFT_ZOMBIE_RECLAIM=1was a plainintread and incremented from every crawl worker. Now anatomic.Int64, with the bound expressed asAdd(1) <= 5.Test:
TestZombieReclaimDiagnosticCounterConcurrentcrawls 60 pages atMaxConcurrent=16— reproduces reliably under-raceonmain(3/3 runs).Note: the test uses a locally-defined embedder rather than the package's
stubEmbedder, whose own call counter is unsynchronized and only safe atMaxConcurrent=1.L22 — embed queue dropped passages and lost in-flight jobs
Two separate losses on the decoupled path:
COSIFT_EMBED_ENQUEUE_WAIT_MS, default 5s) before giving up.runCtx, whichterminatorcancels as soon as the frontier goes quiet. Everything still queued then failed with a cancelled context. Workers now run on a context detached from that cancellation, with a per-job timeout (COSIFT_EMBED_JOB_SECONDS, default 120s) and a post-crawl drain deadline (COSIFT_EMBED_DRAIN_SECONDS, default 300s) so shutdown wall-clock stays bounded when the embedder is slow or unreachable. Work abandoned past the deadline is counted as dropped and left forembed-backfill.The per-job body moved into
runEmbedJobso the timeout context is scoped cleanly.Test:
TestEmbedPoolFinishesQueuedJobsAfterCrawlContextCancelcrawls 40 pages against a 100ms embedder that honors cancellation. Onmain: 26 of 40 jobs fail. Here all 40 complete. Plus focused tests for the enqueue wait budget and the drain deadline.Verification
go build ./...andgo test -race -count=1 ./...are green across all 17 packages.🤖 Generated with Claude Code