fix: serialize concurrent FTS bootstrap repair - #486
100yenadmin wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0fd360804
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| owner_rebuild_needed = ( | ||
| _fts_needs_rebuild_structural(conn, spec) if winner_state_needs_repair else False | ||
| ) | ||
| if not owner_rebuild_needed and not structural_repair_needed and deep_repair_needed: |
There was a problem hiding this comment.
Recheck deep corruption when triggers are missing
When an FTS trigger is missing and updates have also caused same-row-count index drift, structural_repair_needed is true solely because of the trigger, so this condition prevents the owner from running _fts_needs_rebuild. The repair recreates the trigger and reports success with rebuilt=False, but the existing index remains corrupt and searches continue returning stale or missing results; explicit doctor repair should still perform the deep check after fixing trigger-only structural state.
Useful? React with 👍 / 👎.
|
Closing in favour of #505, the maintenance roll-up — this is commit 1 of that stack. Nothing is dropped — the commits are carried across unchanged, so the review history here stays meaningful and the work is not rewritten. The consolidation is packaging: a system-level change reads better as one ordered stack than as several PRs that have to be merged in the right sequence to make sense. Apologies for the churn on your queue. |
* fix: serialize concurrent FTS bootstrap repair Adopt PR #486 commit f0fd360 onto current main. Include only the FTS deep-repair correction from PR #505 commit 5af57d4 so missing triggers do not mask same-row-count corruption. Add transaction-boundary regressions required by the local acceptance packet. Refs #475 * fix: preserve due FTS checks during trigger repair
Summary
BEGIN IMMEDIATEownership transaction;spawnregressions with message conservation, token-level search, FTS5 integrity, shadow-table parity, clean reopen, and bounded lock-contention coverage.Why
Two independent processes could both observe an incomplete fresh-database FTS schema and then race through unconditional virtual-table creation. The loser failed startup with:
The existing WAL conversion retry happens earlier and does not own the later FTS repair boundary.
repair_external_content_fts()now separates cheap preflight detection from the destructive repair boundary:BEGIN IMMEDIATEwhen the connection does not already own a transaction;If SQLite reports lock/busy contention on the repair-needed path, the exception remains an availability failure. It is not classified as FTS corruption and does not authorize destructive repair. Classification uses SQLite
BUSY/LOCKEDresult codes plus genuine lock-message fallbacks; unrelated error text containingtimeoutorbusyis not treated as lock provenance.Validation
python -m pytest -q -p no:cacheprovider tests/test_db_bootstrap_fts.py->35 passedpytest tests/test_lcm_core.py tests/test_lcm_engine.py tests/test_packaging_install.py -q— exact grouped command not run;tests/test_lcm_core.pypassed292, and the release validator's broader focused gate passed.pytest -q— run;2311 passed, 1 skipped, 12 xfailed, 4 failedon the known macOS/varversus/private/varexternalization-path baseline.bash -lc 'ulimit -n 1024 && pytest -q'— exact template command not run; the release validator's low-FD gate produced the same2311 passed, 1 skipped, 12 xfailed, 4 failedbaseline-only result.python -m compileall -q .python -m py_compile scripts/import_lossless_claw.pybash -n scripts/install.sh scripts/update.shgit diff --checkscripts/validate_release.sh --full --keep-going --output /tmp/hermes-lcm-release-validation-475-lock-classifier-final-20260803-> all diff, compilation, shell, focused, benchmark, and stress gates passed; aggregate remained red only for the same ordinary/low-FD baseline failures above.python -m ruff check db_bootstrap.py tests/test_db_bootstrap_fts.py-> passed.f0fd360804afbe6aeddb2bc12f6c86d0a2b2f165: workflow lint, lint, and Python 3.11–3.14 passed.actionlint— not applicable; this PR changes no workflow files.Spawned-process acceptance
The candidate regression uses six independent
spawnworkers. Each child first observes the structurally incomplete fresh FTS state, then a second barrier releases all six from that same stale observation. This deterministically drives the pre-fix implementation into competing virtual-table creation while allowing the candidate's post-BEGIN IMMEDIATErecheck to select one owner.The regression checks:
messages_fts_docsizeparity;PRAGMA integrity_checkandforeign_key_check;MessageStorereopen.The focused suite separately checks bounded contention returning
OperationalError('database is locked')without partial FTS artifacts.Trigger-disappearance ownership acceptance
A second real SQLite connection drops one canonical product trigger after the initial healthy/deep precheck. SQLite tracing on the repair connection records whether each observed
CREATE TRIGGERstatement executes inside a transaction.The healthy fast path now returns without executing trigger DDL only when the second trigger check remains complete. An observed disappearance falls through to
BEGIN IMMEDIATE, complete-state reinspection, and owner-only trigger recreation.A separate preserved 12-process/fork reconnaissance probe also passed constructor startup, but it is not cited as the six-process regression and is not included in this PR.
Notes
SQLITE_BUSY_TIMEOUT_MSownership budget applies only after repair-worthy state is observed.docsizeparity, and canonical trigger names; it does not compare existing trigger bodies.Closes #475.
Refs #475