Skip to content

Commit d698db8

Browse files
authored
Merge pull request #892 from SS-42/fix/adr-cross-process-lock
fix(mcp): keep ADR reads nonblocking during reindex
2 parents cd23672 + 2d0307b commit d698db8

7 files changed

Lines changed: 409 additions & 71 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,9 +616,11 @@ JSON arguments can also be piped on stdin. Inline JSON remains accepted for back
616616
| `get_code_snippet` | Read source code for a function by qualified name. |
617617
| `get_architecture` | Codebase overview: languages, packages, routes, hotspots, clusters, ADR. |
618618
| `search_code` | Grep-like text search within indexed project files. |
619-
| `manage_adr` | CRUD for Architecture Decision Records. |
619+
| `manage_adr` | CRUD for Architecture Decision Records. Query modes do not wait behind a same-project reindex; writes remain serialized. |
620620
| `ingest_traces` | Ingest runtime traces to validate HTTP_CALLS edges. |
621621

622+
`manage_adr` query modes (`get` and `sections`) use the server's cached query store so they can proceed while a same-project reindex is running. If another process publishes a replacement store during reindexing, they can return the pre-publication ADR until idle eviction refreshes that cache. Updates remain serialized through the project mutation guard.
623+
622624
## Graph Data Model
623625

624626
### Node Labels

‎src/daemon/application.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,12 @@ static bool application_session_mutation_begin(void *context, const char *projec
10091009
application_mutation_begin_internal(session->application, session, project, true);
10101010
}
10111011

