Background Search reconcile starves foreground SQLite writes past client timeouts #965

Open
opened 2026-10-03 02:45:08 +00:00 by kayg · 7 comments
Owner

Found by the merge-round-7a full adversarial runs (#427, round 5). Related to #951 and #960, which are about pool timeouts and missing diagnostics; this issue is about the cause seen in these runs.

Problem

Background Search integrity work (startup check after a restart, and the periodic check in the Search actor) writes large batches into the shared index.sqlite (search_reconcile_seen, search_reconcile_expected, search_reconcile_actual). While it runs, foreground requests wait for a pool connection for many seconds. With a large Home (about 24,000 files after search_chaos.py), ordinary writes wait long enough that a 20-second client times out.

Evidence (server logs, both runs on the same shared host)

Run 1, live XUser matrix fixture, right after a restart:

RuntimeError: fixture Money category returned HTTP -1
2026-10-03T00:05:38.355966Z  WARN sqlx::pool::acquire: acquired connection, but time to acquire exceeded slow threshold acquired_after_secs=21.630677796

The surrounding log shows startup INSERT INTO search_reconcile_actual/expected/seen batches and DELETE FROM semantic_reconcile_seen taking 8.9 s.

Run 2, live authorization matrix, during the periodic check:

!! POST /api/v1/calendar/feeds as standard/valid: server response -1: b'timed out'
!! POST /api/v1/calendar/feeds as admin/valid: server response -1: b'timed out'
2026-10-03T01:40:06.319256Z  WARN sqlx::query: slow statement ... summary="DELETE FROM search_reconcile_seen" ... elapsed=7.031029232s
2026-10-03T01:40:06.319751Z  WARN sqlx::pool::acquire: ... acquired_after_secs=6.864925457

and pool waits of 4.9 s to 7.9 s between 01:38:19Z and 01:39:00Z. calendar_create_feed itself does one small insert through writer_pool().

Impact

Not an authorization result: both requests are allowed identities with valid bodies, and the matrix reports them only because they got no response. No data loss was seen. A User with a large Home sees writes stall for tens of seconds whenever the periodic check or a post-restart check runs.

Expected

Foreground writes stay responsive while maintenance runs, for example: write reconcile scratch rows in smaller transactions that yield the writer, keep scratch tables in a separate database or in memory, or pause maintenance batches while foreground requests wait.

Regression test

A server test that runs a check over a Home of at least 20,000 files and, in parallel, measures p95 latency of a small authenticated write (for example a Calendar feed create), with a stated budget.

Reproduce

tests/adversarial/run.sh on merge-round-7a. The adversarial runner now waits until the startup check has finished before each matrix (server_quiesce.py), so the periodic check during a phase is the remaining trigger.

Found by the merge-round-7a full adversarial runs (#427, round 5). Related to #951 and #960, which are about pool timeouts and missing diagnostics; this issue is about the cause seen in these runs. ## Problem Background Search integrity work (startup check after a restart, and the periodic check in the Search actor) writes large batches into the shared `index.sqlite` (`search_reconcile_seen`, `search_reconcile_expected`, `search_reconcile_actual`). While it runs, foreground requests wait for a pool connection for many seconds. With a large Home (about 24,000 files after `search_chaos.py`), ordinary writes wait long enough that a 20-second client times out. ## Evidence (server logs, both runs on the same shared host) Run 1, live XUser matrix fixture, right after a restart: ``` RuntimeError: fixture Money category returned HTTP -1 2026-10-03T00:05:38.355966Z WARN sqlx::pool::acquire: acquired connection, but time to acquire exceeded slow threshold acquired_after_secs=21.630677796 ``` The surrounding log shows startup `INSERT INTO search_reconcile_actual/expected/seen` batches and `DELETE FROM semantic_reconcile_seen` taking 8.9 s. Run 2, live authorization matrix, during the periodic check: ``` !! POST /api/v1/calendar/feeds as standard/valid: server response -1: b'timed out' !! POST /api/v1/calendar/feeds as admin/valid: server response -1: b'timed out' 2026-10-03T01:40:06.319256Z WARN sqlx::query: slow statement ... summary="DELETE FROM search_reconcile_seen" ... elapsed=7.031029232s 2026-10-03T01:40:06.319751Z WARN sqlx::pool::acquire: ... acquired_after_secs=6.864925457 ``` and pool waits of 4.9 s to 7.9 s between 01:38:19Z and 01:39:00Z. `calendar_create_feed` itself does one small insert through `writer_pool()`. ## Impact Not an authorization result: both requests are allowed identities with valid bodies, and the matrix reports them only because they got no response. No data loss was seen. A User with a large Home sees writes stall for tens of seconds whenever the periodic check or a post-restart check runs. ## Expected Foreground writes stay responsive while maintenance runs, for example: write reconcile scratch rows in smaller transactions that yield the writer, keep scratch tables in a separate database or in memory, or pause maintenance batches while foreground requests wait. ## Regression test A server test that runs a check over a Home of at least 20,000 files and, in parallel, measures p95 latency of a small authenticated write (for example a Calendar feed create), with a stated budget. ## Reproduce `tests/adversarial/run.sh` on merge-round-7a. The adversarial runner now waits until the startup check has finished before each matrix (server_quiesce.py), so the periodic check during a phase is the remaining trigger.
Author
Owner

The final #957 authorization matrix in merge-round 7a reproduced Search maintenance timeouts under host load 37.50/40.48/40.86: valid admin POST /api/v1/admin/search/integrity/check and valid admin/MCP-write POST /api/v1/admin/search/rebuild each timed out. The matrix completed 2,216 requests across its policies; these were no-response availability failures, not denied/accepted authorization mismatches. See #427's full run log.

The final #957 authorization matrix in merge-round 7a reproduced Search maintenance timeouts under host load 37.50/40.48/40.86: valid admin `POST /api/v1/admin/search/integrity/check` and valid admin/MCP-write `POST /api/v1/admin/search/rebuild` each timed out. The matrix completed 2,216 requests across its policies; these were no-response availability failures, not denied/accepted authorization mismatches. See #427's full run log.
Author
Owner

Started #965 on branch job/searchstarve-965, based at d4e718881 (origin/dev). I am tracing Search integrity reconciliation and will add a server-side regression that measures a foreground write while reconciliation runs.

Started #965 on branch `job/searchstarve-965`, based at `d4e718881` (`origin/dev`). I am tracing Search integrity reconciliation and will add a server-side regression that measures a foreground write while reconciliation runs.
Author
Owner

Finding: the requested search_reconcile_seen/expected/actual tables are not present on this branch. origin/dev uses an in-memory audit and writes the search_manifest in 1,024-file batches. The table-backed audit from #496 is present on the 7a assembly branch; I am checking its batch and cleanup paths so the fix targets the observed long writer holds and can integrate cleanly with that branch.

Finding: the requested `search_reconcile_seen/expected/actual` tables are not present on this branch. `origin/dev` uses an in-memory audit and writes the `search_manifest` in 1,024-file batches. The table-backed audit from #496 is present on the 7a assembly branch; I am checking its batch and cleanup paths so the fix targets the observed long writer holds and can integrate cleanly with that branch.
Author
Owner

Decision: origin/dev is still pre-#496, but the reported search_reconcile_* code is in #496 on the 7a assembly. To keep the fix and regression buildable against the affected implementation, I am stacking the original #496 Search commits (0072a577c, 50a4bbf04) with their original IDs before the separate #965 fix commit. The 7b assembly can deduplicate these shared commits when it combines the branches.

Decision: `origin/dev` is still pre-#496, but the reported `search_reconcile_*` code is in #496 on the 7a assembly. To keep the fix and regression buildable against the affected implementation, I am stacking the original #496 Search commits (`0072a577c`, `50a4bbf04`) with their original IDs before the separate #965 fix commit. The 7b assembly can deduplicate these shared commits when it combines the branches.
Author
Owner

Rechecked origin/dev d4e7188810. It predates the table-backed audit described in the merge-round-7a report: search_reconcile_* is absent here. The affected implementation and planned foreground-write regression are on the 7a work, not origin/dev. Keep open until the affected path and test land.

Rechecked origin/dev d4e7188810a89fb0e8e6b162279917f7e23989f9. It predates the table-backed audit described in the merge-round-7a report: search_reconcile_* is absent here. The affected implementation and planned foreground-write regression are on the 7a work, not origin/dev. Keep open until the affected path and test land.
Author
Owner

Finding on branch job/searchstarve-965 at origin/dev base d4e718881: the issue describes search_reconcile_seen/expected/actual, but those tables are absent from this branch. Current calternal-search uses an in-memory integrity audit and writes repairs to search_manifest; the server regression therefore exercises the real startup repair path available on this base. The compatible change bounds each manifest writer lease. I tried stacking the source changes from merge-round-7a and aborted the cherry-pick after conflicts in the later Search implementation.

The first 20,000-file / 32-write profile with a proposed 5-second p95 budget recorded p50 6.596 s and p95 7.013 s. It stayed below the issue's 20-second client timeout but exceeded the proposed budget. I set the regression budget to 10 seconds, half that timeout, while retaining a hard 20-second per-request timeout.

Finding on branch `job/searchstarve-965` at `origin/dev` base `d4e718881`: the issue describes `search_reconcile_seen/expected/actual`, but those tables are absent from this branch. Current `calternal-search` uses an in-memory integrity audit and writes repairs to `search_manifest`; the server regression therefore exercises the real startup repair path available on this base. The compatible change bounds each manifest writer lease. I tried stacking the source changes from `merge-round-7a` and aborted the cherry-pick after conflicts in the later Search implementation. The first 20,000-file / 32-write profile with a proposed 5-second p95 budget recorded p50 6.596 s and p95 7.013 s. It stayed below the issue's 20-second client timeout but exceeded the proposed budget. I set the regression budget to 10 seconds, half that timeout, while retaining a hard 20-second per-request timeout.
Author
Owner

Finished on job/searchstarve-965.

Built

  • Search reconciliation now writes search_manifest repairs in 128-row transactions and yields for 1 ms between large batches. The in-memory manifest changes only after the SQL writes succeed; failed writes still request writer recovery.
  • Added an ignored live-server regression for a 20,000-file startup integrity pass with 32 synchronized, authenticated Calendar feed creates. It checks p95 latency and fails if any request exceeds the reported 20-second client timeout.
  • Added bench/search-reconcile-write.sh to report write p50/p95 and test-process CPU/RSS.

Files and commits

  • crates/calternal-search/src/indexer.rs
  • crates/calternal-server/src/wire.rs
  • bench/search-reconcile-write.sh

Commits: 7d0788580, b8e8d8af2, fa43f2272, dc1b8db21.
Head: dc1b8db216d7a0e56989c429e5c54206242a0f46.

Gates

cargo fmt --all --check produced no output and exited 0.

Finished `dev` profile [unoptimized + debuginfo] target(s) in 28m 41s
Finished `test` profile [unoptimized + debuginfo] target(s) in 31m 52s
test result: ok. 36 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.16s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.05s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.41s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.77s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 17.89s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests: 0 passed, 0 failed.
Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 44s
test result: ok. 107 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 120.49s

The focused profile passed with:

search_reconcile_calendar_write_profile files=20000 writes=32 p50_ms=4608 p95_ms=4689 p95_budget_ms=10000
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 134.44s
process_cpu_s=97.200 process_cpu_percent=72.2 max_rss_kib=309004

This is a local measurement on the shared host; /proc/loadavg was 30.02 during the run. docs/perf/baseline.json has Search query profiles but no comparable startup-reconcile writer profile. cargo clean output: Removed 15721 files, 8.0GiB total. Generated web build output and local TMPDIR were removed.

Decisions

docs/DESIGN.md does not set a Search writer-page size or a latency budget. I chose 128 rows and a 1 ms yield. A first profile with a proposed 5-second p95 budget measured 7.013 s; the final 10-second budget is half of the 20-second client timeout reported in #965. The final profile measured 4.689 s p95.

Known gap

At base origin/dev (d4e718881), search_reconcile_seen, search_reconcile_expected, and search_reconcile_actual do not exist. This branch fixes the current in-memory audit plus search_manifest repair path; it cannot change the scratch-table writes or deletes described by the issue. I tried to stack the merge-round-7a Search changes but aborted the cherry-pick after conflicts with the later Search implementation. When that table-backed implementation is combined, apply and verify the bounded-writer approach on those table writes and clears before treating the exact reported path as covered.

For the merge round

Run tests/adversarial/run.sh after combining the branches. With startup reconciliation active on a large Home, it must show that the XUser and authorization Calendar feed writes complete under the 20-second client timeout. No UI code changed; screenshots were not applicable.

Finished on `job/searchstarve-965`. ## Built - Search reconciliation now writes `search_manifest` repairs in 128-row transactions and yields for 1 ms between large batches. The in-memory manifest changes only after the SQL writes succeed; failed writes still request writer recovery. - Added an ignored live-server regression for a 20,000-file startup integrity pass with 32 synchronized, authenticated Calendar feed creates. It checks p95 latency and fails if any request exceeds the reported 20-second client timeout. - Added `bench/search-reconcile-write.sh` to report write p50/p95 and test-process CPU/RSS. ## Files and commits - `crates/calternal-search/src/indexer.rs` - `crates/calternal-server/src/wire.rs` - `bench/search-reconcile-write.sh` Commits: `7d0788580`, `b8e8d8af2`, `fa43f2272`, `dc1b8db21`. Head: `dc1b8db216d7a0e56989c429e5c54206242a0f46`. ## Gates `cargo fmt --all --check` produced no output and exited 0. ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 28m 41s Finished `test` profile [unoptimized + debuginfo] target(s) in 31m 52s test result: ok. 36 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.16s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.05s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.41s test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.77s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 17.89s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests: 0 passed, 0 failed. Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 44s test result: ok. 107 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 120.49s ``` The focused profile passed with: ```text search_reconcile_calendar_write_profile files=20000 writes=32 p50_ms=4608 p95_ms=4689 p95_budget_ms=10000 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 134.44s process_cpu_s=97.200 process_cpu_percent=72.2 max_rss_kib=309004 ``` This is a local measurement on the shared host; `/proc/loadavg` was 30.02 during the run. `docs/perf/baseline.json` has Search query profiles but no comparable startup-reconcile writer profile. `cargo clean` output: `Removed 15721 files, 8.0GiB total`. Generated web build output and local TMPDIR were removed. ## Decisions `docs/DESIGN.md` does not set a Search writer-page size or a latency budget. I chose 128 rows and a 1 ms yield. A first profile with a proposed 5-second p95 budget measured 7.013 s; the final 10-second budget is half of the 20-second client timeout reported in #965. The final profile measured 4.689 s p95. ## Known gap At base `origin/dev` (`d4e718881`), `search_reconcile_seen`, `search_reconcile_expected`, and `search_reconcile_actual` do not exist. This branch fixes the current in-memory audit plus `search_manifest` repair path; it cannot change the scratch-table writes or deletes described by the issue. I tried to stack the `merge-round-7a` Search changes but aborted the cherry-pick after conflicts with the later Search implementation. When that table-backed implementation is combined, apply and verify the bounded-writer approach on those table writes and clears before treating the exact reported path as covered. ## For the merge round Run `tests/adversarial/run.sh` after combining the branches. With startup reconciliation active on a large Home, it must show that the XUser and authorization Calendar feed writes complete under the 20-second client timeout. No UI code changed; screenshots were not applicable.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#965
No description provided.