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 @@ -1857,6 +1857,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 +1861 to +1863

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

yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
Expand Down
36 changes: 35 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 @@ -272,9 +276,16 @@ void cbm_watcher_free(cbm_watcher_t *w) {
if (!w) {
return;
}
/* Safety net: ensure stopped is set before draining pending_free.
* In production the caller should cbm_watcher_stop() + join first. */
atomic_store(&w->stopped, 1);
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 +333,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 +511,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
3 changes: 2 additions & 1 deletion src/watcher/watcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ typedef int (*cbm_index_fn)(const char *project_name, const char *root_path, voi
* user_data is passed to index_fn. */
cbm_watcher_t *cbm_watcher_new(cbm_store_t *store, cbm_index_fn index_fn, void *user_data);

/* Free the watcher and all per-project state. NULL-safe. */
/* Free the watcher and all per-project state. NULL-safe.
* Precondition: cbm_watcher_stop() + thread join must have completed. */
void cbm_watcher_free(cbm_watcher_t *w);

/* ── Watch list management ──────────────────────────────────────── */
Expand Down
Loading
Loading