john-mlika opened a new pull request, #16481:
URL: https://github.com/apache/lucene/pull/16481

   <!-- TITLE: Backport #14977 to branch_10x: invoke 
KnnVectorsReader#finishMerge() from SegmentMerger#cleanupMerge() -->
   <!-- TARGET: apache/lucene branch_10x (PR) -->
   <!-- LOCAL BRANCH: 10x-backport-14977-finishmerge @ 
john-ubuntu:/home/john/lucene-10x -->
   <!-- COMMIT: 13a20e21836d4151b9495be13b199aabc281b76f -->
   
   Closes #16419
   
   ### Problem
   
   On `branch_10x`, `KnnVectorsReader#finishMerge()` is dead code — a sweep of 
the branch finds only
   its definitions (`KnnVectorsReader`, `Lucene99FlatVectorsReader`, 
`Lucene99HnswVectorsReader`,
   `PerFieldKnnVectorsFormat$FieldsReader`) and the `AssertingKnnVectorsFormat` 
wrapper. There is no
   call site.
   
   The flip side of the machinery is fully wired. `MergeState` starts the 
cascade
   (`knnVectorsReaders[i] = knnVectorsReaders[i].getMergeInstance()`), and
   `Lucene99FlatVectorsReader#getMergeInstance` implements it by switching the 
**shared,
   search-visible** raw `.vec` `IndexInput` to `DataAccessHint.SEQUENTIAL` — it 
mutates and returns
   `this`. Nothing ever switches it back. The advice sticks until the segment 
reader is closed, so
   pooled readers that concurrent searches share keep SEQUENTIAL advice long 
after the merge is over,
   and segments that survive as sources of an aborted merge keep it until close.
   
   How the branch got here:
   
   - #14977 fixed exactly this on `main` (`085a0ae054b`, its own rationale: 
*"ReadAdvice not being
     reset properly"*) by moving the cascade initiation into 
`SegmentMerger#cleanupMerge()`, invoked
     from both `IndexWriter` call sites inside `finally` blocks. It was never 
