The dispatcher checked each queued task's endpoint busy flag one item at a
time, while the vcpu that finished the endpoint's previous message cleared
the flag without the taskq lock (a lock there deadlocked, bug-1282). Within
one scan, task N was skipped (endpoint still busy), the endpoint freed up,
and task N+1 of the SAME endpoint was dequeued first. Standby controllers
then handled WAL catch-up entry 258 before 257 and NACKed (log mismatch):
26 reorders -> ~576 NACKs in a 45-min production-like run, both standbys
at once at 21:39:24, primary lost its write quorum and answered
jobdispatch/join with E_XCE_NOT_READY, the scheduler client failed over and
rejoined, and the rejoin JOBS_PUSH carried lagging committed state
("refusing resync state ... existing job is RUN" WARNs).
dataq gains dataq_deq_ctx() (a per-scan matcher context). The scan is now
vtask_deq_eligible(taskq, is_blocked): it remembers every endpoint found
busy and keeps all later tasks of that endpoint ineligible for the rest of
the scan (past 64 distinct busy endpoints, every later IO task waits for
the next scan) -- per-endpoint FIFO without any new lock.
Test lib/testsuite/test_vtask_order.c injects is_blocked to free the
endpoint mid-scan: task N+1 was dequeued before the fix, none after; other
endpoints and FIFO/generic tasks unaffected. Suites: lib 1/1, controller
5/5, worker 10/10, scheduler 87/87, harness 5/5. Chaotic HA soak 2/2 PASS
with 0 WAL reorders (2 and 5 per run before), no ASAN.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
177 lines
4.6 KiB
C
177 lines
4.6 KiB
C
/*
|
|
* bug-1864: the vtaskmgr dispatcher checked each queued task's endpoint
|
|
* "busy" flag one item at a time while the vcpu finishing that endpoint's
|
|
* previous message cleared the flag without the queue lock. Within ONE
|
|
* scan, task N was skipped (endpoint still busy), the endpoint unblocked,
|
|
* and task N+1 for the SAME endpoint was then dequeued first -- a standby
|
|
* processed WAL entry seq 258 before 257 and NACKed (log-mismatch storms).
|
|
*
|
|
* vtask_deq_eligible() takes the busy check as a parameter so these tests
|
|
* can make an endpoint unblock part-way through a scan, deterministically.
|
|
*/
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "dataq.h"
|
|
#include "netio.h"
|
|
#include "vtaskmgr.h"
|
|
|
|
static endpoint_t* g_ep1;
|
|
static endpoint_t* g_ep2;
|
|
static int g_ep1_checks;
|
|
|
|
static vtask_t* make_io_task(endpoint_t* ep)
|
|
{
|
|
vtask_t* t = (vtask_t*)calloc(1, sizeof(*t));
|
|
vtask_data_t* d = (vtask_data_t*)calloc(1, sizeof(*d));
|
|
|
|
TEST_ASSERT_NOT_NULL(t);
|
|
TEST_ASSERT_NOT_NULL(d);
|
|
d->vtype = VTASK_IO;
|
|
d->u.iodata.ep = ep;
|
|
t->data = d;
|
|
return t;
|
|
}
|
|
|
|
static vtask_t* make_generic_task(void)
|
|
{
|
|
vtask_t* t = (vtask_t*)calloc(1, sizeof(*t));
|
|
vtask_data_t* d = (vtask_data_t*)calloc(1, sizeof(*d));
|
|
|
|
TEST_ASSERT_NOT_NULL(t);
|
|
TEST_ASSERT_NOT_NULL(d);
|
|
d->vtype = VTASK_GENERIC;
|
|
t->data = d;
|
|
return t;
|
|
}
|
|
|
|
static void free_task(vtask_t* t)
|
|
{
|
|
if (t != NULL) {
|
|
free(t->data);
|
|
free(t);
|
|
}
|
|
}
|
|
|
|
/* ep1 is busy on the first check of this scan, then its running task
|
|
* finishes and it is free for every later check -- the live interleaving */
|
|
static int ep1_unblocks_mid_scan(endpoint_t* ep)
|
|
{
|
|
if (ep == g_ep1) {
|
|
return (g_ep1_checks++ == 0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ep1_busy(endpoint_t* ep) { return (ep == g_ep1); }
|
|
static int none_busy(endpoint_t* ep) { (void)ep; return 0; }
|
|
|
|
static void order_setup(dataq_t* q)
|
|
{
|
|
dataq_init(q, NULL);
|
|
g_ep1 = (endpoint_t*)calloc(1, sizeof(endpoint_t));
|
|
g_ep2 = (endpoint_t*)calloc(1, sizeof(endpoint_t));
|
|
g_ep1_checks = 0;
|
|
}
|
|
|
|
static void order_teardown(dataq_t* q)
|
|
{
|
|
data_item_t* it;
|
|
|
|
while ((it = dataq_deq(q, NULL)) != NULL) {
|
|
free_task(ITPTR_TO_VTASK(it));
|
|
}
|
|
free(g_ep1);
|
|
free(g_ep2);
|
|
}
|
|
|
|
/* the bug: a later message must never overtake an earlier one of the same
|
|
* endpoint, even if the endpoint frees up while the scan is between them */
|
|
static void test_later_task_of_skipped_endpoint_is_not_dequeued(void)
|
|
{
|
|
dataq_t q;
|
|
vtask_t* first;
|
|
vtask_t* second;
|
|
vtask_t* got;
|
|
|
|
order_setup(&q);
|
|
first = make_io_task(g_ep1);
|
|
second = make_io_task(g_ep1);
|
|
dataq_enq(&q, &(first->link));
|
|
dataq_enq(&q, &(second->link));
|
|
|
|
got = vtask_deq_eligible(&q, ep1_unblocks_mid_scan);
|
|
TEST_ASSERT_NULL_MESSAGE(got,
|
|
"task N was skipped as busy, so task N+1 of the same endpoint must "
|
|
"not be dequeued ahead of it in the same scan");
|
|
|
|
/* next scan: the endpoint is free, and N comes out first */
|
|
got = vtask_deq_eligible(&q, none_busy);
|
|
TEST_ASSERT_EQUAL_PTR(first, got);
|
|
free_task(got);
|
|
got = vtask_deq_eligible(&q, none_busy);
|
|
TEST_ASSERT_EQUAL_PTR(second, got);
|
|
free_task(got);
|
|
|
|
order_teardown(&q);
|
|
}
|
|
|
|
/* a busy endpoint must not hold up other endpoints' tasks */
|
|
static void test_other_endpoint_still_dispatched(void)
|
|
{
|
|
dataq_t q;
|
|
vtask_t* a = NULL;
|
|
vtask_t* c = NULL;
|
|
vtask_t* got;
|
|
|
|
order_setup(&q);
|
|
a = make_io_task(g_ep1);
|
|
c = make_io_task(g_ep2);
|
|
dataq_enq(&q, &(a->link));
|
|
dataq_enq(&q, &(c->link));
|
|
|
|
got = vtask_deq_eligible(&q, ep1_busy);
|
|
TEST_ASSERT_EQUAL_PTR(c, got);
|
|
free_task(got);
|
|
|
|
order_teardown(&q);
|
|
}
|
|
|
|
/* FIFO when nothing is busy; non-IO tasks are always dispatchable */
|
|
static void test_fifo_and_generic_tasks(void)
|
|
{
|
|
dataq_t q;
|
|
vtask_t* a;
|
|
vtask_t* b;
|
|
vtask_t* g;
|
|
vtask_t* got;
|
|
|
|
order_setup(&q);
|
|
a = make_io_task(g_ep1);
|
|
b = make_io_task(g_ep1);
|
|
dataq_enq(&q, &(a->link));
|
|
dataq_enq(&q, &(b->link));
|
|
got = vtask_deq_eligible(&q, none_busy);
|
|
TEST_ASSERT_EQUAL_PTR(a, got);
|
|
free_task(got);
|
|
order_teardown(&q);
|
|
|
|
order_setup(&q);
|
|
a = make_io_task(g_ep1);
|
|
g = make_generic_task();
|
|
dataq_enq(&q, &(a->link));
|
|
dataq_enq(&q, &(g->link));
|
|
got = vtask_deq_eligible(&q, ep1_busy);
|
|
TEST_ASSERT_EQUAL_PTR(g, got);
|
|
free_task(got);
|
|
order_teardown(&q);
|
|
}
|
|
|
|
void test_vtask_order(void)
|
|
{
|
|
RUN_TEST(test_later_task_of_skipped_endpoint_is_not_dequeued);
|
|
RUN_TEST(test_other_endpoint_still_dispatched);
|
|
RUN_TEST(test_fifo_and_generic_tasks);
|
|
}
|