1012+
static bool application_session_mutation_try_begin(void *context, const char *project) {
1013+
cbm_daemon_application_session_t *session = context;
1014+
return session &&
1015+
application_mutation_begin_internal(session->application, session, project, false);
1016+
}
1017+
10121018
static void application_session_mutation_end(void *context, const char *project) {
10131019
cbm_daemon_application_session_t *session = context;
10141020
if (session) {
@@ -2246,6 +2252,8 @@ static cbm_daemon_runtime_application_session_t *application_session_open(
22462252
cbm_mcp_server_set_index_executor(session->mcp, application_index_execute, session);
22472253
cbm_mcp_server_set_project_mutation_guard(session->mcp, application_session_mutation_begin,
22482254
application_session_mutation_end, session);
2255+
cbm_mcp_server_set_project_mutation_try_guard(session->mcp,
2256+
application_session_mutation_try_begin);
22492257
session->tool_profile = CBM_MCP_TOOL_PROFILE_ALL;
22502258
session->application = application;
22512259
session->client_id = client_id;

‎src/main.c‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,21 @@ static bool main_test_worker_project_lock_marker(const main_local_cli_mutation_t
251251
}
252252
#endif
253253

254-
static bool main_local_cli_mutation_begin(void *context, const char *project) {
254+
static bool main_local_cli_mutation_begin_internal(void *context, const char *project, bool wait) {
255255
main_local_cli_mutation_t *mutation = context;
256256
if (!mutation || !mutation->manager || !project || !project[0]) {
257257
return false;
258258
}
259259
for (;;) {
260-
uint64_t now = cbm_now_ms();
261-
uint64_t deadline = now > UINT64_MAX - 100U ? UINT64_MAX : now + 100U;
262260
cbm_project_lock_lease_t *lease = NULL;
263-
cbm_private_file_lock_status_t status =
264-
cbm_project_lock_acquire(mutation->manager, project, deadline, NULL, &lease);
261+
cbm_private_file_lock_status_t status;
262+
if (wait) {
263+
uint64_t now = cbm_now_ms();
264+
uint64_t deadline = now > UINT64_MAX - 100U ? UINT64_MAX : now + 100U;
265+
status = cbm_project_lock_acquire(mutation->manager, project, deadline, NULL, &lease);
266+
} else {
267+
status = cbm_project_lock_try_acquire(mutation->manager, project, &lease);
268+
}
265269
if (status == CBM_PRIVATE_FILE_LOCK_OK && lease) {
266270
main_local_cli_lease_t *held = calloc(1, sizeof(*held));
267271
if (held) {
@@ -292,6 +296,9 @@ static bool main_local_cli_mutation_begin(void *context, const char *project) {
292296
"refuse_mutation");
293297
return false;
294298
}
299+
if (!wait) {
300+
return false;
301+
}
295302
if (mutation->feedback && !mutation->waiting_reported) {
296303
(void)fprintf(mutation->feedback, "Waiting for another CBM mutation of %s...\n",
297304
project);
@@ -301,6 +308,14 @@ static bool main_local_cli_mutation_begin(void *context, const char *project) {
301308
}
302309
}
303310

311+
static bool main_local_cli_mutation_begin(void *context, const char *project) {
312+
return main_local_cli_mutation_begin_internal(context, project, true);
313+
}
314+
315+
static bool main_local_cli_mutation_try_begin(void *context, const char *project) {
316+
return main_local_cli_mutation_begin_internal(context, project, false);
317+
}
318+
304319
static void main_local_cli_mutation_end(void *context, const char *project) {
305320
main_local_cli_mutation_t *mutation = context;
306321
if (!mutation || !project) {
@@ -775,6 +790,8 @@ static int run_cli(int argc, char **argv, cbm_project_lock_manager_t *project_lo
775790
if (project_locks) {
776791
cbm_mcp_server_set_project_mutation_guard(srv, main_local_cli_mutation_begin,
777792
main_local_cli_mutation_end, &mutation);
793+
cbm_mcp_server_set_project_mutation_try_guard(srv,
794+
main_local_cli_mutation_try_begin);
778795
}
779796
}
780797
maintenance_binding_failed = srv && !maintenance_context;

‎src/mcp/mcp.c‎

Lines changed: 112 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,7 @@ struct cbm_mcp_server {
15321532
cbm_proc_log_cb index_log_callback;
15331533
void *index_log_context;
15341534
cbm_mcp_project_mutation_begin_fn mutation_begin;
1535+
cbm_mcp_project_mutation_try_begin_fn mutation_try_begin;
15351536
cbm_mcp_project_mutation_end_fn mutation_end;
15361537
void *mutation_context;
15371538
cbm_mcp_quarantine_test_hook_fn quarantine_test_hook;
@@ -1682,14 +1683,27 @@ void cbm_mcp_server_set_project_mutation_guard(cbm_mcp_server_t *srv,
16821683
return;
16831684
}
16841685
srv->mutation_begin = begin;
1686+
srv->mutation_try_begin = NULL;
16851687
srv->mutation_end = end;
16861688
srv->mutation_context = begin ? context : NULL;
16871689
}
16881690

1691+
void cbm_mcp_server_set_project_mutation_try_guard(
1692+
cbm_mcp_server_t *srv, cbm_mcp_project_mutation_try_begin_fn try_begin) {
1693+
if (srv && srv->mutation_begin) {
1694+
srv->mutation_try_begin = try_begin;
1695+
}
1696+
}
1697+
16891698
static bool mcp_project_mutation_begin(cbm_mcp_server_t *srv, const char *project) {
16901699
return !srv->mutation_begin || srv->mutation_begin(srv->mutation_context, project);
16911700
}
16921701

1702+
static bool mcp_project_mutation_try_begin(cbm_mcp_server_t *srv, const char *project) {
1703+
return !srv->mutation_begin ||
1704+
(srv->mutation_try_begin && srv->mutation_try_begin(srv->mutation_context, project));
1705+
}
1706+
16931707
static void mcp_project_mutation_end(cbm_mcp_server_t *srv, const char *project) {
16941708
if (srv->mutation_end) {
16951709
srv->mutation_end(srv->mutation_context, project);
@@ -2045,8 +2059,18 @@ static bool quarantine_corrupt_store(cbm_mcp_server_t *srv, const char *project,
20452059
/* Open the right project's .db file for query tools.
20462060
* Caches the connection — reopens only when project changes.
20472061
* Tracks last-access time so the event loop can evict idle stores. */
2062+
typedef enum {
2063+
STORE_RECOVERY_NONE,
2064+
STORE_RECOVERY_BUSY,
2065+
STORE_RECOVERY_TRY_GUARD_UNAVAILABLE,
2066+
} store_recovery_status_t;
2067+
20482068
static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *project,
2049-
bool mutation_already_held) {
2069+
bool mutation_already_held, bool nonblocking_recovery,
2070+
store_recovery_status_t *recovery_status) {
2071+
if (recovery_status) {
2072+
*recovery_status = STORE_RECOVERY_NONE;
2073+
}
20502074
if (!project) {
20512075
return NULL; /* project is required — no implicit fallback */
20522076
}
@@ -2076,9 +2100,16 @@ static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *pr
20762100
srv->store = NULL;
20772101
bool mutation_acquired = mutation_already_held;
20782102
if (!mutation_acquired) {
2079-
mutation_acquired = mcp_project_mutation_begin(srv, project);
2103+
mutation_acquired = nonblocking_recovery
2104+
? mcp_project_mutation_try_begin(srv, project)
2105+
: mcp_project_mutation_begin(srv, project);
20802106
}
20812107
if (!mutation_acquired) {
2108+
if (nonblocking_recovery && recovery_status) {
2109+
*recovery_status = srv->mutation_try_begin
2110+
? STORE_RECOVERY_BUSY
2111+
: STORE_RECOVERY_TRY_GUARD_UNAVAILABLE;
2112+
}
20822113
return NULL;
20832114
}
20842115

@@ -2142,7 +2173,7 @@ static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *pr
21422173
}
21432174

21442175
static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
2145-
return resolve_store_internal(srv, project, false);
2176+
return resolve_store_internal(srv, project, false, false, NULL);
21462177
}
21472178

21482179
/* Forward decl — definition lives below alongside list_projects. */
@@ -10486,6 +10517,28 @@ static char *adr_read_legacy_file(const char *root_path) {
1048610517
"then draft and store. Sections: PURPOSE, STACK, ARCHITECTURE, " \
1048710518
"PATTERNS, TRADEOFFS, PHILOSOPHY."
1048810519

10520+
/* resolve_store opens file-backed projects query-only. A mutation must release
10521+
* that reader before opening a dedicated writer because atomic publication uses
10522+
* a self-contained DELETE-mode database that switches back to WAL on write. */
10523+
static cbm_store_t *open_adr_store_for_write(cbm_mcp_server_t *srv, cbm_store_t *resolved,
10524+
cbm_store_t **owned_rw) {
10525+
if (!srv || !resolved || !owned_rw) {
10526+
return NULL;
10527+
}
10528+
const char *resolved_db_path = cbm_store_db_path(resolved);
10529+
if (!resolved_db_path) {
10530+
return resolved;
10531+
}
10532+
char *rw_path = heap_strdup(resolved_db_path);
10533+
if (!rw_path) {
10534+
return NULL;
10535+
}
10536+
invalidate_cached_store(srv);
10537+
*owned_rw = cbm_store_open_path(rw_path);
10538+
free(rw_path);
10539+
return *owned_rw;
10540+
}
10541+
1048910542
static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1049010543
char *project = get_project_arg(args);
1049110544
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
@@ -10517,8 +10570,10 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1051710570
true);
1051810571
}
1051910572

10573+
bool write_request =
10574+
content && (strcmp(mode_str, "update") == 0 || strcmp(mode_str, "store") == 0);
1052010575
bool mutation_held = false;
10521-
if (project) {
10576+
if (write_request && project) {
1052210577
mutation_held = mcp_project_mutation_begin(srv, project);
1052310578
if (!mutation_held) {
1052410579
free(project);
@@ -10539,11 +10594,21 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1053910594
/* ADRs are stored in the SQLite store (project_summaries), the SAME
1054010595
* backend the UI /api/adr endpoints use — so writes via the MCP tool and
1054110596
* the UI are visible to each other (#256). */
10542-
cbm_store_t *resolved = resolve_store_internal(srv, project, mutation_held);
10597+
store_recovery_status_t recovery_status = STORE_RECOVERY_NONE;
10598+
cbm_store_t *resolved =
10599+
resolve_store_internal(srv, project, mutation_held, !write_request, &recovery_status);
1054310600
if (!resolved) {
10544-
char *err = build_no_store_error(project);
10545-
char *res = cbm_mcp_text_result(err, true);
10546-
free(err);
10601+
char *res = NULL;
10602+
if (recovery_status == STORE_RECOVERY_BUSY) {
10603+
res = cbm_mcp_text_result("project is busy; retry after indexing", true);
10604+
} else if (recovery_status == STORE_RECOVERY_TRY_GUARD_UNAVAILABLE) {
10605+
res =
10606+
cbm_mcp_text_result("project recovery requires a nonblocking mutation guard", true);
10607+
} else {
10608+
char *err = build_no_store_error(project);
10609+
res = cbm_mcp_text_result(err, true);
10610+
free(err);
10611+
}
1054710612
if (mutation_held) {
1054810613
mcp_project_mutation_end(srv, project);
1054910614
}
@@ -10553,66 +10618,57 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1055310618
return res;
1055410619
}
1055510620

10556-
/* resolve_store opens file-backed projects READ-ONLY (query stores must
10557-
* not mutate the DB). manage_adr is the only resolve_store caller that
10558-
* WRITES, so it needs a writable handle. For a file-backed project open a
10559-
* dedicated read-write handle to the same DB file (the project is verified
10560-
* to exist via resolve_store, so cbm_store_open_path won't create a ghost
10561-
* DB). For an in-memory / embedded store (db_path == NULL) the resolved
10562-
* store is already writable — use it directly. */
1056310621
cbm_store_t *store = resolved;
1056410622
cbm_store_t *owned_rw = NULL;
10565-
const char *resolved_db_path = cbm_store_db_path(resolved);
10566-
if (resolved_db_path) {
10567-
/* Atomic publication leaves a self-contained DELETE-mode database.
10568-
* Opening the writer switches it back to WAL, which requires an
10569-
* exclusive lock and therefore fails while resolve_store's read-only
10570-
* handle is still open. Copy the ACTUAL resolved path first (it may be
10571-
* a drifted-filename fallback), then release that cached reader before
10572-
* opening the writer. Request-scoped queries reopen it when needed. */
10573-
char *rw_path = heap_strdup(resolved_db_path);
10574-
if (!rw_path) {
10575-
if (mutation_held) {
10576-
mcp_project_mutation_end(srv, project);
10577-
}
10578-
free(project);
10579-
free(mode_str);
10580-
free(content);
10581-
return cbm_mcp_text_result("failed to allocate ADR store path", true);
10582-
}
10583-
invalidate_cached_store(srv);
10584-
owned_rw = cbm_store_open_path(rw_path);
10585-
free(rw_path);
10586-
if (!owned_rw) {
10587-
char *err = build_no_store_error(project);
10588-
char *res = cbm_mcp_text_result(err, true);
10589-
free(err);
10623+
if (write_request) {
10624+
store = open_adr_store_for_write(srv, resolved, &owned_rw);
10625+
if (!store) {
1059010626
if (mutation_held) {
1059110627
mcp_project_mutation_end(srv, project);
1059210628
}
1059310629
free(project);
1059410630
free(mode_str);
1059510631
free(content);
10596-
return res;
10632+
return cbm_mcp_text_result("failed to open writable ADR store", true);
1059710633
}
10598-
store = owned_rw;
1059910634
}
1060010635

1060110636
/* One-time migration: older versions wrote ADRs to a file at
10602-
* <root>/.codebase-memory/adr.md. If the store has no ADR yet but that
10603-
* legacy file exists, import it so nothing is lost on upgrade. */
10637+
* <root>/.codebase-memory/adr.md. A read never waits for the project lease:
10638+
* it returns the legacy content immediately and attempts migration only if
10639+
* a nonblocking acquire succeeds. */
1060410640
cbm_adr_t adr;
1060510641
memset(&adr, 0, sizeof(adr));
1060610642
bool have_adr = (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK);
10607-
if (!have_adr) {
10643+
char *legacy = NULL;
10644+
if (!have_adr && !write_request) {
1060810645
char *root_path = project_root_from_store(store, project);
10609-
char *legacy = adr_read_legacy_file(root_path);
10646+
legacy = adr_read_legacy_file(root_path);
1061010647
free(root_path);
10611-
if (legacy) {
10612-
if (cbm_store_adr_store(store, project, legacy) == CBM_STORE_OK) {
10613-
have_adr = (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK);
10648+
if (legacy && mcp_project_mutation_try_begin(srv, project)) {
10649+
if (!mcp_request_cancelled(srv)) {
10650+
/* A publisher may have completed before the lease was granted.
10651+
* File-backed stores must reopen after acquisition and trust
10652+
* only that generation. Embedded stores have no publication
10653+
* boundary, so retain their live handle. */
10654+
if (cbm_store_db_path(resolved)) {
10655+
invalidate_cached_store(srv);
10656+
resolved = NULL;
10657+
store = NULL;
10658+
resolved = resolve_store_internal(srv, project, true, false, NULL);
10659+
}
10660+
if (resolved) {
10661+
store = open_adr_store_for_write(srv, resolved, &owned_rw);
10662+
if (store) {
10663+
have_adr = (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK);
10664+
if (!have_adr &&
10665+
cbm_store_adr_store(store, project, legacy) == CBM_STORE_OK) {
10666+
have_adr = (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK);
10667+
}
10668+
}
10669+
}
1061410670
}
10615-
free(legacy);
10671+
mcp_project_mutation_end(srv, project);
1061610672
}
1061710673
}
1061810674

@@ -10621,7 +10677,8 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1062110677
yyjson_mut_doc_set_root(doc, root_obj);
1062210678

1062310679
bool is_error = false;
10624-
if ((strcmp(mode_str, "update") == 0 || strcmp(mode_str, "store") == 0) && content) {
10680+
const char *adr_content = have_adr ? adr.content : legacy;
10681+
if (write_request) {
1062510682
if (cbm_store_adr_store(store, project, content) == CBM_STORE_OK) {
1062610683
yyjson_mut_obj_add_str(doc, root_obj, "status", "updated");
1062710684
yyjson_mut_obj_add_str(doc, root_obj, "semantics", "whole_document_replaced");
@@ -10630,10 +10687,10 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1063010687
is_error = true;
1063110688
}
1063210689
} else if (strcmp(mode_str, "sections") == 0) {
10633-
adr_list_sections_from_content(doc, root_obj, have_adr ? adr.content : NULL);
10690+
adr_list_sections_from_content(doc, root_obj, adr_content);
1063410691
} else { /* get */
10635-
if (have_adr && adr.content) {
10636-
yyjson_mut_obj_add_strcpy(doc, root_obj, "content", adr.content);
10692+
if (adr_content) {
10693+
yyjson_mut_obj_add_strcpy(doc, root_obj, "content", adr_content);
1063710694
} else {
1063810695
yyjson_mut_obj_add_str(doc, root_obj, "content", "");
1063910696
yyjson_mut_obj_add_str(doc, root_obj, "status", "no_adr");
@@ -10652,6 +10709,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
1065210709
if (mutation_held) {
1065310710
mcp_project_mutation_end(srv, project);
1065410711
}
10712+
free(legacy);
1065510713
free(project);
1065610714
free(mode_str);
1065710715
free(content);

0 commit comments

Comments
 (0)