Skip to content

test(daemon-ipc): wait for the startup gate event, not an attempt budget - #2409

Merged
DeusData merged 1 commit into
mainfrom
test/daemon-ipc-startup-gate-no-timeout
Sep 30, 2026
Merged

DeusData merged 1 commit into
mainfrom
test/daemon-ipc-startup-gate-no-timeout

Conversation

@DeusData

Copy link
Copy Markdown
Owner

What

Three waits in tests/test_daemon_ipc.c let a bound decide the verdict. Each now waits for its event instead: either the thing it waits for happens, or the thread returns without it.

  • ipc_test_startup_gate_wait_acquired (both Windows rendezvous tests) now waits for acquired OR a new finished flag set by ipc_test_win_startup_call. A thread that returns without reaching the gate fails immediately and reports the startup call's return value.
  • daemon_ipc_wait_forever_is_interruptible: the 2 s wall-clock deadline is replaced by a wait for waiting OR finished.
  • daemon_ipc_connect_waits_for_delayed_listener: the 2000-poll cap is removed. delay_started is the thread's first store, so the wait needs no cap.

No production change and no new sleep.

Why

On Windows, cbm_usleep(us) is Sleep(us / 1000). The gate helper's 50000 x cbm_usleep(200) budget was meant to be about 10 s, but it is really 50000 × Sleep(0): bare yields that a loaded runner can spend in milliseconds. That is how daemon_ipc_windows_startup_retries_transient_rendezvous_reader failed ASSERT(startup_observed) on windows-latest CLANG64 (run 36354336229, job 108779549114; the whole suite took 7 s).

The old wait_forever deadline was worse than a flake. Once it expired, the test skipped the interrupt and hung in the join.

Evidence

  • Harness with the Windows sleep model:
    • Thread arriving after 50000 polls: the old helper fails 20/20 (call.result=1, the CI signature); the new one passes 20/20.
    • Thread descheduled for 500 ms: the old helper gives up after 9 ms and fails 10/10; the new one passes 10/10.
    • Control, the old helper with POSIX usleep: passes 10/10.
    • Thread that never reaches the gate: the new helper fails within 11 ms.
  • macOS daemon_ipc with threads held 4 s and 6 s:
    • New code: 53 passed.
    • Old 2000-poll loop: fails ASSERT(delay_observed).
    • Old 2 s deadline: hangs in the join.
  • Final tree:
    • macOS daemon_ipc: 53 passed, 2 platform skips, 4.0 s.
    • lint-ci is clean.
    • The Windows cross-compile (llvm-mingw, -Werror) is clean.
    • The real-Windows VM leg was not run (VM down); this PR's own Windows CI legs cover it.

Found attributing a Windows CI red on #1781.

daemon_ipc_windows_startup_retries_transient_rendezvous_reader failed on
a windows-latest CLANG64 runner (ASSERT(startup_observed); the whole
suite took 7 s) on a PR that touched only CLI files.

ipc_test_startup_gate_wait_acquired polled 50000 x cbm_usleep(200) and
returned false once the budget ran out. On POSIX that is ~10 s, but on
Windows cbm_usleep(us) is Sleep(us / 1000), so cbm_usleep(200) is
Sleep(0): the budget collapsed to 50000 bare yields, which a loaded
runner spends in milliseconds before the startup thread reaches the
gate. A timeout decided the verdict.

The helper now waits until the startup thread has either parked in the
gate or returned without reaching it (a new `finished` flag, set by
ipc_test_win_startup_call after its call returns). Both flags only go
false -> true and one of them is always set: before the gate the startup
path only try-acquires, and a thread parked in the gate cannot return
until the test releases it. No bound is left to decide the verdict. A
thread that returns without reaching the gate fails at once, and the
test reports its try-acquire status as the reason.

The same shape is removed from two cross-platform tests:

- daemon_ipc_wait_forever_is_interruptible waited for the server's
  forever-wait under a 2 s wall-clock deadline. Past the deadline the
  test skipped the interrupt, so the join then blocked on a server
  parked in WAIT_FOREVER: a hang, not a failure. It now waits for
  `waiting` or for a new `finished` flag set when the server thread
  returns.
- daemon_ipc_connect_waits_for_delayed_listener capped its wait for
  `delay_started` at 2000 polls. That store is the thread's first
  action, so the wait needs no attempt bound.

No production change and no new sleep: the existing per-poll yields
stay.

Proof. A standalone harness mirrors the gate, both helpers and the
thread wrapper, with cbm_usleep modelled as on Windows
(Sleep(us / 1000), so cbm_usleep(200) is a bare yield):
- startup thread arriving only after 50000 polls: old helper 20/20 FAIL
  (startup_observed=false with call.result=1, the CI signature), new
  helper 20/20 PASS;
- startup thread descheduled 500 ms: the old helper gives up after 9 ms,
  10/10 FAIL; new helper 10/10 PASS; control, old helper with POSIX
  usleep: 10/10 PASS;
- thread returning without reaching the gate: the new helper fails
  within 11 ms with call.result=-1 (the old one took 16 s under POSIX).

Real daemon_ipc suite on macOS: with the server and listener threads
held 4 s and 6 s (beyond both old bounds) the two cross-platform tests
pass. The old 2000-poll loop under the 6 s hold fails
ASSERT(delay_observed). The old 2 s deadline under the 4 s hold hangs in
the join until killed. A server that skips accept makes the new loop
fail ASSERT(reached_wait) with no added wait.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit 8b8cafb into main Sep 30, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant