fix(controller): stop a failed test from hanging the unit suite (bug-1429); mkstemp for upgrade temp file (bug-1861)

A failing TEST_ASSERT longjmps out and skips the fixture teardown, leaving
the commit engine's sweep thread and persist's fsync_worker asleep on a
static struct. The next test re-inits it underneath them, corrupting the
condvar, and a later teardown blocks in pthread_cond_destroy/join until
the 300s meson timeout. The failure line itself was lost because stdout
was fully buffered.

- controller_test: line-buffer stdout; tearDown() reaps what a failed test
  leaked via new XCE_TESTING registries (persist_test_reap_unstopped, then
  commit_test_reap_unfinalized_engines -- persist_stop before engine fini).
  Clean run reaps 0; an injected failure now fails in 2s instead of hanging.
- persist: flusher_flush_once reports only its own durable advance, so
  fsync_worker no longer re-fires the durable callback for an advance a
  concurrent persist_flush made (flaky "must not re-fire" test). Regression
  test test_flusher_pass_does_not_claim_concurrent_flush_advance.
- tests: jap observer is atomic and waits (bounded) for a continuation that
  may resolve on the flusher thread; pump catch-up tests wait for trailing
  replies instead of a single scan.
- config_repl: upgrade decode temp file via mkstemp instead of a
  pthread_self()-derived /tmp name (same value across processes, opened
  without O_EXCL -- collisions and a symlink target).

Verified: controller_unit_test 1222/1222; 24 parallel runs (6x4), no
hangs or ASAN errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-25 02:54:09 +00:00
parent 44c1981cff
commit b5d59253e5
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
9 changed files with 316 additions and 23 deletions

View file

@ -2032,9 +2032,9 @@ static int jap_read_wal_entry_at(persist_t *p, off_t at,
/* Notify-callback capture (persist_job_commit_notify_fn). */
typedef struct {
int call_count;
uint64_t last_lsn;
int last_rc;
atomic_int call_count;
uint64_t last_lsn;
int last_rc;
} jap_notify_obs_t;
static jap_notify_obs_t g_jap_obs;
@ -2044,12 +2044,34 @@ static void jap_notify_reset(void)
memset(&g_jap_obs, 0, sizeof(g_jap_obs));
}
/* bug-1429: the continuation may resolve on the fsync_worker thread (its
* durable-advance callback re-evaluates commit and can claim the
* continuation between our ACK being recorded and our own resolve), so
* the count is published with release after last_lsn/last_rc. */
static void jap_notify_fn(void *ctx, uint64_t lsn, int rc)
{
jap_notify_obs_t *obs = (jap_notify_obs_t *)ctx;
obs->call_count++;
obs->last_lsn = lsn;
obs->last_rc = rc;
atomic_fetch_add_explicit(&obs->call_count, 1, memory_order_release);
}
/* Wait (bounded) for the continuation count to reach `want`: after the
* covering ACK, resolution is guaranteed but may be running on the
* flusher thread. Returns the count seen. */
static int jap_wait_calls(int want)
{
int i, n = 0;
for (i = 0; i < 2000; i++) { /* <= 2s */
n = atomic_load_explicit(&g_jap_obs.call_count, memory_order_acquire);
if (n >= want)
break;
usleep(1000);
}
/* let a (wrongly) duplicate resolution surface before asserting == */
usleep(2000);
return atomic_load_explicit(&g_jap_obs.call_count, memory_order_acquire);
}
/* ── One WAL append, one commit continuation, decodable back to the same
@ -2138,7 +2160,7 @@ static void test_job_array_submit_appends_exactly_one_wal_entry_and_one_continua
* resolves the single registered continuation */
TEST_ASSERT_EQUAL_INT(0, persist_flush(&g_jap));
repl_on_ack(&g_jap.repl, &g_jaep_a, out_lsn);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jap_obs.call_count,
TEST_ASSERT_EQUAL_INT_MESSAGE(1, jap_wait_calls(1),
"the notify callback must fire EXACTLY ONCE for the whole array, "
"not once per element");
TEST_ASSERT_EQUAL_UINT64(out_lsn, g_jap_obs.last_lsn);
@ -2288,7 +2310,7 @@ static void test_job_array_submit_one_entry_one_continuation_across_sizes(void)
"array_size=%llu: exactly ONE commit continuation must "
"resolve for the whole array, not one per element",
(unsigned long long)array_size);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jap_obs.call_count, msg);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, jap_wait_calls(1), msg);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_jap_obs.last_rc, msg);
}
@ -2450,7 +2472,7 @@ static void test_job_submit_single_payload_wal_type_matches_pre_feature_baseline
"the continuation must not resolve before a covering ACK");
TEST_ASSERT_EQUAL_INT(0, persist_flush(&g_jap));
repl_on_ack(&g_jap.repl, &g_jaep_a, lsns[0]);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jap_obs.call_count,
TEST_ASSERT_EQUAL_INT_MESSAGE(1, jap_wait_calls(1),
"exactly ONE commit continuation must resolve for the single "
"payload, same contract as before this feature");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_jap_obs.last_rc, 0);