Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/daemon/project_lock.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* project_lock.c — Shared daemon/local-CLI project mutation leases. */
#include "daemon/project_lock.h"
#include "daemon/project_lock_internal.h"

#include <stdbool.h>
#include <stdio.h>
Expand All @@ -20,6 +21,11 @@ struct cbm_project_lock_lease {
cbm_lock_lease_t *project_set;
};

bool cbm_project_lock_set_stage_hook_for_test(cbm_project_lock_manager_t *manager,
cbm_lock_registry_stage_hook_fn hook, void *context) {
return manager && cbm_lock_registry_set_stage_hook_for_test(manager->registry, hook, context);
}

static bool project_lock_key(const char *project, char out[PROJECT_LOCK_KEY_CAP]) {
if (!project || !project[0] || strcmp(project, "*") == 0) {
return false;
Expand Down
11 changes: 11 additions & 0 deletions src/daemon/project_lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
typedef struct cbm_project_lock_manager cbm_project_lock_manager_t;
typedef struct cbm_project_lock_lease cbm_project_lock_lease_t;

/* A CLI attempt validates several native lock files before it can publish a
* lease. Windows path/ACL validation can exceed a 100ms scheduling slice. */
static inline uint64_t cbm_project_lock_cli_attempt_deadline(uint64_t now_ms) {
#ifdef _WIN32
const uint64_t timeout_ms = 5000U;
#else
const uint64_t timeout_ms = 100U;
#endif
return now_ms > UINT64_MAX - timeout_ms ? UINT64_MAX : now_ms + timeout_ms;
}

/* Each manager is an independent process-local registry over the endpoint's
* owner-only runtime directory. Separate CBM processes therefore coordinate
* through the same native lock files without sharing memory. */
Expand Down
11 changes: 11 additions & 0 deletions src/daemon/project_lock_internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef CBM_DAEMON_PROJECT_LOCK_INTERNAL_H
#define CBM_DAEMON_PROJECT_LOCK_INTERNAL_H

#include "daemon/project_lock.h"
#include "foundation/lock_registry_internal.h"

/* Forward the existing idle-only registry seam without exposing its handle. */
bool cbm_project_lock_set_stage_hook_for_test(cbm_project_lock_manager_t *manager,
cbm_lock_registry_stage_hook_fn hook, void *context);

#endif /* CBM_DAEMON_PROJECT_LOCK_INTERNAL_H */
3 changes: 1 addition & 2 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,7 @@ static bool main_local_cli_mutation_begin_internal(void *context, const char *pr
cbm_project_lock_lease_t *lease = NULL;
cbm_private_file_lock_status_t status;
if (wait) {
uint64_t now = cbm_now_ms();
uint64_t deadline = now > UINT64_MAX - 100U ? UINT64_MAX : now + 100U;
uint64_t deadline = cbm_project_lock_cli_attempt_deadline(cbm_now_ms());
status = cbm_project_lock_acquire(mutation->manager, project, deadline, NULL, &lease);
} else {
status = cbm_project_lock_try_acquire(mutation->manager, project, &lease);
Expand Down
73 changes: 73 additions & 0 deletions tests/test_project_lock.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "daemon/ipc.h"
#include "daemon/project_lock.h"
#include "daemon/project_lock_internal.h"
#include "foundation/compat_fs.h"
#include "foundation/platform.h"

Expand Down Expand Up @@ -63,6 +64,78 @@ TEST(project_lock_coordinates_instances_projects_wildcard_and_case_aliases) {
PASS();
}

#ifdef _WIN32
static void project_lock_test_slow_native_preparation(void *context,
cbm_private_file_lock_mode_t mode,
cbm_lock_registry_stage_t stage) {
(void)mode;
if (stage == CBM_LOCK_REGISTRY_STAGE_TURN_HELD) {
(*(unsigned int *)context)++;
cbm_usleep(150000);
}
}
#endif

TEST(project_lock_cli_attempt_survives_slow_windows_preparation) {
#ifdef _WIN32
char runtime_parent[PROJECT_LOCK_TEST_PATH_CAP];
(void)snprintf(runtime_parent, sizeof(runtime_parent), "%s/cbm-cli-lock-budget-XXXXXX",
cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(runtime_parent));
cbm_daemon_ipc_endpoint_t *endpoint =
cbm_daemon_ipc_endpoint_new("9876543210abcdef", runtime_parent);
cbm_project_lock_manager_t *manager = cbm_project_lock_manager_new(endpoint);
cbm_project_lock_manager_t *competitor = cbm_project_lock_manager_new(endpoint);
unsigned int delayed_turns = 0;
bool ready = manager && competitor &&
cbm_project_lock_set_stage_hook_for_test(
manager, project_lock_test_slow_native_preparation, &delayed_turns);
cbm_project_lock_lease_t *lease = NULL;
cbm_private_file_lock_status_t old_status =
ready ? cbm_project_lock_acquire(manager, "slow-preparation", cbm_now_ms() + 100U, NULL,
&lease)
: CBM_PRIVATE_FILE_LOCK_IO;
bool old_rolled_back = lease == NULL;
project_lock_test_release(&lease);
cbm_private_file_lock_status_t status =
ready ? cbm_project_lock_acquire(manager, "slow-preparation",
cbm_project_lock_cli_attempt_deadline(cbm_now_ms()), NULL,
&lease)
: CBM_PRIVATE_FILE_LOCK_IO;
bool acquired = status == CBM_PRIVATE_FILE_LOCK_OK && lease;
cbm_project_lock_lease_t *other = NULL;
cbm_private_file_lock_status_t blocked =
acquired ? cbm_project_lock_try_acquire(competitor, "slow-preparation", &other)
: CBM_PRIVATE_FILE_LOCK_IO;
bool competitor_rolled_back = other == NULL;
project_lock_test_release(&other);
project_lock_test_release(&lease);
cbm_private_file_lock_status_t released =
ready ? cbm_project_lock_try_acquire(competitor, "slow-preparation", &other)
: CBM_PRIVATE_FILE_LOCK_IO;
project_lock_test_release(&other);
cbm_private_file_lock_status_t competitor_freed = cbm_project_lock_manager_free(&competitor);
cbm_private_file_lock_status_t manager_freed = cbm_project_lock_manager_free(&manager);
cbm_daemon_ipc_endpoint_free(endpoint);
(void)th_rmtree(runtime_parent);

ASSERT_TRUE(ready);
ASSERT_EQ(old_status, CBM_PRIVATE_FILE_LOCK_BUSY);
ASSERT_TRUE(old_rolled_back);
ASSERT_TRUE(delayed_turns >= 3U);
ASSERT_TRUE(acquired);
ASSERT_EQ(blocked, CBM_PRIVATE_FILE_LOCK_BUSY);
ASSERT_TRUE(competitor_rolled_back);
ASSERT_EQ(released, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_EQ(competitor_freed, CBM_PRIVATE_FILE_LOCK_OK);
ASSERT_EQ(manager_freed, CBM_PRIVATE_FILE_LOCK_OK);
#else
SKIP_PLATFORM("Windows CLI lock preparation budget");
#endif
PASS();
}

SUITE(project_lock) {
RUN_TEST(project_lock_coordinates_instances_projects_wildcard_and_case_aliases);
RUN_TEST(project_lock_cli_attempt_survives_slow_windows_preparation);
}
Loading