feat(raft-drive-async): SlowLane bounded executor for long blocking I/O (task 1.2)
Dedicated-pthread pool isolated from the vtask pool; non-blocking slowlane_submit (0 enqueued / -EEXIST deduped / -EAGAIN cap-full-deferred), admission cap on running tasks, per-key in-flight dedup, run-exactly-once, drain+join fini. Wired into controller lifecycle (dormant; consumers land in 2.x/3.x). 7 unit tests, mutation-verified cap+dedup; full suite 194/0 green, 15x ASAN.
This commit is contained in:
parent
729055a37f
commit
5650acd375
8 changed files with 710 additions and 1 deletions
|
|
@ -10,7 +10,7 @@ Tasks mirror the design's 4-phase migration. The slow lane is foundational (ever
|
|||
- Observable: a build with the new constants compiles; defaults are readable at controller startup; the resolved reservation logged at init.
|
||||
- _Requirements: 2.3, 3.1_
|
||||
|
||||
- [ ] 1.2 Implement the SlowLane executor
|
||||
- [x] 1.2 Implement the SlowLane executor
|
||||
- Bounded pool of dedicated worker threads, isolated from the vtask request pool; non-blocking submit that enqueues and returns immediately.
|
||||
- Admission cap on concurrent in-flight tasks; per-key in-flight dedup (key 0 disables dedup); init/fini wired into the controller lifecycle.
|
||||
- Observable: a submitted task runs exactly once on a lane worker; submit never blocks the caller; concurrent running tasks never exceed the cap; a submit with an in-flight key is a no-op.
|
||||
|
|
|
|||
|
|
@ -1367,6 +1367,25 @@ int cluster_init_phase2(cluster_t* c)
|
|||
}
|
||||
}
|
||||
|
||||
/* raft-drive-async task 1.2: bring up the SlowLane executor — the dedicated
|
||||
* bounded-thread offload lane for long blocking I/O (snapshot transfer,
|
||||
* config-push, peer reconnect) that must never run on the iomgr/delaymgr
|
||||
* drive threads. Dormant infra here: no production caller until the
|
||||
* reconnect (2.x) / snapshot/config-push (3.x) offloads land. Sized from
|
||||
* the DEFAULT_SLOWLANE_* constants (task 1.1, config.h). Guard c->slowlane
|
||||
* so the legacy two-phase path (phase2 can run twice) inits exactly once. */
|
||||
if (c->slowlane.workers == NULL) {
|
||||
rc = slowlane_init(&c->slowlane, DEFAULT_SLOWLANE_WORKERS,
|
||||
DEFAULT_SLOWLANE_MAX_INFLIGHT);
|
||||
if (rc != 0) {
|
||||
ERROR("slowlane_init failed: %d (workers=%d, max_inflight=%d)",
|
||||
rc, DEFAULT_SLOWLANE_WORKERS, DEFAULT_SLOWLANE_MAX_INFLIGHT);
|
||||
return rc;
|
||||
}
|
||||
INFO("drive-async: slowlane up (workers=%d, max_inflight=%d)",
|
||||
DEFAULT_SLOWLANE_WORKERS, DEFAULT_SLOWLANE_MAX_INFLIGHT);
|
||||
}
|
||||
|
||||
/* Role set by ha_elect_evaluate_and_signal() inside cluster_run() */
|
||||
if (ha_is_primary(&c->ha))
|
||||
ha_promote_to_primary(c);
|
||||
|
|
@ -1743,6 +1762,12 @@ void cluster_fini(cluster_t* c)
|
|||
/* Stop HA watchdog thread before tearing down other subsystems — P1-5 */
|
||||
ha_fini(&c->ha);
|
||||
|
||||
/* raft-drive-async task 1.2: drain + join the SlowLane after the drive
|
||||
* threads have stopped submitting (ha_fini above) and before persist/iomgr
|
||||
* teardown, since lane tasks (snapshot/reconnect, once wired) touch those.
|
||||
* No-op if phase2 never ran (workers==NULL guard inside slowlane_fini). */
|
||||
slowlane_fini(&c->slowlane);
|
||||
|
||||
/* Stop persistence before tearing down the rest */
|
||||
persist_stop(&c->persist);
|
||||
persist_fini(&c->persist);
|
||||
|
|
|
|||
95
src/controller/include/slowlane.h
Normal file
95
src/controller/include/slowlane.h
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#pragma once
|
||||
/*
|
||||
* slowlane.h — SlowLane executor: a bounded pool of dedicated worker threads
|
||||
* that runs long blocking I/O OFF the Raft drive threads (iomgr epoll loop,
|
||||
* delaymgr timer), with admission control and per-key in-flight dedup.
|
||||
*
|
||||
* Service interface is verbatim from
|
||||
* .kiro/specs/raft-drive-async/design.md (Components -> SlowLane).
|
||||
*
|
||||
* The lane owns scheduling/isolation, NOT the work content: the submitted
|
||||
* `fn` (peer connect, snapshot transfer, config-push body) stays in its own
|
||||
* module and is responsible for clearing any caller-side in-flight state on
|
||||
* completion. `slowlane_submit` is non-blocking: a drive-thread caller
|
||||
* enqueues and returns immediately, never waiting for `fn` to run.
|
||||
*
|
||||
* raft-drive-async task 1.2 — Requirements 3.1, 3.2, 3.3, 3.4, 1.5.
|
||||
*/
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void (*slowlane_fn_t)(void *arg);
|
||||
|
||||
/*
|
||||
* One queued unit of work. Allocated by the lane on submit, freed by the
|
||||
* worker after `fn` returns. Held on the FIFO pending queue until a worker
|
||||
* picks it up.
|
||||
*/
|
||||
typedef struct slowlane_task {
|
||||
uint64_t key; /* dedup key; 0 disables dedup */
|
||||
slowlane_fn_t fn;
|
||||
void *arg;
|
||||
struct slowlane_task *next; /* intrusive FIFO link */
|
||||
} slowlane_task_t;
|
||||
|
||||
/*
|
||||
* SlowLane executor state. All fields are lane-private and guarded by `lock`;
|
||||
* callers touch the lane only through the API below. Bounded by design:
|
||||
* - `n_workers` dedicated pthreads, isolated from the vtask request pool;
|
||||
* - at most `max_inflight` tasks RUNNING concurrently (admission cap);
|
||||
* - at most one in-flight task per non-zero key (dedup).
|
||||
*/
|
||||
typedef struct slowlane {
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t cond; /* signalled when work or a slot frees up */
|
||||
|
||||
slowlane_task_t *q_head; /* FIFO pending queue (admission-deferred) */
|
||||
slowlane_task_t *q_tail;
|
||||
|
||||
uint64_t *inflight_keys; /* running OR queued non-zero keys (dedup set) */
|
||||
int n_inflight_keys;
|
||||
int cap_inflight_keys;
|
||||
|
||||
int n_workers;
|
||||
int max_inflight; /* admission cap on concurrently RUNNING tasks */
|
||||
int n_running; /* tasks currently executing on a worker */
|
||||
|
||||
pthread_t *workers;
|
||||
int started; /* worker threads created */
|
||||
int stopping; /* fini requested: drain + join */
|
||||
} slowlane_t;
|
||||
|
||||
/*
|
||||
* Initialise the lane and start `n_workers` dedicated worker threads.
|
||||
* `max_inflight` bounds the number of tasks RUNNING concurrently (excess is
|
||||
* queued, never dropped, never run on a drive thread).
|
||||
* Returns 0 on success, -EINVAL on a bad argument, -errno on a thread/alloc
|
||||
* failure (lane left un-started).
|
||||
*/
|
||||
int slowlane_init(slowlane_t *sl, int n_workers, int max_inflight);
|
||||
|
||||
/*
|
||||
* Submit `fn(arg)` to run exactly once on a lane worker. NON-BLOCKING: the
|
||||
* caller (which may be a drive thread) returns immediately.
|
||||
*
|
||||
* Returns (per design SlowLane Service Interface):
|
||||
* 0 enqueued AND a worker slot was free (running < max_inflight) —
|
||||
* the task can start immediately
|
||||
* -EEXIST `key` (non-zero) already in flight — deduped no-op, NOT enqueued
|
||||
* -EAGAIN admission cap full at submit time (running >= max_inflight) — the
|
||||
* task IS still queued/deferred (never dropped, never run on a drive
|
||||
* thread); the caller is told it was deferred, not that it failed
|
||||
* -errno allocation failure / lane stopping
|
||||
*
|
||||
* In all of {0, -EAGAIN} the work is enqueued and will run exactly once. Only
|
||||
* -EEXIST and -errno mean the task was not enqueued.
|
||||
*
|
||||
* `key == 0` disables dedup (every key-0 submit is independent work).
|
||||
*/
|
||||
int slowlane_submit(slowlane_t *sl, uint64_t key, slowlane_fn_t fn, void *arg);
|
||||
|
||||
/*
|
||||
* Drain the pending queue, stop and join all workers. Shutdown only.
|
||||
* After return the lane is dormant; re-init to reuse.
|
||||
*/
|
||||
void slowlane_fini(slowlane_t *sl);
|
||||
|
|
@ -26,6 +26,7 @@ controller_src = [
|
|||
'persist.c',
|
||||
'repl.c',
|
||||
'ctrl_metrics.c',
|
||||
'slowlane.c',
|
||||
]
|
||||
|
||||
incdir = include_directories('.', '../include', '../lib',
|
||||
|
|
@ -140,6 +141,7 @@ controller_unit_test_src = [
|
|||
'testsuite/test_ha_vote_floor_heartbeat.c',
|
||||
'testsuite/test_term_safety_adapter.c',
|
||||
'testsuite/test_drive_async_config.c',
|
||||
'testsuite/test_slowlane.c',
|
||||
'testsuite/test_waldump.c',
|
||||
'testsuite/waldump.c',
|
||||
'testsuite/test_walcompare.c',
|
||||
|
|
@ -159,6 +161,7 @@ controller_unit_test_src = [
|
|||
'repl.c',
|
||||
'ctrl_metrics.c',
|
||||
'config_repl.c',
|
||||
'slowlane.c',
|
||||
]
|
||||
|
||||
ssl_dep = dependency('openssl')
|
||||
|
|
|
|||
264
src/controller/slowlane.c
Normal file
264
src/controller/slowlane.c
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
#define _GNU_SOURCE
|
||||
#include "slowlane.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* slowlane.c — bounded, admission-capped executor with per-key in-flight
|
||||
* dedup, running long blocking I/O off the Raft drive threads.
|
||||
*
|
||||
* raft-drive-async task 1.2 — Requirements 3.1, 3.2, 3.3, 3.4, 1.5.
|
||||
*
|
||||
* Concurrency model (single mutex `lock`, one condvar `cond`):
|
||||
* - `q_head/q_tail` is the FIFO of admitted-but-not-yet-running tasks.
|
||||
* - `n_running` counts tasks executing on a worker; admission holds
|
||||
* `n_running <= max_inflight` at all times.
|
||||
* - `inflight_keys[]` holds every non-zero key that is queued OR running;
|
||||
* a key is added at submit and removed when its `fn` returns, so a second
|
||||
* submit of the same key in that window dedups (-EEXIST).
|
||||
* - Workers wait on `cond` for "work pending AND a running slot free".
|
||||
* submit and task-completion both signal `cond`.
|
||||
*
|
||||
* Lock discipline: `fn` ALWAYS runs with `lock` released (it blocks for I/O);
|
||||
* the lane never holds `lock` across user work — so a slow/stuck `fn` cannot
|
||||
* stall submit() or other workers' admission decisions.
|
||||
*/
|
||||
|
||||
/* ── dedup key set helpers (caller holds sl->lock) ────────────────── */
|
||||
|
||||
static bool key_present_locked(const slowlane_t *sl, uint64_t key)
|
||||
{
|
||||
for (int i = 0; i < sl->n_inflight_keys; i++)
|
||||
if (sl->inflight_keys[i] == key)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static int key_add_locked(slowlane_t *sl, uint64_t key)
|
||||
{
|
||||
if (sl->n_inflight_keys == sl->cap_inflight_keys) {
|
||||
int newcap = sl->cap_inflight_keys ? sl->cap_inflight_keys * 2 : 8;
|
||||
uint64_t *grown =
|
||||
realloc(sl->inflight_keys, (size_t)newcap * sizeof(uint64_t));
|
||||
if (grown == NULL)
|
||||
return -ENOMEM;
|
||||
sl->inflight_keys = grown;
|
||||
sl->cap_inflight_keys = newcap;
|
||||
}
|
||||
sl->inflight_keys[sl->n_inflight_keys++] = key;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void key_remove_locked(slowlane_t *sl, uint64_t key)
|
||||
{
|
||||
if (key == 0)
|
||||
return;
|
||||
for (int i = 0; i < sl->n_inflight_keys; i++) {
|
||||
if (sl->inflight_keys[i] == key) {
|
||||
/* swap-remove (order irrelevant for a membership set) */
|
||||
sl->inflight_keys[i] =
|
||||
sl->inflight_keys[--sl->n_inflight_keys];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── worker thread ────────────────────────────────────────────────── */
|
||||
|
||||
static void *slowlane_worker(void *arg)
|
||||
{
|
||||
slowlane_t *sl = arg;
|
||||
|
||||
pthread_mutex_lock(&sl->lock);
|
||||
for (;;) {
|
||||
/* Wait for: a queued task AND a free running slot. On shutdown we
|
||||
* still drain the queue (so already-submitted work runs exactly once),
|
||||
* but stop waiting for new arrivals. */
|
||||
while (!sl->stopping &&
|
||||
(sl->q_head == NULL || sl->n_running >= sl->max_inflight))
|
||||
pthread_cond_wait(&sl->cond, &sl->lock);
|
||||
|
||||
if (sl->q_head == NULL) {
|
||||
/* Nothing left to run. If stopping, exit; otherwise (can only be a
|
||||
* spurious wake on a non-empty-but-cap-full path that then emptied)
|
||||
* loop back to wait. */
|
||||
if (sl->stopping)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Even while stopping, respect the admission cap on the drain. */
|
||||
if (sl->n_running >= sl->max_inflight) {
|
||||
if (sl->stopping) {
|
||||
/* A free slot will appear when a running task completes and
|
||||
* signals; wait for it so we can finish draining. */
|
||||
pthread_cond_wait(&sl->cond, &sl->lock);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Pop FIFO head, claim a running slot. */
|
||||
slowlane_task_t *t = sl->q_head;
|
||||
sl->q_head = t->next;
|
||||
if (sl->q_head == NULL)
|
||||
sl->q_tail = NULL;
|
||||
sl->n_running++;
|
||||
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
/* Run user work with NO lock held (it blocks for I/O). Exactly once. */
|
||||
t->fn(t->arg);
|
||||
uint64_t done_key = t->key;
|
||||
free(t);
|
||||
pthread_mutex_lock(&sl->lock);
|
||||
|
||||
sl->n_running--;
|
||||
key_remove_locked(sl, done_key);
|
||||
/* A slot freed (admission) and a key cleared (dedup): wake a waiter
|
||||
* that may now be admissible or a submit blocked on nothing here. */
|
||||
pthread_cond_broadcast(&sl->cond);
|
||||
}
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── public API ───────────────────────────────────────────────────── */
|
||||
|
||||
int slowlane_init(slowlane_t *sl, int n_workers, int max_inflight)
|
||||
{
|
||||
if (sl == NULL || n_workers < 1 || max_inflight < 1)
|
||||
return -EINVAL;
|
||||
|
||||
memset(sl, 0, sizeof(*sl));
|
||||
sl->n_workers = n_workers;
|
||||
sl->max_inflight = max_inflight;
|
||||
|
||||
if (pthread_mutex_init(&sl->lock, NULL) != 0)
|
||||
return -errno;
|
||||
if (pthread_cond_init(&sl->cond, NULL) != 0) {
|
||||
pthread_mutex_destroy(&sl->lock);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
sl->workers = calloc((size_t)n_workers, sizeof(pthread_t));
|
||||
if (sl->workers == NULL) {
|
||||
pthread_cond_destroy(&sl->cond);
|
||||
pthread_mutex_destroy(&sl->lock);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n_workers; i++) {
|
||||
int rc = pthread_create(&sl->workers[i], NULL, slowlane_worker, sl);
|
||||
if (rc != 0) {
|
||||
/* Roll back the partially-started pool: stop and join what exists. */
|
||||
pthread_mutex_lock(&sl->lock);
|
||||
sl->stopping = 1;
|
||||
pthread_cond_broadcast(&sl->cond);
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
for (int j = 0; j < i; j++)
|
||||
pthread_join(sl->workers[j], NULL);
|
||||
free(sl->workers);
|
||||
sl->workers = NULL;
|
||||
pthread_cond_destroy(&sl->cond);
|
||||
pthread_mutex_destroy(&sl->lock);
|
||||
return -rc;
|
||||
}
|
||||
sl->started++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int slowlane_submit(slowlane_t *sl, uint64_t key, slowlane_fn_t fn, void *arg)
|
||||
{
|
||||
if (sl == NULL || fn == NULL)
|
||||
return -EINVAL;
|
||||
|
||||
pthread_mutex_lock(&sl->lock);
|
||||
|
||||
if (sl->stopping) {
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return -ESHUTDOWN;
|
||||
}
|
||||
|
||||
/* Per-key in-flight dedup: a non-zero key already queued/running is a
|
||||
* no-op (R1.5 snapshot-per-standby, R2.5 reconnect-per-peer). */
|
||||
if (key != 0 && key_present_locked(sl, key)) {
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return -EEXIST;
|
||||
}
|
||||
|
||||
slowlane_task_t *t = calloc(1, sizeof(*t));
|
||||
if (t == NULL) {
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return -ENOMEM;
|
||||
}
|
||||
t->key = key;
|
||||
t->fn = fn;
|
||||
t->arg = arg;
|
||||
t->next = NULL;
|
||||
|
||||
if (key != 0) {
|
||||
int rc = key_add_locked(sl, key);
|
||||
if (rc != 0) {
|
||||
free(t);
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Append to FIFO tail. */
|
||||
if (sl->q_tail != NULL)
|
||||
sl->q_tail->next = t;
|
||||
else
|
||||
sl->q_head = t;
|
||||
sl->q_tail = t;
|
||||
|
||||
/* Admission status at submit time: a free running slot means the task can
|
||||
* start immediately (0); a full cap means it is DEFERRED but still queued
|
||||
* (-EAGAIN) — never dropped, never run on a drive thread. */
|
||||
int rc = (sl->n_running >= sl->max_inflight) ? -EAGAIN : 0;
|
||||
|
||||
pthread_cond_signal(&sl->cond);
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void slowlane_fini(slowlane_t *sl)
|
||||
{
|
||||
if (sl == NULL || sl->workers == NULL)
|
||||
return;
|
||||
|
||||
/* Request drain+stop; workers finish the queued tasks, then exit. */
|
||||
pthread_mutex_lock(&sl->lock);
|
||||
sl->stopping = 1;
|
||||
pthread_cond_broadcast(&sl->cond);
|
||||
pthread_mutex_unlock(&sl->lock);
|
||||
|
||||
for (int i = 0; i < sl->started; i++)
|
||||
pthread_join(sl->workers[i], NULL);
|
||||
|
||||
/* All workers joined: no concurrent access remains. Free any tasks that
|
||||
* were never run (only possible if a worker create failed mid-init; the
|
||||
* normal drain path empties the queue). */
|
||||
slowlane_task_t *t = sl->q_head;
|
||||
while (t != NULL) {
|
||||
slowlane_task_t *next = t->next;
|
||||
free(t);
|
||||
t = next;
|
||||
}
|
||||
sl->q_head = sl->q_tail = NULL;
|
||||
|
||||
free(sl->inflight_keys);
|
||||
sl->inflight_keys = NULL;
|
||||
sl->n_inflight_keys = 0;
|
||||
sl->cap_inflight_keys = 0;
|
||||
|
||||
free(sl->workers);
|
||||
sl->workers = NULL;
|
||||
sl->started = 0;
|
||||
|
||||
pthread_cond_destroy(&sl->cond);
|
||||
pthread_mutex_destroy(&sl->lock);
|
||||
}
|
||||
|
|
@ -67,6 +67,7 @@ extern void test_term_safety_adapter(void);
|
|||
extern void test_waldump(void);
|
||||
extern void test_walcompare(void);
|
||||
extern void test_drive_async_config(void);
|
||||
extern void test_slowlane(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -203,6 +204,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_waldump);
|
||||
TEST(test_walcompare);
|
||||
TEST(test_drive_async_config);
|
||||
TEST(test_slowlane);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
318
src/controller/testsuite/test_slowlane.c
Normal file
318
src/controller/testsuite/test_slowlane.c
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "slowlane.h"
|
||||
|
||||
/* ====================================================================
|
||||
* raft-drive-async task 1.2 — SlowLane executor unit tests (RED first).
|
||||
*
|
||||
* The SlowLane is a bounded pool of dedicated worker threads that runs
|
||||
* long blocking I/O off the Raft drive threads, with admission control
|
||||
* and per-key in-flight dedup. These tests assert the four core
|
||||
* behavioral contracts (acceptance criteria 3.1-3.4, 1.5):
|
||||
* (a) slowlane_submit is non-blocking
|
||||
* (b) concurrent RUNNING tasks never exceed max_inflight
|
||||
* (c) a submit with an already-in-flight non-zero key returns -EEXIST
|
||||
* (d) fn runs exactly once on a lane worker
|
||||
* (+) -EAGAIN is returned (but work still queued) when the cap is full
|
||||
*
|
||||
* Determinism: every blocking task parks on a test-owned condvar gate
|
||||
* (NOT a sleep), so the test controls exactly when a worker is occupied
|
||||
* and when it releases. Concurrency peaks are observed via an atomic
|
||||
* running counter the tasks themselves bump.
|
||||
* ==================================================================== */
|
||||
|
||||
/* ── Test-owned gate: a controllable blocking task body ───────────── */
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t release_cv; /* test broadcasts to release parked tasks */
|
||||
pthread_cond_t entered_cv; /* task broadcasts when it has entered/parked */
|
||||
int gate_open; /* 0 = parked tasks must wait */
|
||||
|
||||
_Atomic int running; /* tasks currently inside fn (post-entry) */
|
||||
_Atomic int max_running; /* high-water mark of `running` */
|
||||
_Atomic int total_runs; /* how many times any task fn executed */
|
||||
_Atomic int entered; /* tasks that have reached the gate */
|
||||
} gate_t;
|
||||
|
||||
static void gate_init(gate_t *g)
|
||||
{
|
||||
pthread_mutex_init(&g->mu, NULL);
|
||||
pthread_cond_init(&g->release_cv, NULL);
|
||||
pthread_cond_init(&g->entered_cv, NULL);
|
||||
g->gate_open = 0;
|
||||
atomic_store(&g->running, 0);
|
||||
atomic_store(&g->max_running, 0);
|
||||
atomic_store(&g->total_runs, 0);
|
||||
atomic_store(&g->entered, 0);
|
||||
}
|
||||
|
||||
static void gate_destroy(gate_t *g)
|
||||
{
|
||||
pthread_mutex_destroy(&g->mu);
|
||||
pthread_cond_destroy(&g->release_cv);
|
||||
pthread_cond_destroy(&g->entered_cv);
|
||||
}
|
||||
|
||||
/* Blocking task body: bump running, record the peak, signal entry, then park
|
||||
* on the gate until the test opens it. Runs on a lane worker. */
|
||||
static void gated_task(void *arg)
|
||||
{
|
||||
gate_t *g = arg;
|
||||
|
||||
int now = atomic_fetch_add(&g->running, 1) + 1;
|
||||
int prev = atomic_load(&g->max_running);
|
||||
while (now > prev && !atomic_compare_exchange_weak(&g->max_running, &prev, now))
|
||||
; /* CAS-max */
|
||||
atomic_fetch_add(&g->total_runs, 1);
|
||||
|
||||
pthread_mutex_lock(&g->mu);
|
||||
atomic_fetch_add(&g->entered, 1);
|
||||
pthread_cond_broadcast(&g->entered_cv);
|
||||
while (!g->gate_open)
|
||||
pthread_cond_wait(&g->release_cv, &g->mu);
|
||||
pthread_mutex_unlock(&g->mu);
|
||||
|
||||
atomic_fetch_sub(&g->running, 1);
|
||||
}
|
||||
|
||||
static void gate_open(gate_t *g)
|
||||
{
|
||||
pthread_mutex_lock(&g->mu);
|
||||
g->gate_open = 1;
|
||||
pthread_cond_broadcast(&g->release_cv);
|
||||
pthread_mutex_unlock(&g->mu);
|
||||
}
|
||||
|
||||
/* Wait (bounded) until at least `n` tasks have entered the gate. */
|
||||
static int wait_entered_at_least(gate_t *g, int n, int timeout_ms)
|
||||
{
|
||||
struct timespec dl;
|
||||
clock_gettime(CLOCK_REALTIME, &dl);
|
||||
dl.tv_sec += timeout_ms / 1000;
|
||||
dl.tv_nsec += (timeout_ms % 1000) * 1000000L;
|
||||
if (dl.tv_nsec >= 1000000000L) { dl.tv_sec++; dl.tv_nsec -= 1000000000L; }
|
||||
|
||||
pthread_mutex_lock(&g->mu);
|
||||
while (atomic_load(&g->entered) < n) {
|
||||
if (pthread_cond_timedwait(&g->entered_cv, &g->mu, &dl) == ETIMEDOUT)
|
||||
break;
|
||||
}
|
||||
int got = atomic_load(&g->entered);
|
||||
pthread_mutex_unlock(&g->mu);
|
||||
return got;
|
||||
}
|
||||
|
||||
/* ── (a) submit is non-blocking ───────────────────────────────────── */
|
||||
|
||||
/* A submit must return promptly even when the task it submits will block
|
||||
* indefinitely. We submit a gated (never-released until we open) task and
|
||||
* assert submit returned without us opening the gate. */
|
||||
static void test_submit_is_non_blocking(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 2, 2));
|
||||
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
int rc = slowlane_submit(&sl, 0, gated_task, &g);
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* submit returned while the task is (or will be) parked: a blocking call
|
||||
* would not have returned. Bound the wall time generously (<500ms). */
|
||||
long ms = (t1.tv_sec - t0.tv_sec) * 1000 +
|
||||
(t1.tv_nsec - t0.tv_nsec) / 1000000;
|
||||
TEST_ASSERT_TRUE_MESSAGE(ms < 500, "submit blocked the caller");
|
||||
|
||||
gate_open(&g);
|
||||
slowlane_fini(&sl); /* drains + joins */
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* ── (b) concurrent running tasks never exceed the cap ────────────── */
|
||||
|
||||
static void test_admission_cap_bounds_concurrency(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
const int n_workers = 4;
|
||||
const int max_inflight = 2;
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, n_workers, max_inflight));
|
||||
|
||||
/* Submit more tasks than the cap; all must enqueue, but only `cap` may run
|
||||
* concurrently even though 4 workers exist. */
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int rc = slowlane_submit(&sl, 0, gated_task, &g);
|
||||
/* 0 while a slot is free, -EAGAIN once the cap is full (still queued). */
|
||||
TEST_ASSERT_TRUE_MESSAGE(rc == 0 || rc == -EAGAIN, "submit dropped work");
|
||||
}
|
||||
|
||||
/* Let the first wave run and park at the gate. */
|
||||
int entered = wait_entered_at_least(&g, max_inflight, 2000);
|
||||
TEST_ASSERT_EQUAL_INT(max_inflight, entered);
|
||||
|
||||
/* Give any (incorrect) over-admission a chance to manifest, then assert the
|
||||
* peak never exceeded the cap. */
|
||||
struct timespec nap = { .tv_sec = 0, .tv_nsec = 100 * 1000000L };
|
||||
nanosleep(&nap, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(max_inflight, atomic_load(&g.running));
|
||||
TEST_ASSERT_TRUE_MESSAGE(atomic_load(&g.max_running) <= max_inflight,
|
||||
"concurrent running tasks exceeded the cap");
|
||||
|
||||
/* Release everything; all 6 must eventually run and the peak stays <= cap. */
|
||||
gate_open(&g);
|
||||
slowlane_fini(&sl);
|
||||
TEST_ASSERT_EQUAL_INT(6, atomic_load(&g.total_runs));
|
||||
TEST_ASSERT_TRUE_MESSAGE(atomic_load(&g.max_running) <= max_inflight,
|
||||
"cap violated across the full run");
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* ── (+) -EAGAIN when the cap is full at submit (work still queued) ── */
|
||||
|
||||
static void test_submit_returns_eagain_when_cap_full(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
const int max_inflight = 1;
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 4, max_inflight));
|
||||
|
||||
/* First task takes the only running slot and parks. */
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, 0, gated_task, &g));
|
||||
TEST_ASSERT_EQUAL_INT(1, wait_entered_at_least(&g, 1, 2000));
|
||||
|
||||
/* Cap is now full: the next submit is deferred -> -EAGAIN, but queued. */
|
||||
int rc = slowlane_submit(&sl, 0, gated_task, &g);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-EAGAIN, rc,
|
||||
"over-cap submit must return -EAGAIN");
|
||||
|
||||
gate_open(&g);
|
||||
slowlane_fini(&sl);
|
||||
/* The deferred task was NOT dropped: both ran. */
|
||||
TEST_ASSERT_EQUAL_INT(2, atomic_load(&g.total_runs));
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* ── (c) per-key dedup returns -EEXIST for an in-flight key ────────── */
|
||||
|
||||
static void test_dedup_returns_eexist_for_inflight_key(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 4, 4));
|
||||
|
||||
const uint64_t KEY = 0x51A8;
|
||||
|
||||
/* First submit of KEY is accepted and runs/parks. */
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, KEY, gated_task, &g));
|
||||
TEST_ASSERT_EQUAL_INT(1, wait_entered_at_least(&g, 1, 2000));
|
||||
|
||||
/* Second submit of the SAME in-flight key is a deduped no-op. */
|
||||
TEST_ASSERT_EQUAL_INT(-EEXIST,
|
||||
slowlane_submit(&sl, KEY, gated_task, &g));
|
||||
|
||||
/* A DIFFERENT key is still admitted. */
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, KEY + 1, gated_task, &g));
|
||||
TEST_ASSERT_EQUAL_INT(2, wait_entered_at_least(&g, 2, 2000));
|
||||
|
||||
gate_open(&g);
|
||||
slowlane_fini(&sl);
|
||||
/* Exactly 2 runs: the duplicate KEY submit did not run a third task. */
|
||||
TEST_ASSERT_EQUAL_INT(2, atomic_load(&g.total_runs));
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* After an in-flight key completes, the SAME key is admittable again. */
|
||||
static void test_dedup_key_reusable_after_completion(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 2, 2));
|
||||
|
||||
const uint64_t KEY = 7;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, KEY, gated_task, &g));
|
||||
TEST_ASSERT_EQUAL_INT(1, wait_entered_at_least(&g, 1, 2000));
|
||||
/* Release and let it fully complete (running drops to 0, key cleared). */
|
||||
gate_open(&g);
|
||||
|
||||
/* Re-close the gate for the second wave. */
|
||||
pthread_mutex_lock(&g.mu);
|
||||
/* wait until the first task left fn */
|
||||
while (atomic_load(&g.running) != 0) {
|
||||
pthread_mutex_unlock(&g.mu);
|
||||
struct timespec s = { 0, 1 * 1000000L };
|
||||
nanosleep(&s, NULL);
|
||||
pthread_mutex_lock(&g.mu);
|
||||
}
|
||||
g.gate_open = 0;
|
||||
atomic_store(&g.entered, 0);
|
||||
pthread_mutex_unlock(&g.mu);
|
||||
|
||||
/* Same key, after completion -> admitted again (0, not -EEXIST). */
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, KEY, gated_task, &g));
|
||||
TEST_ASSERT_EQUAL_INT(1, wait_entered_at_least(&g, 1, 2000));
|
||||
|
||||
gate_open(&g);
|
||||
slowlane_fini(&sl);
|
||||
TEST_ASSERT_EQUAL_INT(2, atomic_load(&g.total_runs));
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* ── (d) fn runs exactly once ──────────────────────────────────────── */
|
||||
|
||||
static void test_fn_runs_exactly_once(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
gate_t g;
|
||||
gate_init(&g);
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 4, 4));
|
||||
|
||||
/* The gate starts open so non-blocking tasks run straight through. */
|
||||
gate_open(&g);
|
||||
|
||||
const int N = 32;
|
||||
for (int i = 0; i < N; i++)
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_submit(&sl, 0, gated_task, &g));
|
||||
|
||||
slowlane_fini(&sl); /* drains + joins -> all submitted work has run */
|
||||
|
||||
/* Exactly N executions: no task dropped, none double-run. */
|
||||
TEST_ASSERT_EQUAL_INT(N, atomic_load(&g.total_runs));
|
||||
gate_destroy(&g);
|
||||
}
|
||||
|
||||
/* ── init/fini hardening: fini on an empty lane, double-fini safety ── */
|
||||
|
||||
static void test_fini_on_idle_lane(void)
|
||||
{
|
||||
slowlane_t sl;
|
||||
TEST_ASSERT_EQUAL_INT(0, slowlane_init(&sl, 2, 2));
|
||||
slowlane_fini(&sl); /* no submits — must drain/join cleanly */
|
||||
TEST_PASS();
|
||||
}
|
||||
|
||||
void test_slowlane(void)
|
||||
{
|
||||
RUN_TEST(test_submit_is_non_blocking);
|
||||
RUN_TEST(test_admission_cap_bounds_concurrency);
|
||||
RUN_TEST(test_submit_returns_eagain_when_cap_full);
|
||||
RUN_TEST(test_dedup_returns_eexist_for_inflight_key);
|
||||
RUN_TEST(test_dedup_key_reusable_after_completion);
|
||||
RUN_TEST(test_fn_runs_exactly_once);
|
||||
RUN_TEST(test_fini_on_idle_lane);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
/* ha.h forward-declares cluster_t; including it here is safe (no circular dep) */
|
||||
#include "commit.h"
|
||||
#include "ha.h"
|
||||
#include "slowlane.h"
|
||||
#include "xce/types.h"
|
||||
#include "xlist.h"
|
||||
#include "zarray.h"
|
||||
|
|
@ -123,6 +124,7 @@ typedef struct cluster_s {
|
|||
persist_t persist; /* Persistence module — P0-F */
|
||||
ha_state_t ha; /* HA role management — P1-5 */
|
||||
commit_engine_t commit; /* Per-entry commit engine — ha-commit-engine 5.3 */
|
||||
slowlane_t slowlane; /* Long-blocking-IO offload lane — raft-drive-async 1.2 */
|
||||
_Atomic int restart_requested; /* set by OP_upgradeConfig handler */
|
||||
pthread_t heartbeat_listener_tid; /* pre-Phase-2 SSL listener thread */
|
||||
_Atomic int heartbeat_listener_running;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue