test(daemon): cover host loop stop on coordination file loss

Add cbm_daemon_host_wait_for_lifetime_for_test, which runs the production
lifetime loop over caller-held coordination handles, and a runtime test that
ages the claim marker, unlinks the cohort lifetime lock, and asserts the tick
refreshes the marker, logs daemon.lifetime_end reason=coordination_file_lost
file=cohort_lease, and stops the runtime. The seam runs an ephemeral
generation, so a missed loss ends at the initial client window instead of
hanging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbYKGdU7pqG6AEuMQuJxiF
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
This commit is contained in:
Shane McCarronandClaude Opus 5.5 committed 2026-09-25 14:24:26 -05:00
1 parent 38fc6ea995
commit fee73ced23
3 files changed
+161

No files matched your search

+16
View File
@@ -985,6 +985,22 @@ static bool host_wait_for_lifetime(cbm_daemon_runtime_service_t *service,
}
}
bool cbm_daemon_host_wait_for_lifetime_for_test(cbm_daemon_runtime_service_t *service,
const cbm_daemon_ipc_endpoint_t *endpoint,
cbm_version_cohort_lease_t *cohort_lease,
cbm_version_cohort_daemon_claim_t *daemon_claim,
cbm_daemon_ipc_participant_guard_t *guard) {
host_state_t host = {0};
atomic_int stop_requested = 0;
host_coordination_t coordination = {
.endpoint = endpoint,
.cohort_lease = cohort_lease,
.daemon_claim = daemon_claim,
.participant_guard = guard,
};
return host_wait_for_lifetime(service, &stop_requested, &host, false, &coordination);
}
static bool host_application_shutdown(host_state_t *host) {
if (cbm_daemon_application_shutdown(host->application, HOST_APPLICATION_SHUTDOWN_MS)) {
return true;
+14
View File
@@ -9,6 +9,10 @@
#include <stdint.h>
struct cbm_daemon_ipc_endpoint;
struct cbm_daemon_ipc_participant_guard;
struct cbm_daemon_runtime_service;
struct cbm_version_cohort_daemon_claim;
struct cbm_version_cohort_lease;
typedef bool (*cbm_daemon_host_cleanup_release_for_test_fn)(void *context);
@@ -68,4 +72,14 @@ bool cbm_daemon_host_http_reconcile_free_refusal_for_test(
* the failure, the adapter must cancel so the final free succeeds. */
bool cbm_daemon_host_http_thread_create_failure_lifecycle_for_test(void);
/* Run the production lifetime loop over caller-held coordination handles with
* no HTTP host state and no stop request. The generation is ephemeral, so a
* loop that misses a loss still ends at the initial client window instead of
* hanging the test. Returns the loop's own result. */
bool cbm_daemon_host_wait_for_lifetime_for_test(
struct cbm_daemon_runtime_service *service, const struct cbm_daemon_ipc_endpoint *endpoint,
struct cbm_version_cohort_lease *cohort_lease,
struct cbm_version_cohort_daemon_claim *daemon_claim,
struct cbm_daemon_ipc_participant_guard *guard);
#endif /* CBM_DAEMON_HOST_INTERNAL_H */
+131
View File
@@ -1262,6 +1262,19 @@ static bool runtime_test_fixture_start_failed(const char *tag, const char *stage
static bool runtime_test_fixture_permanent = false;
typedef struct {
cbm_version_cohort_manager_t *manager;
cbm_version_cohort_lease_t *lease;
cbm_daemon_ipc_participant_guard_t *guard;
cbm_version_cohort_daemon_claim_t *claim;
bool held;
} runtime_test_coordination_t;
/* When set, the fixture takes the host's coordination handles in the host's
* order before starting the service: cohort admission refuses once a
* generation is already serving. */
static runtime_test_coordination_t *runtime_test_fixture_coordination = NULL;
static bool runtime_test_fixture_start_configured(
runtime_test_fixture_t *fixture, const char *tag, const cbm_daemon_build_identity_t *identity,
uint32_t max_clients, uint64_t lease_timeout_ms,
@@ -1317,6 +1330,19 @@ static bool runtime_test_fixture_start_configured(
if (application) {
config.application = *application;
}
runtime_test_coordination_t *coordination = runtime_test_fixture_coordination;
if (coordination) {
cbm_daemon_conflict_t conflict;
coordination->manager = cbm_version_cohort_manager_new(fixture->endpoint);
coordination->held =
coordination->manager &&
cbm_version_cohort_acquire(coordination->manager, identity, cbm_now_ms() + 5000,
&coordination->lease, &conflict) == CBM_VERSION_COHORT_OK &&
cbm_daemon_ipc_participant_guard_try_join(fixture->endpoint, &coordination->guard) ==
1 &&
cbm_version_cohort_daemon_claim_acquire(coordination->manager, &coordination->claim) ==
CBM_VERSION_COHORT_OK;
}
fixture->service = cbm_daemon_runtime_service_start(&config);
if (!fixture->service) {
return runtime_test_fixture_start_failed(tag, "service-start",
@@ -1370,6 +1396,110 @@ TEST(daemon_runtime_convenience_service_owns_participant_guard) {
PASS();
}
#ifndef _WIN32
/* The runtime's threads log concurrently while the loop stops the service, so
* the sink only sets flags. */
static atomic_bool runtime_test_coordination_lost_logged;
static atomic_bool runtime_test_window_expired_logged;
static void runtime_test_lifetime_log_sink(const char *line) {
/* Substrings, not key=value spelling, so text and JSON formats both match. */
if (line && strstr(line, "daemon.lifetime_end") && strstr(line, "coordination_file_lost") &&
strstr(line, "cohort_lease")) {
atomic_store(&runtime_test_coordination_lost_logged, true);
}
if (line && strstr(line, "initial_window_expired")) {
atomic_store(&runtime_test_window_expired_logged, true);
}
}
#endif
/* #2178: the host lifetime loop must refresh every held coordination file on
* its tick and stop the generation once one is lost. The claim marker is aged
* and the cohort lifetime lock unlinked: the tick must refresh the marker
* through its held handle, name the lease (touched after the claim) as lost,
* and stop the runtime. */
TEST(daemon_runtime_host_loop_stops_on_coordination_file_loss) {
#ifdef _WIN32
SKIP_PLATFORM("unlinking a held lock file is POSIX behavior");
#else
cbm_daemon_build_identity_t identity =
runtime_test_identity("2.4.0", runtime_test_self_build());
identity.cache_fingerprint = RUNTIME_CACHE_A; /* cohort admission requires one */
runtime_test_fixture_t fixture;
runtime_test_coordination_t coordination = {0};
runtime_test_fixture_coordination = &coordination;
bool started = runtime_test_fixture_start(&fixture, "coordination-loss", &identity);
runtime_test_fixture_coordination = NULL;
bool held = coordination.held;
char marker[RUNTIME_TEST_PATH_CAP];
char lifetime[RUNTIME_TEST_PATH_CAP];
int marker_written = snprintf(marker, sizeof(marker), "%s/cbm-version-cohort-daemon-v1.lock",
fixture.runtime_dir);
int lifetime_written = snprintf(lifetime, sizeof(lifetime),
"%s/cbm-version-cohort-lifetime-v1.lock", fixture.runtime_dir);
const time_t stale = 1000000000;
struct timespec stale_times[2] = {{.tv_sec = stale}, {.tv_sec = stale}};
bool staged = held && marker_written > 0 && marker_written < (int)sizeof(marker) &&
lifetime_written > 0 && lifetime_written < (int)sizeof(lifetime) &&
utimensat(AT_FDCWD, marker, stale_times, 0) == 0 && unlink(lifetime) == 0;
bool loop_ok = false;
if (staged) {
atomic_store(&runtime_test_coordination_lost_logged, false);
atomic_store(&runtime_test_window_expired_logged, false);
CBMLogLevel previous_log_level = cbm_log_get_level();
cbm_log_set_level(CBM_LOG_INFO);
cbm_log_set_sink(runtime_test_lifetime_log_sink);
loop_ok = cbm_daemon_host_wait_for_lifetime_for_test(
fixture.service, fixture.endpoint, coordination.lease, coordination.claim,
coordination.guard);
cbm_log_set_sink(NULL);
cbm_log_set_level(previous_log_level);
}
cbm_daemon_runtime_service_state_t state =
started ? cbm_daemon_runtime_service_state(fixture.service)
: CBM_DAEMON_RUNTIME_SERVICE_RUNNING;
struct stat marker_status = {0};
bool marker_refreshed = staged && stat(marker, &marker_status) == 0 &&
marker_status.st_mtime > stale && marker_status.st_atime > stale;
/* Host teardown order: service first, then guard, claim and lease. Every
* handle references the endpoint, which the fixture frees. */
if (started && state != CBM_DAEMON_RUNTIME_SERVICE_EXITED) {
(void)cbm_daemon_runtime_service_stop(fixture.service, RUNTIME_TEST_TIMEOUT_MS);
}
if (fixture.service && cbm_daemon_runtime_service_free(fixture.service)) {
fixture.service = NULL;
}
(void)cbm_daemon_ipc_participant_guard_release(&coordination.guard);
while (coordination.claim && cbm_version_cohort_daemon_claim_release(&coordination.claim) !=
CBM_PRIVATE_FILE_LOCK_OK) {
cbm_usleep(1000);
}
while (coordination.lease &&
cbm_version_cohort_lease_release(&coordination.lease) != CBM_PRIVATE_FILE_LOCK_OK) {
cbm_usleep(1000);
}
while (coordination.manager &&
cbm_version_cohort_manager_free(&coordination.manager) != CBM_PRIVATE_FILE_LOCK_OK) {
cbm_usleep(1000);
}
runtime_test_fixture_finish(&fixture);
ASSERT_TRUE(started);
ASSERT_TRUE(held);
ASSERT_TRUE(staged);
ASSERT_TRUE(loop_ok);
ASSERT_EQ(state, CBM_DAEMON_RUNTIME_SERVICE_EXITED);
ASSERT_TRUE(marker_refreshed);
ASSERT_TRUE(atomic_load(&runtime_test_coordination_lost_logged));
ASSERT_FALSE(atomic_load(&runtime_test_window_expired_logged));
PASS();
#endif
}
static bool runtime_test_fixture_start_application(runtime_test_fixture_t *fixture, const char *tag,
const cbm_daemon_build_identity_t *identity,
runtime_application_context_t *context) {
@@ -5106,6 +5236,7 @@ SUITE(daemon_runtime) {
RUN_TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path);
#endif
RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard);
RUN_TEST(daemon_runtime_host_loop_stops_on_coordination_file_loss);
RUN_TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent);
RUN_TEST(daemon_runtime_exact_hello_issues_connection_bound_identity);
RUN_TEST(daemon_runtime_image_rejection_reaches_client_issue1383);