From 7c27ff1954033670f4118596aadb6eef48b26089 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 28 May 2026 13:21:36 +0000 Subject: [PATCH] feat(ha-config-bootstrap): implement ha_restore_term() for ha-term.bin read path (task 6.2) --- .kiro/specs/ha-config-bootstrap/tasks.md | 2 +- src/controller/ha.c | 72 ++++++++++++++++++++++++ src/controller/ha.h | 18 ++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/ha-config-bootstrap/tasks.md b/.kiro/specs/ha-config-bootstrap/tasks.md index f37a9a1d..8a7a7c54 100644 --- a/.kiro/specs/ha-config-bootstrap/tasks.md +++ b/.kiro/specs/ha-config-bootstrap/tasks.md @@ -140,7 +140,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A); - `make test` fails - _Requirements: 9.1, 9.2, 9.3_ -- [ ] 6.2 Implement ha_init() restore path for ha-term.bin +- [x] 6.2 Implement ha_init() restore path for ha-term.bin - Read `{workdir}/ha-term.bin` in `ha_init()` at startup; verify magic and CRC32; on match → restore `ha.term` and `ha.voted_for_*`; on missing → term=0 (3rd boot); on CRC mismatch → log WARN, initialize to term=0 - `ha_persist_term()` already implemented in task 4.2; this task only adds the read-path to `ha_init()` - `make test` GREEN on all durability tests diff --git a/src/controller/ha.c b/src/controller/ha.c index f64579d2..b1bcb57a 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -720,6 +720,78 @@ void ha_persist_term(ha_state_t *ha, const char *workdir) rename(tmp, path); } +/* + * ha_restore_term: read {workdir}/ha-term.bin at startup and restore + * ha->term and ha->voted_for_* into ha_state_t. + * + * On missing file (ENOENT): no-op — term stays 0. Req 9.3. + * On CRC/magic mismatch: log WARN, zero ha->term and ha->voted_for_*. Req 9.2. + * On valid file: atomically restore ha->term; copy voted_for fields. Req 9.2. + * + * Returns 0 on success (including gracefully-handled file-not-found and CRC bad), + * -errno on unexpected I/O error. + * If workdir is NULL, function is a no-op (safe for unit tests without a workdir). + */ +int ha_restore_term(ha_state_t *ha, const char *workdir) +{ + if (!workdir) + return 0; + + char path[1024]; + snprintf(path, sizeof(path), "%s/ha-term.bin", workdir); + + int fd = open(path, O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) + return 0; /* first boot — no file yet, term stays 0 (Req 9.3) */ + return -errno; + } + + ha_term_file_t f; + memset(&f, 0, sizeof(f)); + ssize_t n = read(fd, &f, sizeof(f)); + close(fd); + + if (n != (ssize_t)sizeof(f)) { + WARN("HA: ha-term.bin short read (%zd/%zu) — ignoring, starting at term 0", + n, sizeof(f)); + atomic_store_explicit(&ha->term, 0ULL, memory_order_release); + ha->voted_for_term = 0; + memset(ha->voted_for_name, 0, sizeof(ha->voted_for_name)); + return 0; + } + + /* Verify magic */ + if (f.magic != HA_TERM_MAGIC) { + WARN("HA: ha-term.bin bad magic (0x%016" PRIx64 ") — ignoring, starting at term 0", + f.magic); + atomic_store_explicit(&ha->term, 0ULL, memory_order_release); + ha->voted_for_term = 0; + memset(ha->voted_for_name, 0, sizeof(ha->voted_for_name)); + return 0; + } + + /* Verify CRC32 */ + size_t crc_len = offsetof(ha_term_file_t, crc32); + uint32_t computed = (uint32_t)crc32(0L, (const Bytef *)&f, (uInt)crc_len); + if (computed != f.crc32) { + WARN("HA: ha-term.bin CRC mismatch (expected 0x%08x got 0x%08x) — ignoring, starting at term 0", + computed, f.crc32); + atomic_store_explicit(&ha->term, 0ULL, memory_order_release); + ha->voted_for_term = 0; + memset(ha->voted_for_name, 0, sizeof(ha->voted_for_name)); + return 0; + } + + /* Valid file — restore state (Req 9.2) */ + atomic_store_explicit(&ha->term, f.term, memory_order_release); + ha->voted_for_term = f.voted_for_term; + strncpy(ha->voted_for_name, f.voted_for_name, sizeof(ha->voted_for_name) - 1); + ha->voted_for_name[sizeof(ha->voted_for_name) - 1] = '\0'; + + return 0; +} + /* ── Vote Handler (Req 2.2–2.6, 9.1) ─────────────────────────────────────── * * handle_request_vote: process an incoming OP_requestVote RPC. diff --git a/src/controller/ha.h b/src/controller/ha.h index 73660767..ffc0d5f4 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -294,6 +294,24 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode); */ void ha_persist_term(ha_state_t *ha, const char *workdir); +/* + * ha_restore_term: read {workdir}/ha-term.bin at startup and restore + * ha->term and ha->voted_for_* into ha_state_t. + * + * On missing file (ENOENT): no-op — term stays 0, voted_for_name stays empty. + * Returns 0 (safe first-boot path). Req 9.3. + * On CRC/magic mismatch: log WARN, zero ha->term and ha->voted_for_* for + * safety and return 0. Req 9.2. + * On valid file: atomically restore ha->term; copy voted_for_term + * and voted_for_name. Returns 0. Req 9.2. + * On unexpected I/O error: return -errno. + * + * Must be called after ha_init() and before any election or vote processing. + * If workdir is NULL, the function is a no-op (safe for unit tests). + * Requirements: 9.1, 9.2, 9.3. + */ +int ha_restore_term(ha_state_t *ha, const char *workdir); + /* * handle_request_vote: process an incoming OP_requestVote RPC. * Applies term/voted_for/freshness gate; persists state via ha_persist_term().