backported here.
   - `d1c4db43036` ("Prevent writing vectors twice during merging HNSW graphs", 
the `branch_10x`
     backport of #15732) **deleted the previous caller** — the private 
`finishMerge(MergeState)`
     helper and its `reader.finishMerge()` loop in `KnnVectorsWriter#merge()` — 
without the #14977
     replacement existing on the branch.
   - `d1c4db43036` is an ancestor of `releases/lucene/10.5.0`, so 10.5.0 
shipped with it.
   
   This only bites Directory implementations that honour read advice (mmap); it 
is a no-op elsewhere.
   
   ### Change
   
   Backport of `085a0ae054b`, kept as close to the main commit as the branch 
allows.
   
   - `SegmentMerger#cleanupMerge()` restored (`SegmentMerger.java:331`) — 
byte-identical to main's.
   - Invoked from both `IndexWriter` call sites inside `finally` blocks: the 
`addIndexes(CodecReader…)`
     path (`IndexWriter.java:3587`) and the merge path 
(`IndexWriter.java:5408`). Both `try`/`finally`
     restructurings are line-for-line identical to the main commit's hunks.
   - `SegmentMerger#mergeFieldInfos()` narrowed `public` → `private`, as in the 
main commit. Cosmetic:
     `SegmentMerger` is a package-private final class and the only caller is 
internal (`:119`).
   - `TestDoc` and `TestSegmentMerger` each gain the one 
`merger.cleanupMerge()` line the main commit
     added; both drive `SegmentMerger` directly rather than through 
`IndexWriter`.
   
   Two deviations from `085a0ae054b`, both forced by the branch:
   
   1. **The `KnnVectorsWriter.java` hunk is omitted because it is already 
applied.** On main, #14977
      deleted the private `finishMerge(MergeState)` helper and its call from 
`KnnVectorsWriter#merge()`
      (−9 lines). On `branch_10x` `d1c4db43036` already deleted exactly that 
code — that deletion is
      what caused this bug. The file is therefore not touched, and the resulting
      `KnnVectorsWriter#merge()` matches main's post-#14977 state.
   2. **Diff context differs at both `IndexWriter` call sites**: the 
`branch_10x` `SegmentMerger`
      constructor takes an extra trailing `MergePolicy.OneMerge merge` argument 
(from the #16368 /
      #16391 backport) that main does not have. Context only — no change to the 
backported lines.
   
   Nothing else diverges. The post-state of `cleanupMerge()` and both `finally` 
blocks is textually
   identical to main.
   
   ### Test
   
   New `TestMergeReadAdviceRevert` 
(`lucene/core/src/test/org/apache/lucene/codecs/lucene99/`). It
   indexes two segments, holds an NRT reader open across `forceMerge(1)`, and 
records every
   `updateIOContext` per file through a recording `FilterDirectory`, asserting 
that each `.vec`
   switched to SEQUENTIAL for the merge is switched back before its input 
closes.
   
   Three constraints, or it silently false-passes:
   
   - the field must be a **plain float HNSW** field — on `branch_10x` the 
quantized readers don't
     override `getMergeInstance` at all, so a quantized detector would fail for 
the wrong reason;
   - an **NRT reader must be held open across the merge** — a merge-created 
pooled reader is opened
     with `IOContext.merge(...)`, whose `withHints` is a documented no-op, so 
the flip never becomes
     observable;
   - the recording `IndexInput` must explicitly delegate `updateIOContext`, 
`clone()` and **both**
     `slice` overloads — `FilterIndexInput` forwards none of them.
   
   It also carries a precondition assertion that fails loudly if no `.vec` ever 
received a SEQUENTIAL
   `updateIOContext`, so a future change that stops flipping advice cannot turn 
this into a vacuous
   pass.
   
   **Before/after, verified in separate patched and stock worktrees** with the 
exact same detector and
   seed `648B326F8F9AF8DD`:
   
   - Stock production code — **FAILS**:
   
   ```
   java.lang.AssertionError: .vec inputs still using SEQUENTIAL advice:
     _0_Lucene99HnswVectorsFormat_0.vec [ADVICE:SEQUENTIAL]
     _1_Lucene99HnswVectorsFormat_0.vec [ADVICE:SEQUENTIAL]
   ```
   
     One flip per source segment, no revert, and both inputs still open (pinned 
by the NRT reader)
     when the assertion runs.
   
   - Fix restored — **PASSES**, same seed. The temporary stock worktree was 
removed after its failure
     log was preserved.
   
   ### AssertingKnnVectorsFormat
   
   `AssertingKnnVectorsFormat`'s close assertion is tightened to what #14977 
made it on main
   (`AssertingKnnVectorsFormat.java:237`):
   
   ```java
   -      assert finishMergeCount.get() <= 0 || mergeInstanceCount.get() == 
finishMergeCount.get();
   +      assert mergeInstanceCount.get() == finishMergeCount.get();
   ```
   
   This is part of why the regression survived the backport: with the weaker 
form, `AssertingCodec`
   cannot catch a recurrence, and it did not catch this one.
   
   It is not cosmetic. With `cleanupMerge()`'s body disabled and the strict 
assertion in place, a full
   `:lucene:core:test` run fails **27 tests across 9 suites** — 26 of them 
tracing to
   `AssertingKnnVectorsFormat.java:237` via 
`AssertingKnnVectorsReader.close()`, plus the new detector:
   
   ```
   TestPerFieldKnnVectorsFormat, TestColumnBatchVectorColumn, TestCheckIndex,
   TestConcurrentMergeScheduler, TestKnnByteVectorQueryMMap, 
TestKnnFloatVectorQuery,
   TestPatienceByteVectorQuery, TestPatienceFloatVectorQuery, 
TestSeededKnnByteVectorQuery
   ```
   
   With the fix in place all of them pass. So the tightening catches the 
genuine gap this PR closes,
   not unrelated pre-existing breakage — it does not need to be split out.
   
   ### CHANGES.txt
   
   Entry added under 10.6.0 → Bug Fixes, citing GITHUB#16419 and crediting 
Simon Cooper (the original
   #14977 author) alongside the backport.
   
   ### Validation
   
   Rebased onto `branch_10x` at `2ab41d8d0f4` on 2026-08-03. A subsequent 
comment-style pass amended
   the commit to `58948f7aeac`; its range-diff against the pre-audit commit 
changes only this test's
   comments/assertion text and the `CHANGES.txt` wording. A final message-only 
amendment removed the
   commit body, producing `13a20e21836` with the same tree. All production 
paths are byte-identical.
   
   JDK 21 (Temurin 21.0.7), `./gradlew check` across every module — **37 
modules, 19,777 tests,
   1,461 skipped, 0 failures** (seed `F25239DD5384A691`):
   
   | Module | Tests | Result |
   | --- | --- | --- |
   | `:lucene:core` | 8559 (265 skipped) | SUCCESS |
   | `:lucene:backward-codecs` | 2939 (1015 skipped) | SUCCESS |
   | `:lucene:analysis:common` | 1855 (19 skipped) | SUCCESS |
   | `:lucene:spatial3d` | 712 (3 skipped) | SUCCESS |
   | `:lucene:codecs` | 684 (65 skipped) | SUCCESS |
   | `:lucene:test-framework` | 590 (30 skipped) | SUCCESS |
   | … 31 further modules | | all SUCCESS |
   
   Targeted vector-codec sweep (`codecs.lucene99.*`, `lucene95.*`, 
`lucene94.*`, `lucene104.*`,
   `lucene103.*`, `codecs.hnsw.*`, `codecs.perfield.*`, 
`TestMergedVectorValues`): **687 tests,
   23 skipped, 0 failures**.
   
   The two direct `SegmentMerger` lifecycle suites (`TestDoc`, 
`TestSegmentMerger`) add **4 tests,
   0 failures**. `./gradlew tidy` produces no changes; the full `check` 
completed 900 tasks successfully.
   
   ### Notes for review
   
   - The advice-revert cascade is only reachable for readers that propagate 
`getMergeInstance()`. On
     this branch that is plain float/HNSW via 
`Lucene99FlatVectorsReader`/`Lucene99HnswVectorsReader`;
     the quantized readers don't override it at all, so they are shielded from 
the bug by a second gap.
     If that gap is closed on `branch_10x` (a separate change propagating 
`getMergeInstance` through
     the quantized readers), quantized fields would join this regression — 
which is why that backport
     should land after this one, not before.
   - `cleanupMerge()` is invoked from a `finally` that begins *after* `new 
SegmentMerger(...)` returns.
     If the constructor itself throws partway through `MergeState`'s cascade, 
merge instances already
     obtained are not finished. That is the same on main; this backport does 
not change it, and it
     seems out of scope here, but a reviewer may want to note it.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to