Repository navigation
Conversation
Search results often describe a resource as it was created rather than as it is now, and resources deleted days ago keep appearing. The indexer's replicas share one JetStream queue group, each batches for a flush interval, and a flush replaces the whole document, so when one object's events land on different replicas the older event's batch can be enqueued last and overwrite the newer state. Dry-run creates are also indexed, leaving documents for objects that never existed. Key changes: - Record the newest state of every document in a JetStream KV bucket the indexer creates and every replica shares. A flush claims each write with a compare-and-set, applies the recorded state instead of its own when that is newer, and after enqueueing records the Meilisearch task that carries it. Meilisearch applies tasks in uid order, so a flush whose task would overwrite a newer state enqueued earlier re-enqueues that state behind its own task. Reading the current document from Meilisearch cannot arbitrate: its writes are queued tasks that take longer to apply than a flush interval under load. - Order operations within one buffer by resourceVersion, so a late or redelivered older event never displaces a newer buffered one. - Record a deleted object as a tombstone that wins over any later flushed write regardless of resourceVersion. Removal because an object stopped matching a policy stays an ordinary versioned state, so a newer matching write brings the document back. - Skip audit events whose request URI carries dryRun, and writes whose response has no resourceVersion. - Add stale_writes_superseded_total and order_repairs_total counters. Partitioning the stream by object instead would need a per-object token in the subject, which the Vector sink does not publish, a fixed replica count in place of the HPA, and would still regress on redelivery of a nacked older batch, so a version guard is needed either way.
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.
Summary
Search results often showed a resource as it was when it was created, so a load balancer that received its hostname seconds later could never be found by that hostname, and resources deleted days ago kept appearing. This happened because the indexer's replicas each batch events for a few seconds and replace the whole document on flush, so whichever replica flushed last decided what the index held, and nothing checked whether that state was the newest. With this change a document only ever moves forward: results reflect the current state of a resource, deleted resources disappear and stay gone, and hostnames or addresses assigned after creation become findable. Dry-run requests, which never create anything, no longer leave documents behind for objects that never existed.
What changed
Ordering guard (compare-and-set, with a shared arbiter). The indexer now creates and shares a JetStream KV bucket (
SEARCH_INDEXER_DOC_STATE, flag--nats-doc-state-bucket) that records, per index and document, the newest state any replica has accepted: itsresourceVersion, whether it is indexed, unindexed or deleted, a copy of the document, and the latest Meilisearch task known to carry a state for it. Every flush runs three steps ininternal/indexer/ordering.go:Within one buffer, an operation older than the one already buffered for its document is dropped instead of replacing it.
Why not read the current document from Meilisearch. Its writes are queued tasks; in production a task takes 47-79 s to apply at a healthy queue depth, longer than the 10 s flush window, so a read of the current document misses exactly the newer write it would need to see. The bucket is linearizable per key and has no lag. Keeping a copy of the document in the record is what lets the flush that would clobber a newer state re-enqueue that state itself, which closes the race between two replicas that both claim before either enqueues (covered by
TestBatcher_SameObject_ConcurrentFlushes_RepairsOverwrite).Why not partition by object. Audit events are published on one literal subject (
audit.k8s.activity) with no per-object token, so JetStream cannot partition them by uid without changing the Vector sink, the stream, the consumers and the Deployment, and replacing the HPA with a fixed partition count. Partitioning alone also does not survive redelivery: a nacked older batch redelivered after a newer one still regresses the document, so a version guard is needed either way.Delete wins over late writes. A
deleteverb, or a write carrying adeletionTimestamp, is recorded as a tombstone that supersedes any later-flushed write regardless ofresourceVersion, since a uid is never reused. Removal because an object stopped matching a policy stays an ordinary versioned state (DeleteOp.ObjectDeletedfalse), so a newer matching write brings the document back and a policy change cannot strand an object.Dry-run events are skipped. Events whose
requestURIcarriesdryRunare acked without indexing, for deletes as well as creates, and so are writes whose response object has noresourceVersion. The one e2e fixture that publishes hand-built audit events now carries aresourceVersion, as every real API server response does.Operations. The bucket is created by the indexer at startup (
CreateOrUpdateKeyValue), file-backed, history 1, with a TTL of 7 days (--doc-state-ttl, matching the audit stream's retention so no redelivered event can outlive the record that orders it) and--nats-doc-state-replicas(default 1; production can raise it to match its streams). Its size is roughly one entry per live indexed document. Two new counters,search_indexer_stale_writes_superseded_totalandsearch_indexer_order_repairs_total, show how often the guard acts. The 400 HTTPProxy documents already frozen in production are not touched by this change; a reindex of HTTPProxy after deploy brings them up to date or removes the ones whose objects are gone.Test plan
go test -timeout 5m ./...go vet ./...Fixes #123