Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,11 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
}

cbm_pipeline_unlock();

if (srv->watcher) {
cbm_watcher_unwatch(srv->watcher, name);
}
Comment on lines +1815 to +1817

cbm_mem_collect(); /* return freed pages to OS after closing database */

yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
Expand Down
33 changes: 32 additions & 1 deletion src/watcher/watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ struct cbm_watcher {
CBMHashTable *projects; /* name → project_state_t* */
cbm_mutex_t projects_lock;
atomic_int stopped;
/* Deferred-free list: freed after the next poll_once. */
project_state_t **pending_free;
int pending_free_count;
int pending_free_cap;
};

/* ── Constants ─────────────────────────────────────────────────── */
Expand Down Expand Up @@ -275,6 +279,10 @@ void cbm_watcher_free(cbm_watcher_t *w) {
cbm_mutex_lock(&w->projects_lock);
cbm_ht_foreach(w->projects, free_state_entry, NULL);
cbm_ht_free(w->projects);
for (int i = 0; i < w->pending_free_count; i++) {
state_free(w->pending_free[i]);
}
free(w->pending_free);
cbm_mutex_unlock(&w->projects_lock);
cbm_mutex_destroy(&w->projects_lock);
free(w);
Expand Down Expand Up @@ -322,7 +330,23 @@ void cbm_watcher_unwatch(cbm_watcher_t *w, const char *project_name) {
project_state_t *s = cbm_ht_get(w->projects, project_name);
if (s) {
cbm_ht_delete(w->projects, project_name);
state_free(s);
/* Defer free: the state may still be referenced by a poll_once
* snapshot taken before we acquired the lock. poll_once will
* drain this list at the start of its next cycle. */
if (w->pending_free_count >= w->pending_free_cap) {
int new_cap = w->pending_free_cap ? w->pending_free_cap * 2 : 8;
project_state_t **tmp =
realloc(w->pending_free, (size_t)new_cap * sizeof(project_state_t *));
if (tmp) {
w->pending_free = tmp;
w->pending_free_cap = new_cap;
}
}
if (w->pending_free_count < w->pending_free_cap) {
w->pending_free[w->pending_free_count++] = s;
} else {
state_free(s); /* realloc failed — fall back to immediate free */
}
removed = true;
}
cbm_mutex_unlock(&w->projects_lock);
Expand Down Expand Up @@ -484,6 +508,13 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) {
* This keeps the critical section small — poll_project does git I/O
* and may invoke index_fn which runs the full pipeline. */
cbm_mutex_lock(&w->projects_lock);

/* Free deferred entries from the previous cycle. */
for (int i = 0; i < w->pending_free_count; i++) {
state_free(w->pending_free[i]);
}
w->pending_free_count = 0;

int n = cbm_ht_count(w->projects);
if (n == 0) {
cbm_mutex_unlock(&w->projects_lock);
Expand Down