Skip to content

feat: backfill 等待/跳过守卫 — 适配 Codex 26.609,PR3#18

Merged
Wangnov merged 1 commit into
feat/multi-store-writefrom
feat/multi-store-backfill-guard
Jun 13, 2026
Merged

feat: backfill 等待/跳过守卫 — 适配 Codex 26.609,PR3#18
Wangnov merged 1 commit into
feat/multi-store-writefrom
feat/multi-store-backfill-guard

Conversation

@Wangnov

@Wangnov Wangnov commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Stacked on #17(base 指向 feat/multi-store-write)。#17 合并后会自动对到上一层 / main。

背景

PR1(#16,只读发现+多库 status)、PR2(#17,sync/bucket switch 写路径多库化)。Codex 26.609 启动时会跑一次 backfill(从 rollout 重建 state DB),进度记在 backfill_state 表(running/complete)。PR3 给多库写路径加 backfill 等待/跳过守卫,避免与重建竞态。共识见 #14

改动

  • wait_for_store_backfill(db, budget):只读轮询 backfill_state.status。无表(老 Codex)或 complete → 立即就绪;其它状态 → 等到 backfill_wait(默认 10s)后报 busy;读错按就绪(写阶段再暴露真错)。
  • busy 库标 StoreOutcome::Skipped,既不收集其 rollout 目标、也不写其 DB
  • 整轮跳过的关键安全规则:改写共享 rollout JSONL 会和正在跑的 backfill 读其事实源竞态(torn read)。因此只要 scope 会动 rollout(AllRows)且任一库 backfill 进行中,整轮全部 Skipped;--sqlite-only(RolloutScope::None,不碰 rollout)仍写其就绪库。
  • 退出码:部分更新+跳过 = Partial(2)、全跳过 = Failed(1);--sqlite-only 的 App 持久性告警改为仅当 App 库真被更新才打。

⚠️ 行为变化(请知悉)

更新 Codex 后那段 backfill 运行期间,完整 sync整轮跳过并提示稍后重跑(退出码 1),只有 --sqlite-only 会写非重建中的库。这是有意的数据安全取舍——不与 Codex 的重建抢同一批 rollout 文件。backfill 完成后 sync 恢复正常。

并行 Review(Codex × Claude subagent)

  • Codex 抓到一个 subagent 漏掉的真 🔴:第一版虽然"不读不写 busy 库",但仍会改写共享 rollout,而那正是 busy 库 backfill 正在读的事实源 → 竞态。
  • 已修复:上面的"整轮跳过"规则;新增回归测试 reconcile_all_stores_skips_whole_round_when_backfill_running(断言 backfill 运行时共享 rollout 与两库 DB 均不变)。
  • 两位的 🟡 也采纳:Partial 文案改"失败或跳过"、Skipped 文案覆盖"卡死/失败终态"、补 complete→Updated 与 sqlite-only-busy→Partial 测试。

测试 / 质量

  • cargo test:53 passed(新增/重写 3 个 backfill 守卫测试)。
  • cargo clippy --all-targets:0;cargo fmt --check:clean。

后续

watch 多库化(MismatchedRows 跨库语义)→ PR4;--store flag → 之后。

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Wangnov
Wangnov force-pushed the feat/multi-store-write branch from 078dd5f to bb50bfa Compare June 13, 2026 05:56
@Wangnov
Wangnov force-pushed the feat/multi-store-backfill-guard branch from 2aa959e to 7e18e57 Compare June 13, 2026 05:58
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov force-pushed the feat/multi-store-write branch from bb50bfa to abcee40 Compare June 13, 2026 06:11
@Wangnov
Wangnov force-pushed the feat/multi-store-backfill-guard branch from 7e18e57 to 030fd33 Compare June 13, 2026 06:12
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov force-pushed the feat/multi-store-write branch from abcee40 to fcffc5e Compare June 13, 2026 06:34
@Wangnov
Wangnov force-pushed the feat/multi-store-backfill-guard branch from 030fd33 to 61940f9 Compare June 13, 2026 06:34
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov force-pushed the feat/multi-store-write branch from fcffc5e to 64f5c8c Compare June 13, 2026 06:44
@Wangnov
Wangnov force-pushed the feat/multi-store-backfill-guard branch from 61940f9 to 5d6cce5 Compare June 13, 2026 06:45
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov force-pushed the feat/multi-store-write branch from 64f5c8c to 268baf2 Compare June 13, 2026 06:58
PR3 of the multi-store adaptation (builds on #17). Before writing, the
multi-store path now waits (bounded) for each store's Codex startup-backfill to
finish, so threadripper never races Codex's rebuild.

- wait_for_store_backfill polls backfill_state (read-only): no table or a
  `complete` status is ready immediately; otherwise it waits up to backfill_wait
  (default 10s) and then reports the store busy.
- A busy store is reported StoreOutcome::Skipped, and neither its rollout targets
  nor its DB are touched.
- Because rewriting the shared rollout JSONL would race a running backfill's
  reads (the rollout files are its source of truth), a rollout-rewriting scope
  (AllRows) combined with any in-progress backfill skips the whole round;
  --sqlite-only (RolloutScope::None) still writes its ready stores.
- main returns Partial(2) / Failed(1) accordingly; the --sqlite-only App warning
  now fires only when the App store was actually updated.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the first
cut still rewrote the shared rollout JSONL (racing the backfill) even while
skipping busy stores' DBs; fixed with the whole-round skip above, plus a
regression test asserting that a shared rollout and both DBs stay untouched while
a backfill runs. 53 tests, clippy and fmt clean.
@Wangnov
Wangnov force-pushed the feat/multi-store-backfill-guard branch from 5d6cce5 to 20e1a7e Compare June 13, 2026 06:59
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov merged commit 20e1a7e into feat/multi-store-write Jun 13, 2026
5 of 8 checks passed
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
Wangnov added a commit that referenced this pull request Jun 13, 2026
PR4 of the multi-store adaptation (builds on #18). `watch` now maintains every
discovered store, not just one, by sharing the multi-store write path.

- Extract reconcile_stores_core(.., backup, ..) used by both the one-shot
  backed-up path (reconcile_all_stores_with_backup) and a new no-backup
  reconcile_all_stores for the continuous watch service.
- The core supports the incremental MismatchedRows scope with an AllRows rollout
  followup (when rows change), matching the former single-store watch path.
- backup=false skips per-store backups AND the rollout change journal, so watch
  does not litter backups/ every poll (matching the old reconcile_once, which
  passed no journal path).
- watch passes Duration::ZERO as the backfill wait: a store mid-backfill is
  skipped this poll and retried next, never blocking the loop. With a
  rollout-rewriting scope, PR3's whole-round skip means watch quietly waits out a
  backfill and resumes when it completes.
- Remove the now-unused single-store path: reconcile_once, ReconcileSummary,
  print_sync_summary, add_rollout_summary.

Reviewed in parallel by Codex and a Claude subagent. Codex caught that the
no-backup path still wrote a rollout change journal (a regression vs the old
watch); fixed by gating the journal on backup, with a regression test asserting
the watch path writes no backups/rollouts.*.jsonl. 54 tests, clippy and fmt
clean.
@Wangnov
Wangnov deleted the feat/multi-store-backfill-guard branch June 13, 2026 08:15
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