2020#include "foundation/log.h"
2121#include "foundation/hash_table.h"
2222#include "foundation/compat.h"
23+ #include "foundation/compat_thread.h"
2324#include "foundation/compat_fs.h"
2425#include "foundation/str_util.h"
2526
@@ -50,6 +51,7 @@ struct cbm_watcher {
5051 cbm_index_fn index_fn ;
5152 void * user_data ;
5253 CBMHashTable * projects ; /* name → project_state_t* */
54+ cbm_mutex_t projects_lock ;
5355 atomic_int stopped ;
5456};
5557
@@ -236,6 +238,7 @@ cbm_watcher_t *cbm_watcher_new(cbm_store_t *store, cbm_index_fn index_fn, void *
236238 w -> index_fn = index_fn ;
237239 w -> user_data = user_data ;
238240 w -> projects = cbm_ht_create (CBM_SZ_32 );
241+ cbm_mutex_init (& w -> projects_lock );
239242 atomic_init (& w -> stopped , 0 );
240243 return w ;
241244}
@@ -244,8 +247,11 @@ void cbm_watcher_free(cbm_watcher_t *w) {
244247 if (!w ) {
245248 return ;
246249 }
250+ cbm_mutex_lock (& w -> projects_lock );
247251 cbm_ht_foreach (w -> projects , free_state_entry , NULL );
248252 cbm_ht_free (w -> projects );
253+ cbm_mutex_unlock (& w -> projects_lock );
254+ cbm_mutex_destroy (& w -> projects_lock );
249255 free (w );
250256}
251257
@@ -264,25 +270,38 @@ void cbm_watcher_watch(cbm_watcher_t *w, const char *project_name, const char *r
264270 }
265271
266272 /* Remove old entry first (key points to state's project_name) */
273+ cbm_mutex_lock (& w -> projects_lock );
267274 project_state_t * old = cbm_ht_get (w -> projects , project_name );
268275 if (old ) {
269276 cbm_ht_delete (w -> projects , project_name );
270277 state_free (old );
271278 }
272279
273280 project_state_t * s = state_new (project_name , root_path );
281+ if (!s ) {
282+ cbm_mutex_unlock (& w -> projects_lock );
283+ cbm_log_warn ("watcher.watch.oom" , "project" , project_name , "path" , root_path );
284+ return ;
285+ }
274286 cbm_ht_set (w -> projects , s -> project_name , s );
287+ cbm_mutex_unlock (& w -> projects_lock );
275288 cbm_log_info ("watcher.watch" , "project" , project_name , "path" , root_path );
276289}
277290
278291void cbm_watcher_unwatch (cbm_watcher_t * w , const char * project_name ) {
279292 if (!w || !project_name ) {
280293 return ;
281294 }
295+ bool removed = false;
296+ cbm_mutex_lock (& w -> projects_lock );
282297 project_state_t * s = cbm_ht_get (w -> projects , project_name );
283298 if (s ) {
284299 cbm_ht_delete (w -> projects , project_name );
285300 state_free (s );
301+ removed = true;
302+ }
303+ cbm_mutex_unlock (& w -> projects_lock );
304+ if (removed ) {
286305 cbm_log_info ("watcher.unwatch" , "project" , project_name );
287306 }
288307}
@@ -291,18 +310,23 @@ void cbm_watcher_touch(cbm_watcher_t *w, const char *project_name) {
291310 if (!w || !project_name ) {
292311 return ;
293312 }
313+ cbm_mutex_lock (& w -> projects_lock );
294314 project_state_t * s = cbm_ht_get (w -> projects , project_name );
295315 if (s ) {
296316 /* Reset backoff — poll immediately on next cycle */
297317 s -> next_poll_ns = 0 ;
298318 }
319+ cbm_mutex_unlock (& w -> projects_lock );
299320}
300321
301322int cbm_watcher_watch_count (const cbm_watcher_t * w ) {
302323 if (!w ) {
303324 return 0 ;
304325 }
305- return (int )cbm_ht_count (w -> projects );
326+ cbm_mutex_lock (& ((cbm_watcher_t * )w )-> projects_lock );
327+ int count = (int )cbm_ht_count (w -> projects );
328+ cbm_mutex_unlock (& ((cbm_watcher_t * )w )-> projects_lock );
329+ return count ;
306330}
307331
308332/* ── Single poll cycle ──────────────────────────────────────────── */
@@ -411,17 +435,53 @@ static void poll_project(const char *key, void *val, void *ud) {
411435 s -> next_poll_ns = ctx -> now + ((int64_t )s -> interval_ms * US_PER_MS );
412436}
413437
438+ /* Callback to snapshot project state pointers into an array. */
439+ typedef struct {
440+ project_state_t * * items ;
441+ int count ;
442+ int cap ;
443+ } snapshot_ctx_t ;
444+
445+ static void snapshot_project (const char * key , void * val , void * ud ) {
446+ (void )key ;
447+ snapshot_ctx_t * sc = ud ;
448+ if (val && sc -> count < sc -> cap ) {
449+ sc -> items [sc -> count ++ ] = val ;
450+ }
451+ }
452+
414453int cbm_watcher_poll_once (cbm_watcher_t * w ) {
415454 if (!w ) {
416455 return 0 ;
417456 }
418457
458+ /* Snapshot project pointers under lock, then poll without holding it.
459+ * This keeps the critical section small — poll_project does git I/O
460+ * and may invoke index_fn which runs the full pipeline. */
461+ cbm_mutex_lock (& w -> projects_lock );
462+ int n = cbm_ht_count (w -> projects );
463+ if (n == 0 ) {
464+ cbm_mutex_unlock (& w -> projects_lock );
465+ return 0 ;
466+ }
467+ project_state_t * * snap = malloc (n * sizeof (project_state_t * ));
468+ if (!snap ) {
469+ cbm_mutex_unlock (& w -> projects_lock );
470+ return 0 ;
471+ }
472+ snapshot_ctx_t sc = {.items = snap , .count = 0 , .cap = n };
473+ cbm_ht_foreach (w -> projects , snapshot_project , & sc );
474+ cbm_mutex_unlock (& w -> projects_lock );
475+
419476 poll_ctx_t ctx = {
420477 .w = w ,
421478 .now = now_ns (),
422479 .reindexed = 0 ,
423480 };
424- cbm_ht_foreach (w -> projects , poll_project , & ctx );
481+ for (int i = 0 ; i < sc .count ; i ++ ) {
482+ poll_project (NULL , snap [i ], & ctx );
483+ }
484+ free (snap );
425485 return ctx .reindexed ;
426486}
427487
0 commit comments