From 4cb05faa3725f915e9b1d0eadd2bde2ed0d68c80 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 19 Jul 2026 00:59:16 +0200 Subject: [PATCH] fix(daemon): retry the cohort claim across a generation handoff With the Windows client-connection cluster fixed, the smoke's rapid session churn exposed the next layer: each phase starts a fresh first-session daemon, and a just-superseded generation still holds the cohort daemon-claim EX lock for the brief window between going client-terminal (which lets the next generation start) and releasing its cohort locks. claim_acquire was a single non-blocking attempt, so the new daemon failed immediately with daemon.start_failed component=claim and the client saw "daemon could not start within 30000 ms". The host now retries the claim on BUSY for a bounded 12 s handoff window (well under the client's 30 s budget); a persistent BUSY still fails as a real conflict. Cross-platform path, POSIX-syntax verified. Signed-off-by: Martin Vogel --- src/daemon/host.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/daemon/host.c b/src/daemon/host.c index c19f5519e..f5d433bfe 100644 --- a/src/daemon/host.c +++ b/src/daemon/host.c @@ -37,6 +37,12 @@ enum { HOST_APPLICATION_SHUTDOWN_MS = 10000, HOST_COORDINATION_CLEANUP_MS = 500, HOST_INITIAL_CLIENT_TIMEOUT_MS = 10000, + /* A just-superseded daemon generation can still hold the cohort claim for + * the brief window between going client-terminal (which lets the next + * generation start) and releasing its cohort locks. Wait out that handoff + * rather than failing startup on a transient BUSY. Bounded well under the + * client's 30 s daemon-start budget. */ + HOST_DAEMON_CLAIM_TIMEOUT_MS = 12000, HOST_WAIT_TICK_MS = 100, HOST_HTTP_CONFIG_POLL_MS = 1000, HOST_HTTP_RETRY_INITIAL_MS = 1000, @@ -799,8 +805,16 @@ int cbm_daemon_host_run(const cbm_daemon_host_config_t *config) { return -1; } cbm_version_cohort_daemon_claim_t *daemon_claim = NULL; - if (cbm_version_cohort_daemon_claim_acquire(cohort_manager, &daemon_claim) != - CBM_VERSION_COHORT_OK) { + uint64_t claim_deadline = host_deadline_after(HOST_DAEMON_CLAIM_TIMEOUT_MS); + cbm_version_cohort_status_t claim_status = + cbm_version_cohort_daemon_claim_acquire(cohort_manager, &daemon_claim); + while (claim_status == CBM_VERSION_COHORT_BUSY && cbm_now_ms() < claim_deadline) { + /* The previous generation is releasing; retry until the claim is free + * or the handoff window elapses (a persistent BUSY = a real conflict). */ + cbm_usleep(HOST_WAIT_TICK_MS * 1000); + claim_status = cbm_version_cohort_daemon_claim_acquire(cohort_manager, &daemon_claim); + } + if (claim_status != CBM_VERSION_COHORT_OK) { cbm_log_error("daemon.start_failed", "component", "claim"); host_participant_guard_close(&participant_guard); host_daemon_claim_close(&daemon_claim);