Merge pull request #2409 from DeusData/test/daemon-ipc-startup-gate-no-timeout

test(daemon-ipc): wait for the startup gate event, not an attempt budget
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-09-30 22:21:43 +02:00
commit 8b8cafb616
1 file changed
+48 -20
+48 -20
View File
@@ -401,6 +401,7 @@ static int ipc_test_win_lock_child(const char *kind, const char *key, const char
typedef struct {
const cbm_daemon_ipc_endpoint_t *endpoint;
int result;
atomic_bool finished; /* set once the startup call has fully returned */
} ipc_test_win_startup_call_t;
typedef struct {
@@ -421,16 +422,22 @@ static void ipc_test_startup_gate(void *context) {
}
}
static bool ipc_test_startup_gate_wait_acquired(ipc_test_startup_gate_t *gate) {
/* `acquired` never clears, so this wait cannot miss the event; the bound
* only catches a thread that never started at all. */
for (size_t attempt = 0; attempt < 50000U; attempt++) {
if (atomic_load(&gate->acquired)) {
return true;
}
/* Waits until the startup thread has either parked in the gate or returned
* without reaching it. Both flags only ever go false -> true, and one of them
* is always eventually set: before the gate the startup path only try-acquires
* (no blocking wait on anything this test holds), and a thread parked in the
* gate cannot return until the test releases it. So no attempt or time bound
* is needed, and none may decide the verdict: the previous 50000 x
* cbm_usleep(200) budget meant ~10 s on POSIX but, since cbm_usleep(200) is
* Sleep(0) on Windows, only 50000 bare yields there -- a loaded CI runner
* exhausted them before the thread reached the gate. A thread that never
* started is caught by the caller's thread_started check. */
static bool ipc_test_startup_gate_wait_acquired(ipc_test_startup_gate_t *gate,
ipc_test_win_startup_call_t *call) {
while (!atomic_load(&gate->acquired) && !atomic_load(&call->finished)) {
cbm_usleep(200);
}
return false;
return atomic_load(&gate->acquired);
}
static void *ipc_test_win_startup_call(void *opaque) {
@@ -441,6 +448,7 @@ static void *ipc_test_win_startup_call(void *opaque) {
call->result = 0;
}
cbm_daemon_ipc_startup_lock_release(&startup);
atomic_store(&call->finished, true);
return NULL;
}
@@ -1395,6 +1403,7 @@ TEST(daemon_ipc_windows_startup_retries_transient_rendezvous_reader) {
bool startup_observed = false;
int join_status = -1;
ipc_test_win_startup_call_t call = {.result = -1};
atomic_init(&call.finished, false);
ipc_test_startup_gate_t gate;
atomic_init(&gate.acquired, false);
atomic_init(&gate.may_proceed, false);
@@ -1420,7 +1429,7 @@ TEST(daemon_ipc_windows_startup_retries_transient_rendezvous_reader) {
thread_started = cbm_thread_create(&thread, 0, ipc_test_win_startup_call, &call) == 0;
}
if (thread_started) {
startup_observed = ipc_test_startup_gate_wait_acquired(&gate);
startup_observed = ipc_test_startup_gate_wait_acquired(&gate, &call);
}
ipc_test_win_lock_release(&record_reader);
atomic_store(&gate.may_proceed, true);
@@ -1441,7 +1450,14 @@ TEST(daemon_ipc_windows_startup_retries_transient_rendezvous_reader) {
ASSERT_EQ(directory_status, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_EQ(record_status, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(startup_observed);
if (!startup_observed) {
/* The startup thread returned without reaching the gate; its
* try-acquire status is the true reason. */
char reason[96];
(void)snprintf(reason, sizeof(reason), "startup call returned %d without reaching the gate",
call.result);
FAIL(reason);
}
ASSERT_EQ(join_status, 0);
ASSERT_EQ(call.result, 1);
ASSERT_TRUE(address[0] != '\0');
@@ -1464,6 +1480,7 @@ TEST(daemon_ipc_windows_rendezvous_bridges_concurrent_lifetime_owner) {
bool startup_observed = false;
int join_status = -1;
ipc_test_win_startup_call_t call = {.result = -1};
atomic_init(&call.finished, false);
bool parent_ok = ipc_test_parent_new(parent, "win-lifetime-bridge");
if (parent_ok) {
@@ -1501,7 +1518,7 @@ TEST(daemon_ipc_windows_rendezvous_bridges_concurrent_lifetime_owner) {
thread_started = cbm_thread_create(&thread, 0, ipc_test_win_startup_call, &call) == 0;
}
if (thread_started) {
startup_observed = ipc_test_startup_gate_wait_acquired(&gate);
startup_observed = ipc_test_startup_gate_wait_acquired(&gate, &call);
}
/* Claim the lifetime lock while startup is parked in the gate, so the
* concurrent-owner bridge under test is exercised deterministically. */
@@ -1534,7 +1551,14 @@ TEST(daemon_ipc_windows_rendezvous_bridges_concurrent_lifetime_owner) {
ASSERT_EQ(directory_status, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_EQ(record_status, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(startup_observed);
if (!startup_observed) {
/* The startup thread returned without reaching the gate; its
* try-acquire status is the true reason. */
char reason[96];
(void)snprintf(reason, sizeof(reason), "startup call returned %d without reaching the gate",
call.result);
FAIL(reason);
}
ASSERT_EQ(lifetime_status, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_EQ(join_status, 0);
ASSERT_EQ(call.result, 0);
@@ -2442,6 +2466,7 @@ typedef struct {
cbm_daemon_ipc_listener_t *listener;
cbm_daemon_ipc_connection_t *connection;
atomic_bool waiting;
atomic_bool finished; /* set once the server thread has returned */
int receive_result;
} ipc_forever_wait_server_t;
@@ -2464,6 +2489,7 @@ static void *ipc_forever_wait_server(void *opaque) {
server->connection, CBM_DAEMON_IPC_WAIT_FOREVER, &frame, &payload);
}
free(payload);
atomic_store_explicit(&server->finished, true, memory_order_release);
return NULL;
}
@@ -2480,6 +2506,7 @@ TEST(daemon_ipc_wait_forever_is_interruptible) {
int join_result = -1;
ipc_forever_wait_server_t server = {0};
atomic_init(&server.waiting, false);
atomic_init(&server.finished, false);
if (ipc_test_parent_new(parent, "wait-forever")) {
endpoint = cbm_daemon_ipc_endpoint_new(key, parent);
@@ -2495,9 +2522,11 @@ TEST(daemon_ipc_wait_forever_is_interruptible) {
if (thread_started) {
client = cbm_daemon_ipc_connect(endpoint, 2000);
}
uint64_t deadline = cbm_now_ms() + 2000;
while (client && cbm_now_ms() < deadline &&
!atomic_load_explicit(&server.waiting, memory_order_acquire)) {
/* Wait for the event itself, never a deadline: the server either enters
* its forever-wait or returns (its accept is bounded by the call's own
* timeout), and both flags only ever go false -> true. */
while (client && !atomic_load_explicit(&server.waiting, memory_order_acquire) &&
!atomic_load_explicit(&server.finished, memory_order_acquire)) {
cbm_usleep(1000);
}
reached_wait = atomic_load_explicit(&server.waiting, memory_order_acquire);
@@ -2561,14 +2590,13 @@ TEST(daemon_ipc_connect_waits_for_delayed_listener) {
thread_started = cbm_thread_create(&thread, 0, ipc_delayed_listener_start, &delayed) == 0;
}
if (thread_started) {
/* `delay_started` is the thread's first store, so it cannot return
* without setting it: wait for that event with no attempt bound. */
struct timespec poll_delay = {.tv_sec = 0, .tv_nsec = 1000000};
for (size_t i = 0; i < 2000; i++) {
if (atomic_load_explicit(&delayed.delay_started, memory_order_acquire)) {
delay_observed = true;
break;
}
while (!atomic_load_explicit(&delayed.delay_started, memory_order_acquire)) {
(void)cbm_nanosleep(&poll_delay, NULL);
}
delay_observed = true;
}
if (delay_observed) {
client = cbm_daemon_ipc_connect(endpoint, 1500);