Invariant — Proton LID Reuse Requires Monotonic RCU Generation Drain

In vespa/searchcore (proton), a freed 32-bit Local Document ID (LID) in ReferenceLidTracker / LidAllocator must never be reassigned to a new document until every concurrent search reader holding a GenerationHandler::Guard from the removal generation or earlier has completed (first_used_generation > removal_generation).

Why It Holds (Chesterton Fence)

Search threads in proton read AttributeVector arrays and HNSW link lists without per-lookup locks (O(1) direct array indexing by LID; see proton-lid-space-and-attribute-vectors). If a removed LID were immediately reused while an in-flight query still held a posting list containing that LID, the query would silently score and return attributes belonging to the newly inserted document.

Seductive Wrong Takeaway (CRIT-001)

  • Misconception: remove(docId) immediately frees the integer LID for the next put(docId).
  • Falsification: LidAllocator::unregister_lid places removed LIDs onto a generation-tagged hold list (_holdLids) drained only after generationHandler.update_oldest_used_generation() advances past the hold generation.
  • Do-Not-Import Boundary: Never bypass GenerationHandler hold queues when adding custom columnar or graph structures in vespa/searchlib.