fix(controller): resume at checkpoint+1 after loading a snapshot (bug-1870 follow-up)

Regression from 63fd3d2a (bug-1870), found by the --config-upgrade HA soak
(force-elect: "submit on {ctrl-2,ctrl-1} failed", ~640 standby NACKs;
bisected: 725301dd passes, 88c4310e fails).

load_snapshot() seeded current_lsn = checkpoint_lsn, but current_lsn is
the NEXT lsn to write and the snapshot covers 1..checkpoint_lsn. The
off-by-one was always there, hidden because every checkpoint also wrote a
WAL_CHECKPOINT marker ABOVE the checkpoint, so the WAL scan moved
current_lsn past it. With the marker gone, a standby whose WAL held
nothing above its config-marker checkpoint (LSN 11) recovered with
current_lsn=11: its raft layer counted entry 11 as held (ae_already_have,
not written), and persist_wal_write_replicated() then refused entry 12 as
a gap (ae_append_persist_fail) on every resend -- stuck at 11 forever.

Both of load_snapshot's commit points now set current_lsn =
checkpoint_lsn + 1, matching the apply-side catch-up path (lsn + 1).

New test_recover_from_snapshot_without_wal_resumes_after_it (RED before:
current_lsn 3, not 4). Two snapshot-entity tests asserted the old
current_lsn == checkpoint_lsn and now expect +1. Controller suite 1245/0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-27 06:47:30 +00:00
parent 154a169ff3
commit cb1415d7c1
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
3 changed files with 69 additions and 6 deletions

View file

@ -1960,7 +1960,8 @@ static void test_snapent_load_snapshot_restores_entities_before_advancing_positi
* TOGETHER with the entities actually being present -- both checked
* above must be simultaneously true of the end state. */
TEST_ASSERT_EQUAL_UINT64(expected_lsn, dst->c->persist.checkpoint_lsn);
TEST_ASSERT_EQUAL_UINT64(expected_lsn, dst->c->persist.current_lsn);
/* current_lsn is the NEXT lsn to write (bug-1870 follow-up). */
TEST_ASSERT_EQUAL_UINT64(expected_lsn + 1, dst->c->persist.current_lsn);
snapent_teardown(dst);
snapent_teardown(src);
@ -2265,10 +2266,11 @@ static void test_snapent_load_snapshot_legacy_file_reports_position_only(void)
dst->c->persist.checkpoint_lsn,
"a legacy checkpoint still records the committed position it was "
"taken at -- existing, preserved behavior");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAPENT_LEGACY_CHECKPOINT_LSN,
TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAPENT_LEGACY_CHECKPOINT_LSN + 1,
dst->c->persist.current_lsn,
"a legacy checkpoint still advances current_lsn to its recorded "
"position -- existing, preserved behavior");
"a legacy checkpoint still advances current_lsn past its recorded "
"position (current_lsn is the next lsn to write, bug-1870 "
"follow-up)");
snapent_teardown(dst);
printf("test_snapent_load_snapshot_legacy_file_reports_position_only: PASS\n");