feat(ha-term-safety): drive raft_log_append on primary append path (task 3.1)

This commit is contained in:
Rongsong Shen 2026-06-22 08:09:47 +00:00
parent 3dd8493fff
commit 6a519a928b
5 changed files with 299 additions and 11 deletions

View file

@ -1866,6 +1866,15 @@ void ha_cb_commit_advanced_for_test(ha_state_t *ha, uint64_t commit_index)
{
s_real_cbs.on_commit_advanced(ha, commit_index);
}
/* ha-term-safety task 3.1: drive the (static) own-term gate arm — the WAL_NOOP
* append now routed through ha_primary_log_append — so a unit test can assert
* the promotion no-op records the live term via the library. Exposed only under
* XCE_TESTING. */
void ha_arm_own_term_gate_for_test(cluster_t *c)
{
ha_arm_own_term_gate(c);
}
#endif
/* ha_check_heartbeat_nocluster() / ha_watchdog_thread() / ha_start_watchdog() /
@ -2645,6 +2654,89 @@ static void ha_register_persist_hooks(cluster_t *c)
c->ha.hooks_registered = 1;
}
/*
* ha_primary_log_append (ha-term-safety task 3.1, R1.1/R1.2/R2.1): the SINGLE
* primary append entry point. Routes a local primary append (job-state entry or
* the own-term promotion no-op) through the raft library's raft_log_append, so
* the LIBRARY stamps the live election term (s->term) and assigns the LSN
* (s->log_len+1), then drives on_persist_entry — which (primary role) performs
* the actual WAL write via persist_wal_async. This makes the library index the
* authority for both the per-entry term (R1) and the LSN, retiring the
* hand-rolled bypass that assigned the LSN directly through persist_wal_async.
*
* Co-tracking invariant (closes the task 2.1 deferral): because raft_log_append
* is the only primary append path, every primary append advances BOTH the
* library index (s->log_len) and the persist write cursor (p->current_lsn) by
* exactly one, in lockstep — so p->current_lsn == s->log_len + 1 always holds and
* the LSN the library assigns equals the LSN on_persist_entry writes. raft_init
* (task 2.5) seeds s->log_len from the recovered durable floor, which equals
* p->current_lsn - 1 at boot, establishing the invariant before the first append.
*
* Persist-before-act: raft_log_append calls on_persist_entry BEFORE advancing
* s->log_len and propagates a non-zero persist rc without mutating the log (no
* phantom entry) — preserving persist-before-ACK and the bug-850 contiguity
* guard beneath the WAL write.
*
* Legacy fallback (non-raft road): when the raft library is not in RAFT_LEADER
* state (raft_log_append returns -EPERM) — the init-time non-raft promotion road
* (ha_promote_to_primary) and engine-less unit-test contexts — the library is
* not tracking the log, so there is no co-tracking hazard. We append locally
* through the legacy persist_wal_async path (which still records the live term
* via the persist_set_term seam set on the prior promotion) so those roads keep
* working unchanged. Once raft drives leadership in production, the -EPERM
* branch is never taken and the library is the sole LSN/term source.
*
* out_lsn (optional) receives the assigned 1-based LSN on success.
* Returns 0 on success; propagates a non-zero persist rc on append failure.
*/
int ha_primary_log_append(cluster_t *c, uint32_t type,
const void *payload, uint32_t len, uint64_t *out_lsn)
{
if (!c)
return -EINVAL;
/* Pack the in-flight adapter envelope [raft_entry_blob_t{type,len}][payload]
* — the SAME format on_persist_entry (ha_cb_persist_entry_stub) unpacks, so
* the encoder and decoder share the single wal_types.h definition. */
size_t blob_len = sizeof(raft_entry_blob_t) + len;
uint8_t stackbuf[256];
uint8_t *blob = (blob_len <= sizeof(stackbuf)) ? stackbuf : malloc(blob_len);
if (!blob)
return -ENOMEM;
raft_entry_blob_t *bh = (raft_entry_blob_t *)blob;
bh->type = type;
bh->payload_len = len;
if (payload && len > 0)
memcpy(blob + sizeof(*bh), payload, len);
uint64_t idx = 0;
int rc = raft_log_append(&c->ha.raft, blob, blob_len, &idx);
if (rc == -EPERM) {
/* Non-raft road: the library is not LEADER and is not tracking the log.
* Append locally through the legacy path (no library index to co-track).
* Stamp the live election term (ha->term) so the entry still records the
* leader's term (R1.1) even off the raft road — matching on_persist_entry's
* persist_set_term on the raft path. */
persist_set_term(&c->persist,
atomic_load_explicit(&c->ha.term, memory_order_acquire));
rc = persist_wal_async(&c->persist, type, payload, len);
if (rc == 0) {
pthread_mutex_lock(&c->persist.lock);
idx = c->persist.current_lsn - 1; /* the LSN just assigned */
pthread_mutex_unlock(&c->persist.lock);
}
}
if (blob != stackbuf)
free(blob);
if (rc == 0 && out_lsn)
*out_lsn = idx;
return rc;
}
/*
* ha_arm_own_term_gate: append the own-term WAL_NOOP and arm the commit
* engine's writable_lsn gate (ha-single-leader-role task 2.2, Req 1.4/2.1/2.2).
@ -2670,7 +2762,13 @@ static void ha_register_persist_hooks(cluster_t *c)
*/
static void ha_arm_own_term_gate(cluster_t *c)
{
int rc = persist_wal_async(&c->persist, WAL_NOOP, NULL, 0);
/* ha-term-safety task 3.1: route the own-term promotion no-op through the
* library (ha_primary_log_append → raft_log_append) so the WAL_NOOP records
* the newly established term (R1.2) and the library assigns its LSN. On the
* non-raft promotion road this falls back to the legacy append; either way
* the returned LSN is the no-op's WAL LSN. */
uint64_t noop_lsn = 0;
int rc = ha_primary_log_append(c, WAL_NOOP, NULL, 0, &noop_lsn);
if (rc < 0) {
ERROR("HA: own-term WAL_NOOP append failed: %d", rc);
WARN("HA: own-term gate left UNSET (degraded-but-safe); first "
@ -2678,13 +2776,6 @@ static void ha_arm_own_term_gate(cluster_t *c)
return;
}
/* Derive the no-op's LSN: the append post-incremented current_lsn, so the
* entry just written took current_lsn - 1 (read under p->lock). */
uint64_t noop_lsn;
pthread_mutex_lock(&c->persist.lock);
noop_lsn = c->persist.current_lsn - 1;
pthread_mutex_unlock(&c->persist.lock);
/* Observability-only bookkeeping (lock-free, non-blocking, records
* nothing per entry — commit state derives from registry watermarks). */
(void)commit_submit(&c->commit, noop_lsn, COMMIT_MODE_RETAIN, 0);