Skip to content

feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)#817

Open
DeusData wants to merge 2 commits into
mainfrom
distill/738-watcher-prune
Open

feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)#817
DeusData wants to merge 2 commits into
mainfrom
distill/738-watcher-prune

Conversation

@DeusData

@DeusData DeusData commented Jul 3, 2026

Copy link
Copy Markdown
Owner

feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)

What does this PR do?

Implements the stale-root cleanup from #286: the watcher now prunes a watched
project whose root directory has genuinely disappeared — the cached project DB
(<name>.db plus -wal/-shm) is deleted and the watch entry removed — so
vanished worktrees stop being watched (and re-stat'ed) forever.

Distilled from #738 (thanks @pcristin), with safety hardening on top of the
original mechanism. The cached DB can hold user-authored data (e.g. the
project ADR) that is unrecoverable once deleted, so pruning is deliberately
conservative:

  • ENOENT-only classification — a poll counts as "missing" only when
    stat() fails with ENOENT/ENOTDIR (or the root exists but is no longer
    a directory). Any other failure — EACCES, EIO, a transient network
    mount, macOS TCC permission revocation — resets the streak and is logged
    distinctly as watcher.root_stat_error errno=..., never counting toward
    deletion. Windows (mingw/UCRT) maps ERROR_FILE_NOT_FOUND/
    ERROR_PATH_NOT_FOUND to ENOENT, so the same check holds there (same
    convention as find_deleted_files in pipeline_incremental.c).
  • Sustained-absence grace window — pruning requires BOTH at least 3
    consecutive missing polls AND at least CBM_WATCHER_PRUNE_GRACE_S seconds
    (default 600 = 10 minutes, env-overridable) elapsed since the streak's
    first missing observation (tracked via monotonic cbm_now_ms()).
  • Reappearance resets — if the root comes back, the streak and its
    first-miss timestamp reset (watcher.root_restored); a fresh uninterrupted
    streak is required to prune.
  • Deferred free — the pruned entry is released through the deferred-free
    list introduced in fix: unwatch deleted projects to prevent zombie reindex #537 for cbm_watcher_unwatch (drained by the next
    poll_once), keeping one consistent freeing model for poll snapshots.

DB deletion goes through cbm_validate_project_name and builds paths only
inside cbm_resolve_cache_dir().

Tests

  • watcher_prunes_sustained_missing_root — positive prune path (grace 0s
    isolates the streak threshold): misses 1–2 keep project + DB, miss 3 prunes
    the watch entry and deletes db/-wal/-shm. Red-first verified: on
    origin/main this fails (nothing prunes); green with the fix.
  • watcher_grace_window_blocks_prune — 4 fast missing polls with a 600s
    window: streak threshold reached, time gate not → NOT pruned, DB intact.
  • watcher_root_restore_resets_prune_streak — 2 misses, root restored,
    2 misses again → not pruned (streak restarted); 3rd miss of the new streak
    prunes. Red-first verified on origin/main.
  • watcher_root_missing_errno_classification — unit test of the errno
    classifier with injected values (ENOENT/ENOTDIR → missing; 0,
    EACCES, EIO, EINVAL, ENAMETOOLONG → not). A real EACCES cannot be
    simulated portably (root on CI, Windows ACLs), hence the injected-errno
    unit test on the factored classifier.

Known limitation (honest residual of #286)

Only currently-watched projects are pruned. Stale cached DBs left behind
by older sessions (projects never watched in this server's lifetime) are out
of scope for this change.

Validation

  • make -f Makefile.cbm cbm-Werror clean
  • make -f Makefile.cbm lint-ci — clean
  • ./build/c/test-runner watcher — 58 passed, 0 failed (ASan/UBSan runner)

Closes #286.

DeusData and others added 2 commits July 3, 2026 20:40
…grace window)

Distilled from #738: the watcher now prunes a watched project whose root
directory has genuinely disappeared - deleting the cached DB (+wal/shm,
validated name, cache-dir-only paths) and removing the watch entry - so
vanished worktrees stop being watched forever (#286).

Hardened beyond the original PR to remove a data-loss hazard (the cached
DB can hold user-authored data such as the ADR, unrecoverable once
deleted):

- Only ENOENT/ENOTDIR stat failures count as missing. Any other failure
  (EACCES, EIO, transient mounts, macOS TCC revocation) resets the
  streak and logs watcher.root_stat_error with the errno; Windows
  (mingw/UCRT) maps not-found to ENOENT so the check holds there.
- Pruning requires BOTH >=3 consecutive missing polls AND a sustained-
  absence grace window since the streak's first miss (default 600s,
  override via CBM_WATCHER_PRUNE_GRACE_S), tracked with monotonic
  cbm_now_ms.
- Root reappearance resets streak + timestamp (watcher.root_restored).
- The pruned entry is released via the deferred-free list poll_once
  drains (one freeing model shared with cbm_watcher_unwatch).

Limitation: only currently-watched projects are pruned; stale DBs left
by older sessions are out of scope.

Co-authored-by: pcristin <xxxokzxxx@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData enabled auto-merge July 3, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Automatic deletion of project when underlying folder is deleted

1 participant