feat(ha-term-safety): append config markers via unified log + derive committed config (task 4.2)

Wires do_upgrade_config_with_bytes to append WAL_CONFIG_MARKER through
raft_log_append (live term-stamped consensus record), demotes committed_config_tag
to a non-authoritative cache, and derives the committed config from the latest
committed marker. Preserves the derive across checkpoint compaction via an optional,
backward-compatible config-ref tail in the snapshot sentinel.
This commit is contained in:
Rongsong Shen 2026-06-22 10:34:10 +00:00
parent f447ad4183
commit c3201a2b1e
7 changed files with 883 additions and 21 deletions

View file

@ -1009,6 +1009,19 @@ pthread_t persist_test_last_fdatasync_thread(persist_t* p)
return p->test_last_fdatasync_thread;
}
/* Force a fresh WAL segment at the current LSN (test seam — see persist.h).
* Takes p->lock for the same single-writer serialization the append path uses,
* so the segment swap cannot race a concurrent write. */
int persist_test_force_new_segment(persist_t* p)
{
if (!p)
return -EINVAL;
pthread_mutex_lock(&p->lock);
int rc = rotate_segment(p);
pthread_mutex_unlock(&p->lock);
return rc;
}
/* ------------------------------------------------------------------ *
* Durability-verification test seam (ha-commit-durability Req 3.2/3.3)*
* *
@ -1208,10 +1221,18 @@ static int write_snapshot(persist_t* p)
if (fd < 0)
return -errno;
/* Sentinel header: magic (8 bytes) + checkpoint_lsn (8 bytes) */
char header[16];
/* Sentinel: magic (8) + checkpoint_lsn (8) + committed-config reference
* (1 present + 8 tag + 32 sha = 41 bytes; ha-term-safety task 4.2 r1).
* The config-ref tail is OPTIONAL on read-back, so an older 16-byte
* sentinel still loads (load_snapshot reads the tail best-effort). The
* tag is little-endian on this arch, matching the checkpoint_lsn field. */
char header[16 + 1 + 8 + 32];
memset(header, 0, sizeof(header));
memcpy(header, "XCESNAP\0", 8);
memcpy(header + 8, &p->checkpoint_lsn, 8);
header[16] = (char)(p->cfgref_present ? 1 : 0);
memcpy(header + 17, &p->cfgref_tag, 8);
memcpy(header + 25, p->cfgref_sha, 32);
ssize_t nw = write(fd, header, sizeof(header));
fdatasync(fd);
@ -1230,6 +1251,83 @@ static int write_snapshot(persist_t* p)
return 0;
}
/* scan_latest_committed_config_ref (ha-term-safety task 4.2 remediation r1):
* walk the local WAL segments and capture the HIGHEST-LSN WAL_CONFIG_MARKER
* whose LSN is <= committed_lsn into p->cfgref_{present,tag,sha}. This is the
* SAME committed-prefix marker config_committed_marker derives, cached durably
* so a later checkpoint can trim the marker's segment without losing the
* derived committed-config reference (compaction safety). The marker payload is
* a fixed 40-byte reference (8-byte big-endian version_tag + 32-byte sha256 —
* mirrors config_marker_encode; the config BYTES are never in the WAL), decoded
* inline here so persist stays free of a config_repl layering dependency. */
static void scan_latest_committed_config_ref(persist_t* p)
{
uint64_t committed = atomic_load_explicit(&p->committed_lsn,
memory_order_acquire);
if (committed == 0)
return; /* nothing committed → keep any prior cached ref unchanged */
DIR* d = opendir(p->wal_path);
if (!d)
return;
uint64_t seg_lsns[4096];
int nseg = 0;
struct dirent* e;
while ((e = readdir(d)) != NULL &&
nseg < (int)(sizeof(seg_lsns) / sizeof(seg_lsns[0]))) {
uint64_t sl = 0;
if (sscanf(e->d_name, "seg-%016" SCNx64 ".wal", &sl) == 1)
seg_lsns[nseg++] = sl;
}
closedir(d);
if (nseg == 0)
return;
for (int i = 1; i < nseg; i++) {
uint64_t key = seg_lsns[i];
int j = i - 1;
while (j >= 0 && seg_lsns[j] > key) { seg_lsns[j + 1] = seg_lsns[j]; j--; }
seg_lsns[j + 1] = key;
}
for (int si = 0; si < nseg; si++) {
char seg_path[4096];
snprintf(seg_path, sizeof(seg_path), WAL_SEG_FMT, p->wal_path,
seg_lsns[si]);
int fd = open(seg_path, O_RDONLY);
if (fd < 0)
continue;
uint32_t off = 0;
for (;;) {
wal_entry_hdr_t hdr;
ssize_t n = pread(fd, &hdr, sizeof(hdr), (off_t)off);
if (n < (ssize_t)sizeof(hdr)) break;
if (hdr.lsn == 0 && hdr.type == 0 && hdr.payload_len == 0) break;
if (hdr.lsn > committed) break; /* past committed prefix */
if (hdr.type == WAL_CONFIG_MARKER &&
hdr.payload_len >= WAL_CONFIG_MARKER_PAYLOAD_LEN) {
uint8_t buf[WAL_CONFIG_MARKER_PAYLOAD_LEN];
ssize_t pr = pread(fd, buf, sizeof(buf),
(off_t)(off + sizeof(hdr)));
if (pr == (ssize_t)sizeof(buf)) {
p->cfgref_tag =
((uint64_t)buf[0] << 56) | ((uint64_t)buf[1] << 48) |
((uint64_t)buf[2] << 40) | ((uint64_t)buf[3] << 32) |
((uint64_t)buf[4] << 24) | ((uint64_t)buf[5] << 16) |
((uint64_t)buf[6] << 8) | ((uint64_t)buf[7]);
memcpy(p->cfgref_sha, buf + 8, 32);
p->cfgref_present = 1; /* later markers overwrite — latest wins */
}
}
off += (uint32_t)(sizeof(hdr) + hdr.payload_len);
}
close(fd);
}
}
/* Delete WAL segment files whose start LSN is strictly less than the
* currently-open segment's start LSN. We parse the active segment
* filename (seg_path) to get its start LSN and never delete that file,
@ -1385,8 +1483,14 @@ int persist_checkpoint(persist_t* p)
p->checkpoint_lsn = p->current_lsn;
pthread_mutex_unlock(&p->lock);
/* 3. Serialize in-memory state to a snapshot file
* (P0: no live kvmgrs — writes sentinel header only) */
/* 3a. Capture the latest committed config-marker reference BEFORE trimming
* (ha-term-safety task 4.2 r1): the trim below can unlink the segment
* holding a committed WAL_CONFIG_MARKER, so the derived committed config
* must be cached durably (into the sentinel) before that happens. */
scan_latest_committed_config_ref(p);
/* 3b. Serialize in-memory state to a snapshot file
* (P0: no live kvmgrs — writes sentinel header + cached config ref) */
rc = write_snapshot(p);
if (rc != 0)
goto done;
@ -1417,7 +1521,8 @@ static int load_snapshot(persist_t* p, const char* path, uint64_t lsn)
if (fd < 0)
return -errno;
char header[16];
/* magic(8) + checkpoint_lsn(8) + optional config-ref tail(1+8+32). */
char header[16 + 1 + 8 + 32];
ssize_t n = read(fd, header, sizeof(header));
close(fd);
@ -1433,6 +1538,17 @@ static int load_snapshot(persist_t* p, const char* path, uint64_t lsn)
p->checkpoint_lsn = stored_lsn;
p->current_lsn = stored_lsn;
/* Optional committed-config reference (ha-term-safety task 4.2 r1): present
* only when the sentinel carries the 41-byte tail. An older 16-byte sentinel
* (n == 16) leaves the cached ref untouched (cfgref_present stays 0 from
* persist_init), so a member that never checkpointed a config marker simply
* has no fallback reference — the live WAL scan is authoritative. */
if (n >= 16 + 1 + 8 + 32 && header[16] != 0) {
p->cfgref_present = 1;
memcpy(&p->cfgref_tag, header + 17, 8);
memcpy(p->cfgref_sha, header + 25, 32);
}
return 0;
}
@ -1541,6 +1657,18 @@ int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn,
* replay), not here — recognised explicitly rather than via default. */
break;
case WAL_CONFIG_MARKER:
/* Config-change marker (ha-term-safety task 4.2): a {version_tag,
* sha256} reference, NEVER the config bytes (R7.4). It carries NO
* entity state, so replay performs no restore here — the committed
* configuration is DERIVED from the latest committed marker on demand
* (config_committed_marker), and the bytes are obtained + hash-verified
* out-of-band before activation (task 4.3). Recognised explicitly so
* the per-LSN term index (built above this switch's caller) records its
* term and on_get_entry_term / (term,lsn) reconcile treat it as an
* ordinary log entry. */
break;
default:
break;
}
@ -2223,6 +2351,14 @@ wal_commit_mode_t wal_classify(uint32_t wal_type, int detail)
* retained until committed (ha-commit-barrier-rollout pre-declared
* row: promotion | internal | RETAIN; guard forces classification) */
return WAL_MODE_RETAIN;
case WAL_CONFIG_MARKER:
/* config-change marker (ha-term-safety task 4.2): a committable
* (term,lsn) log entry like a job-state fact — replicated and retained
* until committed; it commits through the SAME barrier as job state
* (R7.1/R7.11), so it is RETAIN, NOT a no-op or a LOCAL marker. The
* requester barrier on a config upgrade is owned by the upgrade path
* (do_upgrade_config_with_bytes), not the per-entry WAIT hook. */
return WAL_MODE_RETAIN;
case WAL_CHECKPOINT:
/* local segment marker — never replicated, by design */
return WAL_MODE_LOCAL;