Skip to content

fix(index): finalize full FTS rebuild maintenance (#1630) - #141

Open
DevNexsler wants to merge 1 commit into
mainfrom
maint/1630-regression-1160-was-marked-done-on-2026-08-18-but-
Open

fix(index): finalize full FTS rebuild maintenance (#1630)#141
DevNexsler wants to merge 1 commit into
mainfrom
maint/1630-regression-1160-was-marked-done-on-2026-08-18-but-

Conversation

@DevNexsler

Copy link
Copy Markdown
Owner

Ticket #1630

Ensure all full FTS rebuild fallbacks finish Lance restore-point, version-prune, and orphan-index cleanup lifecycle.

Validation: make gate code tiers passed (static 2014 collected; unit 1712 passed; integration 90 passed; staging-e2e 39 passed, 21/21 MCP tools covered/traced). Live preflight blocked externally: worktree config_test.yaml missing, production indexer active, Comm-Data-Store Postgres hostname unreachable.

Production follow-up: after merge/deploy, run 1160-lance-orphan-indices.sh once daily for three consecutive days; all runs must exit 0 before ticket moves to done.


Maint ticket: #1630 — tickets/review/1630-regression-1160-was-marked-done-on-2026-08-18-but-.md
On merge: verify acceptance boxes against production (not tests), tick them, then move ticket to tickets/done/ and set status: done.
Merged PR != closed ticket; unmerged PR != shipped fix.

@DevNexsler

Copy link
Copy Markdown
Owner Author

Left open by the 2026-08-27 reconciliation — the premise does not hold in production

I reproduced this ticket's defect and it is real: the #1160 outcome check fails right now.

$ scripts/verify-checks/1160-lance-orphan-indices.sh
verify FAIL (#1160): 448 index directories on disk for only 2 live indices (limit 34)
7.6G    /data/index/chunks.lance/_indices
10G     /data/index/chunks.lance/data

A read-only lance probe of the production dataset confirms it independently: 448 _indices
directories, only 8 uuids reachable across all 2994 retained versions, so 440 genuine
orphans
.

But this PR cannot fix it, because the path it hardens has never executed.

rebuild_fts_index() is wired into the three call sites that invoke store.create_fts_index()
(the needs_full_rebuild branch, the incremental-failure fallback, and the post-recovery
rebuild). Across the whole of /data/index/indexer.log — 629,428 lines, 2026-07-21 to
2026-08-27, the only other file being indexer.log.prev which ends 2026-07-21:

grep -c "Rebuilding FTS index"        -> 0     # the full-rebuild branch
grep -c "FTS index created/rebuilt"   -> 0     # create_fts_index()'s replace=True line
grep -c "FTS index created on column" -> 1     # 2026-08-22, the missing-index path only
grep -c "FTS index optimized (incremental merge)" -> 1803

The full-rebuild fallback has fired zero times in five weeks. The 440 orphans are not its
residue.

What they actually are

Orphan directory mtimes, by UTC day:

2026-08-22:  44      2026-08-24: 198
2026-08-23: 196      2026-08-25:   2   (newest orphan 2026-08-25T00:56:05)

Against ~103 incremental merges on 08-23 and ~102 on 08-24, that is roughly two index
generations per incremental merge
— the leak #1160 was originally about, from
ensure_fts_index(), not from a full rebuild.

Why the check is still red

_prune_orphan_indices runs only inside _finish_index_maintenance, at the end of a
completed index run. No index run has completed since 2026-08-24T23:00:12
(Index run completion: events: ~100/day through 08-24, then 0 on 08-25, 08-26 and 08-27;
index_run_state.json last_success.finished_at = 2026-08-24T23:00:16). Every run since
ended on an external event — a Docker daemon restart (08-25 00:38Z, 08-27 02:20Z, both
matching systemctl timestamps) or a container recreate from a deploy (08-25 12:52Z,
08-26 14:32Z, the latter one second after this image was built).

#1160's cleanup shipped in PR #122 and is present in the running image
(grep -c _prune_orphan_indices /app/lancedb_store.py → 2), but it has had no opportunity
to run since it deployed
. The check is red because the fix has never executed, not because
the fix is wrong and not because of the full-rebuild path.

Verdict

Left open. Per the reconciliation brief, "if you cannot make the suite go red on a defect you
can see in production, say so on the PR and leave it open — that finding outranks a merge."
That is this case: the new tests/test_store.py cases do go red at base and green after, but
they exercise a branch production never takes, so merging this would let #1630 be closed with
a verify: that is still red three days later — the exact false-close pattern this loop keeps
repeating.

To be fair to the change on its own terms: making a full rebuild finalize index maintenance is
correct, since a full rebuild does replace index generations. It is defensible hardening. It
is simply not this ticket's fix, and it should not carry this ticket's closure.

Also blocking a clean merge: this branch is based on a5b8955 and expects
ensure_fts_index(self, *, compact_data: bool = True), but main has since removed that
parameter — hence the CONFLICTING state in lancedb_store.py and tests/test_scan.py.
It needs a rebase onto current main regardless.

What would change my mind

A completed index run that leaves the orphan count high. I am about to get one: the sweep
running now is ~55 minutes from finishing. If its finalizer reclaims the 440 directories,
this analysis is confirmed; if it does not, the cleanup itself is broken and that is the
bug to fix — still not the full-rebuild path.

A second opinion (GPT-5.6 Sol, asked to refute) returned REFUTED against my first,
stronger claim that these orphans were caused by ticket #1625. It was right: the orphans
predate the run-completion drought rather than following from it, and I have corrected the
analysis above accordingly. Its narrow finding — "PR #141 did not create the current 440
orphans" — is what stands.

DevNexsler added a commit that referenced this pull request Aug 27, 2026
…tion

Every defect below was reproduced from production state or from a control run here
before its fix was read, and every red/green was executed on both sides by this pass
rather than taken from a PR body. Where a PR's red was only an ImportError on a new
symbol (#1655, #1657), a behavioural test that runs unchanged on both sides was written
so the red proves wrong behaviour, not an absent name.

Landed (PR -> ticket):
  #154 -> #1625  serve queued source work at bounded checkpoints during a long sweep,
                 and return 503 stale_tail once a request waits past its SLO
  #153 -> #1654  tier tests by location before name; match the _live.py suffix
  #152 -> #1663  stop the unit tier reaching the real detached indexer launcher
  #151 -> #1659  stop the staging gate racing for fixed host ports
  #150 -> #1661  keep Lance stale-read recovery in the store, not in mcp_server
  #149 -> #1662  keep the upstream body at every provider rejection site
  #148 -> #1657  keep the upstream body on a permanent HTTP rejection
  #147 -> #1658  match provider-sim's embedding character cap to the real route
  #146 -> #1650  let the telemetry seam see qwen's discarded first answer
  #145 -> #1655  bound embed inputs by the route's character cap, not tokens alone
  #144 -> #1656  recover Lance reads while a peer writer keeps moving the table
  #143 -> #1649  bound the nested index write lock join for deadlock, not scheduling
  #142 -> #1629  observe the enrichment route contract on every answer

Overlaps resolved rather than merged blind: #149 stacks on #148, so one merge lands
both; #150 depends on #144's table-identity recovery and would be a downgrade alone, so
they land together; #142 and #146 are two fixes to one root cause (the in-request JSON
validation retry) and compose cleanly rather than competing; #145 and #147 are the
client and simulator halves of the same character cap.

One change is this pass's own, written test-first: #1662 left
providers/llm/litellm_llm.py and providers/llm/openrouter_llm.py on the bare
raise_for_status, and litellm_llm is the enrichment route that was observably
discarding 400 bodies in production on 2026-08-26 and 2026-08-27.

Gate on the reconcile branch, all five tiers:
  static PASS | unit 1864 passed | integration 90 passed
  staging-e2e 41 passed (tool coverage 21/21) | live 123 passed, 0 failed
Baseline at origin/main run the same way: unit 1723 passed, integration 90 passed. No
new failures. Because lancedb_store.py changed and the venv resolves lance 4.0.0 while
the production image resolves lance 10.0.0, the deterministic suite was re-run inside
the candidate image: tests/test_store.py 149 passed on lance 10.

PROTECTED SURFACE: #154 changes the /health contract (new 503 stale_tail, new
INDEX_PENDING_MAX_AGE_S in docker-compose.yml). The container healthcheck raises on
503 and the post-deploy verify requires status=ok, so a stale tail can mark the
container unhealthy and can trip an auto-rollback. Status composition was checked:
stale_tail sits between index_failed and degraded, and disk_full still overwrites last,
so #0910's ordering holds.

Left open with written reasons: #141 (#1630 — hardens a path that has executed zero
times in five weeks; the 440 orphan _indices were reclaimed by the existing incremental
finalizer during this pass, confirming it) and #79 (#0584 — a month stale, and its
label alias map matches none of the 512 unstable doc_types measured in production).

Report: Maint-Manager/.briefs/reconcile/20260827/RAG-in-a-Box.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant