summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ops.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py60
1 files changed, 60 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index c9375b3..c880c75 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -922,6 +922,66 @@ async def set_scan_settings(state: dict, group_id: str, reconcile_interval_secs:
"debounce_secs": debounce_secs, "group_id": group_id}
+# ── Index cache maintenance ───────────────────────────────────────────────────
+#
+# The (path, size, mtime) -> hash accelerator (indexer/cache.py) is node-wide
+# and grows for as long as a path was ever seen — a folder an operator later
+# stops sharing (root removed, or every group hosting it is deleted) leaves
+# its rows behind forever otherwise. Nothing about correctness needs this:
+# a stale row just sits unused (lookup() keys on the live path string, so a
+# path nothing scans any more is never looked up). This is disk space
+# hygiene the operator can run when they want it, not a background job.
+
+async def index_cache_stats(state: dict) -> dict:
+ """Row count only — cheap, safe to call on every dashboard render.
+ The actual staleness check (prune_index_cache) is not this cheap and
+ must never run implicitly."""
+ cache = state.get("index_cache")
+ return {"count": await cache.count() if cache else 0}
+
+
+async def prune_index_cache(state: dict) -> dict:
+ """
+ Drop cache rows that cannot be right for anything any more: the path is
+ not under any group's root at all, or it is under a root that is
+ available right now and the file is genuinely gone from disk.
+
+ Deliberately leaves alone anything under a root that is currently
+ *unavailable* (a disconnected drive) — indexer.py's own rule is that
+ such a root freezes rather than empties, precisely so it does not pay a
+ full rehash the moment it comes back. Pruning through an unavailable
+ root here would reintroduce exactly that cost via a different door, so
+ an owning-but-unavailable root wins over "the file isn't there right
+ now" every time, unconditionally.
+
+ A row lost here costs one rehash the next time that path is scanned,
+ never a wrong answer: lookup() (cache.py) always re-validates size and
+ mtime against a live stat() before trusting a cached hash.
+ """
+ cache = state.get("index_cache")
+ if cache is None:
+ raise OpError("No index cache in this process", status=503)
+
+ indexers = list((state.get("indexers") or {}).values())
+ roots = [root for indexer in indexers for root in indexer.roots]
+
+ def _is_stale(path_str: str) -> bool:
+ path = Path(path_str)
+ owning = [r for r in roots if r.path in path.parents]
+ if not owning:
+ return True
+ if any(not r.available for r in owning):
+ return False
+ return not path.exists()
+
+ paths = await cache.all_paths()
+ stale = await asyncio.to_thread(lambda: [p for p in paths if _is_stale(p)])
+ await cache.remove_many(stale)
+ log.info("Index cache pruned: %d stale row(s) removed, %d kept",
+ len(stale), len(paths) - len(stale))
+ return {"status": "pruned", "removed": len(stale), "kept": len(paths) - len(stale)}
+
+
# ── Reload ──────────────────────────────────────────────────────────────────
async def reload_config(state: dict) -> dict: