forked from lei.chen/myxce
feat(ha-term-safety): restore controller build + wire vote/AE term wire fields (tasks 1.1, 1.2)
1.1: WalCatchupEntry gains term@4/prevLogIndex@5/prevLogTerm@6 (append-only). 1.2: RequestVote/PreVote gain lastLogTerm@4/lastLogIndex@5; cluster.c vote callers fixed to the extended library arity (RESTORES xce_controller + controller_unit_test compile+link, broken by the raft-commit-safety signature extension); ha.c emits the candidate's last-log tuple on send. 5 minimal stub log/commit callbacks wired into s_real_cbs so raft_init's new non-NULL requirement is satisfied — tasks 2.x replace them with real bodies; the logOk freshness gate is inert until the term seam (1.3) + on_get_entry_term (2.2). Pre-existing test_repl_unit.c wal_catchup -EAGAIN failures + ASAN leaks are an overlayfs /tmp fsync env artifact (bug-850), not from this task (untouched paths; append-only schema cannot affect the @3 entries-list decode). Build/baseline isolation is blocked by a c-capnproto flake-hash drift — see Implementation Notes.
This commit is contained in:
parent
e8e41752c0
commit
004a40bb3b
9 changed files with 264 additions and 9 deletions
|
|
@ -389,6 +389,11 @@ static int myreq_handler(vtask_t* task)
|
|||
uint64_t cand_tag = vrq ? vrq->config_tag : 0;
|
||||
uint64_t cand_lsn = vrq ? vrq->last_log_lsn : 0;
|
||||
uint64_t cand_term = vrq ? vrq->term : 0;
|
||||
/* ha-term-safety task 1.2: candidate's Raft (lastLogTerm, lastLogIndex)
|
||||
* tuple drives the library logOk freshness gate (Req 2.3/2.4/2.6),
|
||||
* distinct from the config_tag/last_log_lsn application gate above. */
|
||||
uint64_t cand_llt = vrq ? vrq->last_log_term : 0;
|
||||
uint64_t cand_lli = vrq ? vrq->last_log_index : 0;
|
||||
uint64_t local_tag = c->myconf->config_version_tag;
|
||||
/* Task 4.2 (ha-commit-durability): the voter floor's local-state LSN
|
||||
* input is the DURABLE WAL tip (persist_durable_floor), not the
|
||||
|
|
@ -428,7 +433,7 @@ static int myreq_handler(vtask_t* task)
|
|||
int granted = 0;
|
||||
if (freshness_ok && from_idx >= 0) {
|
||||
int r = raft_on_vote_request(&c->ha.raft, from_idx, cand_term,
|
||||
&reply_term);
|
||||
cand_llt, cand_lli, &reply_term);
|
||||
granted = (r == RAFT_GRANT) ? 1 : 0;
|
||||
} else {
|
||||
/* Freshness deny or unknown candidate: read current term for reply */
|
||||
|
|
@ -479,6 +484,10 @@ static int myreq_handler(vtask_t* task)
|
|||
uint64_t cand_tag = vrq ? vrq->config_tag : 0;
|
||||
uint64_t cand_lsn = vrq ? vrq->last_log_lsn : 0;
|
||||
uint64_t cand_term = vrq ? vrq->term : 0;
|
||||
/* ha-term-safety task 1.2: candidate's Raft (lastLogTerm, lastLogIndex)
|
||||
* tuple drives the SHARED library logOk gate (identical to real-vote). */
|
||||
uint64_t cand_llt = vrq ? vrq->last_log_term : 0;
|
||||
uint64_t cand_lli = vrq ? vrq->last_log_index : 0;
|
||||
uint64_t local_tag = c->myconf->config_version_tag;
|
||||
/* Task 4.2 (ha-commit-durability): voter-floor local-state LSN is the
|
||||
* DURABLE WAL tip (persist_durable_floor), not the volatile applied_lsn
|
||||
|
|
@ -521,7 +530,8 @@ static int myreq_handler(vtask_t* task)
|
|||
} else {
|
||||
/* raft_on_pre_vote_request is pure: stale-term + eligibility gate,
|
||||
* no term/voted_for/role mutation, no persist. */
|
||||
int r = raft_on_pre_vote_request(&c->ha.raft, from_idx, cand_term);
|
||||
int r = raft_on_pre_vote_request(&c->ha.raft, from_idx, cand_term,
|
||||
cand_llt, cand_lli);
|
||||
if (r == RAFT_GRANT) {
|
||||
granted = 1;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ typedef struct {
|
|||
uint64_t my_term;
|
||||
uint64_t my_tag;
|
||||
uint64_t my_lsn;
|
||||
/* ha-term-safety task 1.2: candidate's own Raft (lastLogTerm, lastLogIndex)
|
||||
* advertised in its RequestVote/PreVote so a voter's library logOk gate can
|
||||
* compare. my_last_log_index == candidate WAL log_len; my_last_log_term ==
|
||||
* term of that last entry (recovered/live WAL term). */
|
||||
uint64_t my_last_log_term;
|
||||
uint64_t my_last_log_index;
|
||||
const char *my_name;
|
||||
} vote_ep_ctx_t;
|
||||
|
||||
|
|
@ -148,6 +154,14 @@ static int raft_cb_send_vote_request(void *ctx, int peer_idx, uint64_t term)
|
|||
/* tag and lsn come from votes[0] which holds our own values */
|
||||
vctx->my_tag = (ectx->n_peers > 0) ? ectx->votes[0].my_tag : 0;
|
||||
vctx->my_lsn = (ectx->n_peers > 0) ? ectx->votes[0].my_lsn : 0;
|
||||
/* ha-term-safety task 1.2: advertise our Raft (lastLogTerm, lastLogIndex).
|
||||
* lastLogIndex == our WAL log length (the library's log_len == WAL tip LSN);
|
||||
* lastLogTerm == the recovered/live WAL term that stamps that last entry.
|
||||
* NOTE: sourced directly from persist (c->persist.term / raft.log_len), NOT
|
||||
* via on_get_entry_term — that callback seam is task 2.x and is unwired here
|
||||
* (see CONCERNS). The formal persist_entry_term accessor is task 1.3. */
|
||||
vctx->my_last_log_index = ha->raft.log_len;
|
||||
vctx->my_last_log_term = c ? c->persist.term : 0;
|
||||
vctx->my_name = c->myconf ? c->myconf->name : "";
|
||||
|
||||
/* Get a fresh endpoint */
|
||||
|
|
@ -256,6 +270,10 @@ static int raft_cb_send_pre_vote_request(void *ctx, int peer_idx,
|
|||
vctx->my_term = prospective_term;
|
||||
vctx->my_tag = (ectx->n_peers > 0) ? ectx->votes[0].my_tag : 0;
|
||||
vctx->my_lsn = (ectx->n_peers > 0) ? ectx->votes[0].my_lsn : 0;
|
||||
/* ha-term-safety task 1.2: advertise our Raft (lastLogTerm, lastLogIndex)
|
||||
* — identical sourcing to the real-vote send path above. */
|
||||
vctx->my_last_log_index = ha->raft.log_len;
|
||||
vctx->my_last_log_term = c ? c->persist.term : 0;
|
||||
vctx->my_name = c->myconf ? c->myconf->name : "";
|
||||
|
||||
endpoint_t *ep = endpoint_get();
|
||||
|
|
@ -1306,6 +1324,60 @@ static void raft_cb_trace(void *ctx, const char *event, const char *detail)
|
|||
detail ? " " : "", detail ? detail : "");
|
||||
}
|
||||
|
||||
/* ── ha-term-safety: MINIMAL log/commit callback placeholders ────────────────
|
||||
*
|
||||
* TODO(ha-term-safety task 2.x): REPLACE these stubs with the real bodies.
|
||||
* The raft library's raft_init() now REQUIRES all five non-NULL, and
|
||||
* raft_on_vote_request() internally calls on_get_entry_term for its logOk gate.
|
||||
* These placeholders exist ONLY to restore the controller build (tasks 1.1/1.2);
|
||||
* they perform NO persistence, NO replication, and NO truncation. See CONCERNS.
|
||||
*
|
||||
* on_persist_entry → task 2.1 (durable WAL append/overwrite)
|
||||
* on_get_entry_term → task 2.2 (WAL entry-term lookup; persist_entry_term, 1.3)
|
||||
* on_send_append_entries→ task 2.3 (AppendEntries transmit)
|
||||
* on_truncate_from → task 2.4 (conflict truncation)
|
||||
* on_commit_advanced → task 2.5 (commit-barrier trigger)
|
||||
*
|
||||
* on_get_entry_term returns 0 (== "absent") for now: with the log-replication
|
||||
* path unwired, the library's local last-log term reads as 0, so the logOk gate
|
||||
* grants on lastLogTerm >= 0 — i.e. it does NOT yet tighten the existing
|
||||
* config_tag/last_log_lsn freshness gate already enforced in cluster.c. The real
|
||||
* accessor (task 2.2/1.3) makes the gate effective. */
|
||||
static int ha_cb_persist_entry_stub(void *ctx, uint64_t index, uint64_t term,
|
||||
const void *entry, size_t len)
|
||||
{
|
||||
(void)ctx; (void)index; (void)term; (void)entry; (void)len;
|
||||
return 0; /* TODO task 2.1 */
|
||||
}
|
||||
|
||||
static uint64_t ha_cb_get_entry_term_stub(void *ctx, uint64_t index)
|
||||
{
|
||||
(void)ctx; (void)index;
|
||||
return 0; /* TODO task 2.2: 0 == absent (logOk reads local last-log term as 0) */
|
||||
}
|
||||
|
||||
static int ha_cb_send_append_entries_stub(void *ctx, int peer_idx,
|
||||
uint64_t prev_log_index,
|
||||
uint64_t prev_log_term,
|
||||
uint64_t leader_commit, int n_entries)
|
||||
{
|
||||
(void)ctx; (void)peer_idx; (void)prev_log_index; (void)prev_log_term;
|
||||
(void)leader_commit; (void)n_entries;
|
||||
return 0; /* TODO task 2.3 */
|
||||
}
|
||||
|
||||
static int ha_cb_truncate_from_stub(void *ctx, uint64_t index)
|
||||
{
|
||||
(void)ctx; (void)index;
|
||||
return 0; /* TODO task 2.4 */
|
||||
}
|
||||
|
||||
static void ha_cb_commit_advanced_stub(void *ctx, uint64_t commit_index)
|
||||
{
|
||||
(void)ctx; (void)commit_index;
|
||||
/* TODO task 2.5: trigger the existing synchronous commit barrier. */
|
||||
}
|
||||
|
||||
static const raft_callbacks_t s_real_cbs = {
|
||||
.on_send_vote_request = raft_cb_send_vote_request,
|
||||
.on_send_pre_vote_request = raft_cb_send_pre_vote_request,
|
||||
|
|
@ -1316,6 +1388,13 @@ static const raft_callbacks_t s_real_cbs = {
|
|||
.on_schedule_timeout = raft_cb_schedule_timeout,
|
||||
.on_cancel_timeout = raft_cb_cancel_timeout,
|
||||
.on_start_election_after_delay = raft_cb_start_election_after_delay,
|
||||
/* ha-term-safety: MINIMAL placeholders to satisfy raft_init's non-NULL
|
||||
* requirement — REPLACE in task 2.x (see stubs + CONCERNS). */
|
||||
.on_persist_entry = ha_cb_persist_entry_stub,
|
||||
.on_get_entry_term = ha_cb_get_entry_term_stub,
|
||||
.on_send_append_entries = ha_cb_send_append_entries_stub,
|
||||
.on_truncate_from = ha_cb_truncate_from_stub,
|
||||
.on_commit_advanced = ha_cb_commit_advanced_stub,
|
||||
.on_trace = raft_cb_trace,
|
||||
};
|
||||
|
||||
|
|
@ -3191,6 +3270,9 @@ static void on_vote_connected(endpoint_t *ep)
|
|||
vrq.candidate_id = (char *)(vctx->my_name ? vctx->my_name : "");
|
||||
vrq.config_tag = vctx->my_tag;
|
||||
vrq.last_log_lsn = vctx->my_lsn;
|
||||
/* ha-term-safety task 1.2: candidate's Raft (lastLogTerm, lastLogIndex). */
|
||||
vrq.last_log_term = vctx->my_last_log_term;
|
||||
vrq.last_log_index = vctx->my_last_log_index;
|
||||
|
||||
/* Wrap in xce_request_t */
|
||||
xce_request_t req;
|
||||
|
|
@ -3233,6 +3315,9 @@ static void on_pre_vote_connected(endpoint_t *ep)
|
|||
vrq.candidate_id = (char *)(vctx->my_name ? vctx->my_name : "");
|
||||
vrq.config_tag = vctx->my_tag;
|
||||
vrq.last_log_lsn = vctx->my_lsn;
|
||||
/* ha-term-safety task 1.2: candidate's Raft (lastLogTerm, lastLogIndex). */
|
||||
vrq.last_log_term = vctx->my_last_log_term;
|
||||
vrq.last_log_index = vctx->my_last_log_index;
|
||||
|
||||
xce_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
#include <capnp_c.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "controller.capnp.h"
|
||||
|
|
@ -132,6 +134,115 @@ static void test_repl_envelope_capnp_fields(void)
|
|||
printf("test_repl_envelope_capnp_fields: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 4: RequestVoteRequest carries the Raft (lastLogTerm, */
|
||||
/* lastLogIndex) tuple through a real encode → wire → decode round-trip */
|
||||
/* (ha-term-safety task 1.2, Req 2.3/2.4/2.6). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_request_vote_last_log_tuple_round_trip(void)
|
||||
{
|
||||
request_vote_req_t src;
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.term = 5ULL;
|
||||
src.candidate_id = "node-1";
|
||||
src.config_tag = 9ULL;
|
||||
src.last_log_lsn = 100ULL;
|
||||
src.last_log_term = 4ULL; /* NEW: candidate's last-log term */
|
||||
src.last_log_index = 100ULL; /* NEW: candidate's last-log index */
|
||||
|
||||
/* Encode the typemap struct into a capnp message, serialize to a flat
|
||||
* buffer, then decode it back — exercises BOTH the schema field and the
|
||||
* typemap.h mirror mapping (encode_/decode_ go through the C struct). */
|
||||
struct capn c;
|
||||
capn_init_malloc(&c);
|
||||
struct capn_segment *cs = capn_root(&c).seg;
|
||||
|
||||
/* encode_*_ptr allocates a struct in the segment and writes the C struct
|
||||
* fields into it (via the $C.mapname mapping). */
|
||||
RequestVoteRequest_ptr p;
|
||||
encode_RequestVoteRequest_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
int64_t sz = capn_size(&c);
|
||||
TEST_ASSERT_TRUE(sz > 0);
|
||||
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
capn_write_mem(&c, buf, (size_t)sz, 0);
|
||||
capn_free(&c);
|
||||
|
||||
struct capn d;
|
||||
TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0));
|
||||
RequestVoteRequest_ptr dp;
|
||||
dp.p = capn_getp(capn_root(&d), 0, 1);
|
||||
struct RequestVoteRequest wire;
|
||||
read_RequestVoteRequest(&wire, dp);
|
||||
request_vote_req_t dst;
|
||||
memset(&dst, 0, sizeof(dst));
|
||||
decode_RequestVoteRequest(&dst, &wire);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(5ULL, dst.term);
|
||||
TEST_ASSERT_EQUAL_UINT64(9ULL, dst.config_tag);
|
||||
TEST_ASSERT_EQUAL_UINT64(100ULL, dst.last_log_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(4ULL, dst.last_log_term); /* survives the wire */
|
||||
TEST_ASSERT_EQUAL_UINT64(100ULL, dst.last_log_index); /* survives the wire */
|
||||
|
||||
capn_free(&d);
|
||||
free(buf);
|
||||
printf("test_request_vote_last_log_tuple_round_trip: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 5: WalCatchupEntry (replication envelope) round-trips the */
|
||||
/* AppendEntries term fields (prev_log_index, prev_log_term, term) */
|
||||
/* (ha-term-safety task 1.1, Req 1.3/1.4). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_wal_catchup_append_term_fields_round_trip(void)
|
||||
{
|
||||
wal_catchup_entry_t src;
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.lsn = 42ULL;
|
||||
src.entry_type = (uint8_t)WAL_JOB_SUBMIT;
|
||||
src.term = 7ULL; /* NEW: this entry's term stamp */
|
||||
src.prev_log_index = 41ULL; /* NEW: lsn - 1 */
|
||||
src.prev_log_term = 6ULL; /* NEW: term at prev_log_index */
|
||||
|
||||
struct capn c;
|
||||
capn_init_malloc(&c);
|
||||
struct capn_segment *cs = capn_root(&c).seg;
|
||||
|
||||
WalCatchupEntry_ptr p;
|
||||
encode_WalCatchupEntry_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
int64_t sz = capn_size(&c);
|
||||
TEST_ASSERT_TRUE(sz > 0);
|
||||
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
capn_write_mem(&c, buf, (size_t)sz, 0);
|
||||
capn_free(&c);
|
||||
|
||||
struct capn d;
|
||||
TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0));
|
||||
WalCatchupEntry_ptr dp;
|
||||
dp.p = capn_getp(capn_root(&d), 0, 1);
|
||||
struct WalCatchupEntry wire;
|
||||
read_WalCatchupEntry(&wire, dp);
|
||||
wal_catchup_entry_t dst;
|
||||
memset(&dst, 0, sizeof(dst));
|
||||
decode_WalCatchupEntry(&dst, &wire);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(42ULL, dst.lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(7ULL, dst.term); /* survives the wire */
|
||||
TEST_ASSERT_EQUAL_UINT64(41ULL, dst.prev_log_index); /* survives the wire */
|
||||
TEST_ASSERT_EQUAL_UINT64(6ULL, dst.prev_log_term); /* survives the wire */
|
||||
|
||||
capn_free(&d);
|
||||
free(buf);
|
||||
printf("test_wal_catchup_append_term_fields_round_trip: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Suite entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -143,4 +254,6 @@ void test_e2e_capnp(void)
|
|||
RUN_TEST(test_persist_config_capnp_round_trip);
|
||||
RUN_TEST(test_snapshot_header_capnp_fields);
|
||||
RUN_TEST(test_repl_envelope_capnp_fields);
|
||||
RUN_TEST(test_request_vote_last_log_tuple_round_trip);
|
||||
RUN_TEST(test_wal_catchup_append_term_fields_round_trip);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,13 +444,19 @@ static void test_prevote_reply_over_real_transport_reaches_tally(void)
|
|||
* heartbeat gate fires first — so the freshness check is omitted for clarity.)
|
||||
*
|
||||
* Returns 1 = grant, 0 = deny; *out_reply_term = voter's unchanged term. */
|
||||
extern int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term);
|
||||
/* raft_on_pre_vote_request is declared in raft.h (now carries the candidate
|
||||
* (lastLogTerm, lastLogIndex) tuple — ha-term-safety task 1.2); no local extern. */
|
||||
|
||||
static int voter_pre_vote_decide(ha_state_t *ha, int from_idx, uint64_t cand_term,
|
||||
uint64_t *out_reply_term)
|
||||
{
|
||||
/* Pass (0,0) for the candidate last-log tuple: this PreVote test exercises
|
||||
* the role/heartbeat eligibility gate, not the logOk freshness path; with
|
||||
* the on_get_entry_term stub reading local last-log term as 0, logOk grants
|
||||
* so existing gate behavior is preserved (ha-term-safety task 1.2). */
|
||||
int granted =
|
||||
(raft_on_pre_vote_request(&ha->raft, from_idx, cand_term) == RAFT_GRANT)
|
||||
(raft_on_pre_vote_request(&ha->raft, from_idx, cand_term, 0, 0)
|
||||
== RAFT_GRANT)
|
||||
? 1 : 0;
|
||||
if (out_reply_term)
|
||||
*out_reply_term = atomic_load_explicit(&ha->term, memory_order_acquire);
|
||||
|
|
|
|||
|
|
@ -474,7 +474,13 @@ static int pre_vote_dispatch_decide(ha_state_t *ha,
|
|||
|
||||
int granted = 0;
|
||||
if (freshness_ok && from_idx >= 0)
|
||||
granted = (raft_on_pre_vote_request(&ha->raft, from_idx, cand_term)
|
||||
/* ha-term-safety task 1.2: pass candidate (lastLogTerm, lastLogIndex).
|
||||
* These gate tests do not exercise the logOk freshness path, so the
|
||||
* tuple is (0,0); the library's local last-log term also reads 0 (the
|
||||
* on_get_entry_term stub), so logOk grants and pre-existing gate
|
||||
* behavior is unchanged. */
|
||||
granted = (raft_on_pre_vote_request(&ha->raft, from_idx, cand_term,
|
||||
0, 0)
|
||||
== RAFT_GRANT) ? 1 : 0;
|
||||
|
||||
/* PreVote NEVER adopts the candidate term: the reply carries the voter's
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue