fix(state): recover from malformed legacy FTS index during rebuild - #86062
Christopher-Schulze wants to merge 9 commits into
Conversation
45772c8 to
67d6600
Compare
|
Thanks for the detailed review — all four points addressed:\n\n1. Recovery now runs on a trigger-complete corrupt install: a non-mutating FTS5 |
|
Nice turnaround on the review round — v2's narrow classifier and atomic One delta worth weighing for whichever branch maintainers pick as home: v2's No urgency from my side on which one wins — happy to fold ideas either way. |
fix(state): recover from malformed legacy FTS index during rebuild
|
67d6600 to
7cd2681
Compare
|
Thanks — adopted the one-pass probe. After a clean integrity-check (or a successful rebuild) we persist |
…view findings Adopt the two refinements from the NousResearch#86062 review round (credit @StanleyStetson for the analysis, @Christopher-Schulze for the implementation shape): - narrow error classification: only the malformed-index message class triggers a rebuild; transient lock/busy/IO errors are re-raised from the legacy rebuild path (restoring the pre-fallback escape to the open-retry path) and leave the engine marker unstamped in the gate so the next open retries the sweep - atomic recovery: the drop-triggers/drop-table/legacy-DDL/backfill fallback now runs as one BEGIN IMMEDIATE-wrapped executescript (the _recover_stale_fts house pattern) with rollback + the fts_stale breadcrumb on failure, so a concurrent writer can never observe a half-dropped index and a failed recovery can never freeze an empty-but-valid index under the stamped marker The engine-version marker gating, v23/cjk layout coverage, and breadcrumb containment from the original change are unchanged.
7cd2681 to
edfe0c6
Compare
|
Thanks both — the branch is rebased onto current Rebase + conflict resolution. The rebase kept upstream's newer semantics intact: the split trigger-subset repair condition ( Probe cost (your point, @strzhao, same as @Enough1122's #2). Adopted your Still material vs. #86183: this branch also carries the narrow malformed-only classifier (lock/busy/IO re-raised), the atomic |
|
Re-verified at rebased head
One latent nit (non-blocking): in except sqlite3.DatabaseError as exc:
...
except sqlite3.OperationalError as exc: # dead: OperationalError IS a DatabaseError
Nothing further from me otherwise — tight rebase. |
|
Thanks for carrying the marker gate through the rebase — with that adopted, the two branches are functionally converged, so I'm stepping #86183 down as a competing PR. Rather than leaving our remaining deltas as review suggestions (and risking another idea-only adoption), I've put them on top of your rebased head as one ready-to-cherry-pick commit, authorship-preserving:
What it adds, each with the failure it closes:
Verified: 7/7 in If you'd rather review than cherry-pick, everything above is also described in #86183's thread with the underlying verification data. Either way works — the goal is one merged fix, not two PRs. |
edfe0c6 to
85283ad
Compare
|
Convergence follow-up is now on
Evidence on current |
6bdf091 to
2611d64
Compare
2611d64 to
9eb4419
Compare
Maintenance updateRebased onto current
Head: |
04e35d7 to
c79f18f
Compare
The legacy inline FTS rebuild caught every sqlite3.DatabaseError (including lock/busy/IO) and ran non-atomic DROP + executescript, so a transient lock could commit a half-dropped index and a corrupt-but-trigger-complete DB was never rebuilt (the rebuild gate only fired on missing triggers). - _is_malformed_fts_index_error matches only the corrupt-index class and re-raises lock/busy/IO to the open retry path. - The recovery now runs inside one BEGIN IMMEDIATE transaction (drop + recreate + backfill + COMMIT), matching _recover_stale_fts. - A legacy FTS integrity-check probe on open detects malformed shadow tables even when all triggers are present, so a trigger-complete corrupt install self-heals on SessionDB open while keeping the inline FTS shape.
…ity stamp Adopts the remaining NousResearch#86183 deltas onto this branch's gate: - Class-based corruption classifier: corruption surfaces as plain sqlite3.DatabaseError while lock/busy/IO surface as OperationalError, so an isinstance split classifies both directions without depending on SQLite's wording. The string list previously missed the 'fts5: corrupt structure record' variant a really-corrupt index produced in testing. - Failed drop/recreate fallback now persists the fts_stale breadcrumb and detaches FTS (triggers down, same ordering contract as the deferred rebuild branch) instead of re-raising: a script that dies after CREATE can otherwise leave an empty-but-valid index that a later integrity-check would pass and stamp, freezing a silently-dead index. The next open routes through _recover_stale_fts. - Engine stamp gains a capability signature (|missing=<tables> when a probe returns None): an incapable host sweeps at most once per engine+capability pair, and a capable host reading its stamp sees the mismatch and re-verifies instead of trusting indexes the incapable host never checked. - The optional messages_fts_cjk index joins the corruption probe (probed first; absent installs untouched). - _legacy_fts_index_corrupt: the separate except sqlite3.OperationalError arm was unreachable (the DatabaseError arm catches the subclass); its no-such-table/no-such-module conditions moved into the single arm.
…d plugin-compat pointers
SessionDB._conn is Connection | None on current main and the FTS recovery lives in a mixin that never declares it, so the fallback rollback goes through the cursor's own connection and the regression tests read the live handle through a helper that asserts it is open.
The changed-path type check compares the replacement against the bound method it stands in for; an explicit self annotation makes the assignment exact.
ty rejects assigning a plain function to a class method attribute; every other patch in this file already goes through monkeypatch, which also restores the attribute after the test.
09b402b to
5426cf6
Compare
|
Closing: main already fails open on DEADBEEF trigram corruption and rebuilds on the next open ( |
What does this PR do?
When the SQLite FTS trigram index is malformed (e.g. from a legacy schema), the rebuild previously failed. This PR:
_init_fts(once per SQLite engine)fts_stalebreadcrumb and detaches FTS on failed recoveryRelated Issue
Fixes #86027
Type of Change
Changes Made
hermes_state_schema.py: Malformed FTS index recovery, narrowed error classification, engine-scoped integrity probe, stale breadcrumb, capability stamp.tests/state/test_legacy_fts_malformed_rebuild.py: Tests updated to use HEAD's_rebuild_fts_indexesAPI.How to Test
scripts/run_tests.sh tests/state/test_legacy_fts_malformed_rebuild.py— FTS recovery tests pass.Checklist