mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
38fc6ea995
commit
fee73ced23
3 files changed
+161
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user