feat(raft-drive-async): offload requester-less config-push to slow lane (task 3.3)
push_config_transport_only spawns one thread per standby then joins them with a CONF_PUSH_TIMEOUT_MS bounded wait — blocking the caller. On the requester-less drive-thread paths (promotion + heartbeat convergence) that wait stalls the io_worker. Route those through ha_config_push_offloadable, gated by g_drive_async_configpush_offload (default OFF → inline, byte-for-byte legacy; ON → slow lane, non-blocking). Flag ON deep-copies the config bytes + sha into a lane ctx (caller keeps owning and freeing its own buffer); a single cluster-wide single-flight guard (ha_state_t.configpush_inflight scalar CAS + lane key=1) bounds it to one config-push fan-out in flight; the in-flight flag is cleared on every terminal path (two alloc failures, -EEXIST, hard submit error, lane completion). Send body factored into ha_config_push_do_send. Three requester-less drive-thread call sites converted: raft_cb_become_leader (promotion), the HA_REPL_PUSH_CONFIG heartbeat case, and the direct-CTRL_PRIMARY FSM promotion branch. The admin-upgrade barrier (push_config_to_standbys, has a waiting requester) and join.c are untouched. Lands DORMANT like tasks 3.1/3.2: the lane reads c->ha.standby_endpoints and writes those borrowed standby endpoints, racing io_worker RDHUP teardown if run off-thread (bug-1197 family). Flag OFF keeps push+teardown serialized on the io_worker. Task 3.4 flips this ON together with the snapshot and peerctrl flags after establishing endpoint-lifetime discipline. Tests (controller suite EXIT=0): test_configpush_offload_runs_on_lane, test_configpush_inline_when_flag_off, test_configpush_offload_dedups. Adversarial cpp-review: no CRITICAL/HIGH/MEDIUM findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6fDCtXkkEgupu7rUah1y2
This commit is contained in:
parent
6c0523b69b
commit
bd02064ebc
5 changed files with 515 additions and 13 deletions
|
|
@ -1969,6 +1969,210 @@ static void test_snapshot_offload_dedups_per_peer(void)
|
|||
printf("test_snapshot_offload_dedups_per_peer: PASS\n");
|
||||
}
|
||||
|
||||
/* ───────────────────────────────────────────────────────────────────────
|
||||
* Task 3.3 — requester-less ConfigPush slow-lane offload (Req 1.1, 1.5; bug-1198).
|
||||
*
|
||||
* Probe: ha_configpush_probe records the thread the push body ran on and
|
||||
* supports an optional gate so a test can hold the lane worker in flight while
|
||||
* it submits a second config push (cluster-wide dedup proof, Req 1.5). The two
|
||||
* production call sites are #ifndef XCE_TESTING, so the tests drive the router
|
||||
* directly via ha_config_push_offloadable_for_test(). Mirrors the snapshot
|
||||
* offload probe/gate above.
|
||||
* ─────────────────────────────────────────────────────────────────────── */
|
||||
extern void (*ha_configpush_probe)(void);
|
||||
extern int ha_config_push_offloadable_for_test(cluster_t *c, const uint8_t *bytes,
|
||||
size_t blen, const uint8_t sha256[32],
|
||||
uint64_t new_tag);
|
||||
|
||||
static _Atomic int g_cp_runs = 0;
|
||||
static _Atomic pthread_t g_cp_last_thread;
|
||||
static _Atomic int g_cp_gate_on = 0;
|
||||
static pthread_mutex_t g_cp_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_cp_release_cv = PTHREAD_COND_INITIALIZER;
|
||||
static int g_cp_parked = 0;
|
||||
static int g_cp_release = 0;
|
||||
|
||||
static void cp_probe(void)
|
||||
{
|
||||
atomic_store_explicit(&g_cp_last_thread, pthread_self(),
|
||||
memory_order_relaxed);
|
||||
if (atomic_load(&g_cp_gate_on)) {
|
||||
pthread_mutex_lock(&g_cp_mu);
|
||||
g_cp_parked = 1;
|
||||
while (!g_cp_release)
|
||||
pthread_cond_wait(&g_cp_release_cv, &g_cp_mu);
|
||||
pthread_mutex_unlock(&g_cp_mu);
|
||||
}
|
||||
atomic_fetch_add(&g_cp_runs, 1);
|
||||
}
|
||||
|
||||
/* Bounded poll until the gated body parks; 0 on park, -1 on timeout. */
|
||||
static int cp_wait_parked(int timeout_ms)
|
||||
{
|
||||
pthread_mutex_lock(&g_cp_mu);
|
||||
int parked = 0;
|
||||
for (int i = 0; i < timeout_ms; i++) {
|
||||
if (g_cp_parked) { parked = 1; break; }
|
||||
pthread_mutex_unlock(&g_cp_mu);
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000L }; /* 1 ms */
|
||||
nanosleep(&ts, NULL);
|
||||
pthread_mutex_lock(&g_cp_mu);
|
||||
}
|
||||
pthread_mutex_unlock(&g_cp_mu);
|
||||
return parked ? 0 : -1;
|
||||
}
|
||||
|
||||
static int cp_wait_runs(int want, int timeout_ms)
|
||||
{
|
||||
for (int i = 0; i < timeout_ms; i++) {
|
||||
if (atomic_load(&g_cp_runs) >= want) break;
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000L }; /* 1 ms */
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return atomic_load(&g_cp_runs);
|
||||
}
|
||||
|
||||
static void cp_release_gate(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_cp_mu);
|
||||
g_cp_release = 1;
|
||||
pthread_cond_broadcast(&g_cp_release_cv);
|
||||
pthread_mutex_unlock(&g_cp_mu);
|
||||
}
|
||||
|
||||
static void cp_reset_probe(void)
|
||||
{
|
||||
atomic_store(&g_cp_runs, 0);
|
||||
atomic_store(&g_cp_gate_on, 0);
|
||||
pthread_mutex_lock(&g_cp_mu);
|
||||
g_cp_parked = 0;
|
||||
g_cp_release = 0;
|
||||
pthread_mutex_unlock(&g_cp_mu);
|
||||
}
|
||||
|
||||
/*
|
||||
* Task 3.3 GREEN (Req 1.1, 8.1/8.2): with the offload flag ON, a requester-less
|
||||
* config push is submitted to the slow lane — the push body runs on a LANE
|
||||
* worker (a DIFFERENT thread from the caller) and the dispatch returns promptly.
|
||||
* This proves the config push no longer blocks the io_worker/drive thread.
|
||||
*
|
||||
* RED before Task 3.3 lands: the push ran inline on the caller thread, so the
|
||||
* different-thread assertion fails.
|
||||
*/
|
||||
static void test_configpush_offload_runs_on_lane(void)
|
||||
{
|
||||
cluster_t c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
ha_init(&c.ha, &g_cfg, 0);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&c.slowlane, 2, 4));
|
||||
|
||||
cp_reset_probe();
|
||||
ha_configpush_probe = cp_probe;
|
||||
ha_set_configpush_offload_for_test(1);
|
||||
|
||||
pthread_t caller = pthread_self();
|
||||
int rc = ha_config_push_offloadable_for_test(&c, NULL, 0, NULL, 7);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"Req 8.1/8.2: dispatch must return promptly (0) after lane submit");
|
||||
|
||||
int runs = cp_wait_runs(1, 2000);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, runs,
|
||||
"Req 1.1: the config push body must run on the lane exactly once");
|
||||
TEST_ASSERT_FALSE_MESSAGE(
|
||||
pthread_equal(atomic_load_explicit(&g_cp_last_thread,
|
||||
memory_order_relaxed), caller),
|
||||
"Req 1.1: the config push must run on a LANE worker, NOT the "
|
||||
"caller's (io_worker/drive) thread");
|
||||
|
||||
ha_set_configpush_offload_for_test(0); /* restore production default (OFF) */
|
||||
ha_configpush_probe = NULL;
|
||||
slowlane_fini(&c.slowlane);
|
||||
ha_fini(&c.ha);
|
||||
printf("test_configpush_offload_runs_on_lane: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Task 3.3 no-regression (Req 6.x): with the offload flag OFF (the production
|
||||
* default until Task 3.4 resolves the borrowed-endpoint lifetime), the config
|
||||
* push runs INLINE on the calling thread — byte-for-byte the pre-task behavior.
|
||||
*/
|
||||
static void test_configpush_inline_when_flag_off(void)
|
||||
{
|
||||
cluster_t c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
ha_init(&c.ha, &g_cfg, 0);
|
||||
|
||||
cp_reset_probe();
|
||||
ha_configpush_probe = cp_probe;
|
||||
ha_set_configpush_offload_for_test(0); /* explicit OFF */
|
||||
|
||||
pthread_t caller = pthread_self();
|
||||
int rc = ha_config_push_offloadable_for_test(&c, NULL, 0, NULL, 7);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Synchronous: the body already ran on THIS thread before returning. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, atomic_load(&g_cp_runs),
|
||||
"Req 6.x: with the flag OFF the config push must run inline");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
pthread_equal(atomic_load_explicit(&g_cp_last_thread,
|
||||
memory_order_relaxed), caller),
|
||||
"Req 6.x: with the flag OFF the push must run on the caller's thread");
|
||||
|
||||
ha_configpush_probe = NULL;
|
||||
ha_fini(&c.ha);
|
||||
printf("test_configpush_inline_when_flag_off: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Task 3.3 (Req 1.5): cluster-wide single-flight. While one config push is in
|
||||
* flight on the lane (parked at the gate), a second push enqueues nothing — the
|
||||
* CAS on configpush_inflight fails. Proof: with the first body parked, the run
|
||||
* count stays at 0; after release, exactly ONE body has run (the duplicate was
|
||||
* deduped, not queued).
|
||||
*/
|
||||
static void test_configpush_offload_dedups(void)
|
||||
{
|
||||
cluster_t c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
ha_init(&c.ha, &g_cfg, 0);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&c.slowlane, 2, 4));
|
||||
|
||||
cp_reset_probe();
|
||||
atomic_store(&g_cp_gate_on, 1); /* lane body parks at the gate */
|
||||
ha_configpush_probe = cp_probe;
|
||||
ha_set_configpush_offload_for_test(1);
|
||||
|
||||
/* First config push — submitted; its body parks in the lane. */
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_config_push_offloadable_for_test(&c, NULL, 0, NULL, 7));
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, cp_wait_parked(2000),
|
||||
"the first config push body must reach the gate (running on the lane)");
|
||||
|
||||
/* Second config push while the first is in flight. */
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_config_push_offloadable_for_test(&c, NULL, 0, NULL, 7));
|
||||
|
||||
/* Give a generous window: the duplicate must NOT run. */
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 200 * 1000000L }; /* 200 ms */
|
||||
nanosleep(&ts, NULL);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, atomic_load(&g_cp_runs),
|
||||
"Req 1.5: the duplicate config push must be deduped while one is in flight");
|
||||
|
||||
/* Release; exactly the first body completes. */
|
||||
cp_release_gate();
|
||||
int runs = cp_wait_runs(1, 2000);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, runs,
|
||||
"Req 1.5: exactly one config push ran (duplicate deduped)");
|
||||
/* And no second body sneaks in after release. */
|
||||
nanosleep(&ts, NULL);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, atomic_load(&g_cp_runs),
|
||||
"Req 1.5: the deduped duplicate was dropped, not queued behind the first");
|
||||
|
||||
ha_set_configpush_offload_for_test(0); /* restore production default (OFF) */
|
||||
ha_configpush_probe = NULL;
|
||||
slowlane_fini(&c.slowlane);
|
||||
ha_fini(&c.ha);
|
||||
printf("test_configpush_offload_dedups: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Suite entry point ──────────────────────────────────────────────────── */
|
||||
|
||||
void test_ha_liveness(void)
|
||||
|
|
@ -2010,5 +2214,9 @@ void test_ha_liveness(void)
|
|||
RUN_TEST(test_snapshot_offload_runs_on_lane);
|
||||
RUN_TEST(test_snapshot_inline_when_flag_off);
|
||||
RUN_TEST(test_snapshot_offload_dedups_per_peer);
|
||||
/* raft-drive-async Task 3.3 — requester-less ConfigPush slow-lane offload (Req 1.1, 1.5, 6.x) */
|
||||
RUN_TEST(test_configpush_offload_runs_on_lane);
|
||||
RUN_TEST(test_configpush_inline_when_flag_off);
|
||||
RUN_TEST(test_configpush_offload_dedups);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue