forked from lei.chen/myxce
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:
parent
f447ad4183
commit
c3201a2b1e
7 changed files with 883 additions and 21 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue