From 46b77c3e3b267a6471ca2f6bf0b668d7c154a52e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 28 Jul 2026 01:02:41 +0200 Subject: [PATCH] test(daemon-app): wait for the stable terminal state, not the draining window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fresh-request guard test waited for 'job terminal AND >= N/2 subscribers still attached'. Publish flips terminal and releases the blocked prior requests in the same locked step, so that conjunction is a transient window whose width is thread-wakeup scheduling: the busy-spin caught it by luck, a 1ms-yield poll (#1307) sampled past it on fast x64 runners, and no budget can pin it (release runs 30305464193 and 30309182389 failed it from both directions). The production guard never consults subscriber counts — application_find_active_job_locked skips any terminal job — and the test's downstream assertions (starts==2, destroys==2, stale/fresh response separation) catch a terminal-job reuse in every interleaving. So wait only for the stable end-state (active jobs == 0) and drop the racy helper. 47/47 locally. Signed-off-by: Martin Vogel --- tests/test_daemon_application.c | 36 +++++++++++---------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/tests/test_daemon_application.c b/tests/test_daemon_application.c index 934944c90..cda557ea5 100644 --- a/tests/test_daemon_application.c +++ b/tests/test_daemon_application.c @@ -1462,23 +1462,6 @@ static bool app_wait_for_active_jobs(cbm_daemon_application_t *application, size return false; } -static bool app_wait_for_terminal_job_with_subscribers(cbm_daemon_application_t *application, - const char *project, - size_t minimum_subscribers) { - uint64_t deadline = cbm_now_ms() + APP_TEST_TIMEOUT_MS; - while (cbm_now_ms() < deadline) { - if (cbm_daemon_application_active_jobs(application) == 0 && - cbm_daemon_application_job_subscribers(application, project) >= minimum_subscribers) { - return true; - } - /* Yield like every sibling wait helper: a sleepless spin pins a core - * and can starve the daemon threads it polls on scarce-CPU runners - * (windows-11-arm release leg, run 30305464193). */ - cbm_usleep(1000); - } - return false; -} - typedef struct { cbm_daemon_application_t *application; const char *project; @@ -2646,16 +2629,21 @@ TEST(daemon_application_fresh_request_does_not_reuse_terminal_subscribed_job) { bool first_worker_ready_to_publish = all_subscribed && app_wait_for_atomic_int(&fake.destroys, 1); atomic_store(&fake.release_destroy, true); - bool terminal_with_prior_subscribers = - first_worker_ready_to_publish && - app_wait_for_terminal_job_with_subscribers(application, project, PRIOR_SUBSCRIBERS / 2U); + /* Wait only for the stable end-state. Publish flips terminal and lets the + * blocked prior requests drain in the same breath, so "terminal AND still + * subscribed" is a transient window no poll cadence can pin (release runs + * 30305464193 and 30309182389 missed it from both directions). The + * production guard ignores subscriber counts — find_active_job skips any + * terminal job — and the stale/fresh assertions below catch a reuse in + * every interleaving. */ + bool job_terminal = first_worker_ready_to_publish && app_wait_for_active_jobs(application, 0); uint8_t *fresh = NULL; uint32_t fresh_length = 0; cbm_daemon_runtime_application_status_t fresh_status = - terminal_with_prior_subscribers ? app_test_request(&callbacks, sessions[PRIOR_SUBSCRIBERS], - tool, tool_length, &fresh, &fresh_length) - : CBM_DAEMON_RUNTIME_APPLICATION_REJECTED; + job_terminal ? app_test_request(&callbacks, sessions[PRIOR_SUBSCRIBERS], tool, tool_length, + &fresh, &fresh_length) + : CBM_DAEMON_RUNTIME_APPLICATION_REJECTED; for (size_t i = 0; i < PRIOR_SUBSCRIBERS; i++) { if (started[i]) { (void)cbm_thread_join(&threads[i]); @@ -2672,7 +2660,7 @@ TEST(daemon_application_fresh_request_does_not_reuse_terminal_subscribed_job) { ASSERT_TRUE(setup); ASSERT_TRUE(all_subscribed); ASSERT_TRUE(first_worker_ready_to_publish); - ASSERT_TRUE(terminal_with_prior_subscribers); + ASSERT_TRUE(job_terminal); ASSERT_EQ(fresh_status, CBM_DAEMON_RUNTIME_APPLICATION_OK); ASSERT_EQ(atomic_load(&fake.starts), 2); ASSERT_EQ(atomic_load(&fake.destroys), 2);