aboutsummaryrefslogtreecommitdiffstats
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, 59 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index c9d862a..154813d 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -22,6 +22,7 @@ in the adapter.
from __future__ import annotations
+import asyncio
import logging
import re
from dataclasses import asdict
@@ -730,12 +731,69 @@ async def set_enabled_apps(state: dict, group_id: str, apps: list[str]) -> dict:
return {"apps": apps, "group_id": group_id}
+# ── Scan settings ────────────────────────────────────────────────────────────
+
+async def set_scan_settings(state: dict, group_id: str, reconcile_interval_secs: float,
+ debounce_secs: float) -> dict:
+ """
+ How often the indexer's reconciliation backstop runs, and how long a
+ changed file is left alone before being hashed (indexer.py
+ DirectoryIndexer). Persisted like set_member_upload/set_enabled_apps —
+ but there is also a *live* DirectoryIndexer object to update, since it
+ reads these once at construction and runs its own background loop with
+ them rather than consulting groups_ctx on every use.
+ """
+ roster = _roster(state)
+ await roster.set_scan_settings(group_id, reconcile_interval_secs, debounce_secs,
+ set_by=state.get("node_user_id", ""))
+ indexer = state.get("indexers", {}).get(group_id)
+ if indexer:
+ indexer.reconcile_secs = reconcile_interval_secs
+ indexer.debounce_secs = debounce_secs
+ # Apply the new interval now rather than after whatever backoff had
+ # already stretched the wait to.
+ indexer.note_activity()
+ # Optional, unlike _group_ctx(): a group can be persisted here before it
+ # is hot-loaded (or in a test that only cares about the roster/indexer
+ # side), and that must not turn a successful write into a 404.
+ ctx = state.get("groups_ctx", {}).get(group_id)
+ if ctx is not None:
+ ctx["reconcile_interval_secs"] = reconcile_interval_secs
+ ctx["debounce_secs"] = debounce_secs
+ log.info("Scan settings for group %s: reconcile=%.0fs debounce=%.0fs",
+ group_id[:8], reconcile_interval_secs, debounce_secs)
+ return {"reconcile_interval_secs": reconcile_interval_secs,
+ "debounce_secs": debounce_secs, "group_id": group_id}
+
+
# ── Reload ──────────────────────────────────────────────────────────────────
async def reload_config(state: dict) -> dict:
- """Hot-reload node.toml without dropping connections."""
+ """Hot-reload node.toml without dropping connections. Blocks until the
+ reload actually finishes — see start_reload for why the loopback route
+ uses that instead."""
reload_fn = state.get("reload_fn")
if not reload_fn:
raise OpError("Reload not available", status=503)
await reload_fn()
return {"status": "reloaded"}
+
+
+async def start_reload(state: dict) -> dict:
+ """
+ Same as reload_config, but does not wait for the reload to finish.
+
+ The loopback route uses this one: the Electron bridge caps every call at
+ a fixed 30s (main.js node:call), and hot-loading a brand-new group runs
+ its full initial scan synchronously inside _reload_config_inner()
+ (daemon.py) before that coroutine returns — minutes, not seconds, on a
+ real library (found against a 45 GB group on the same slow disk the
+ StarWars benchmark used). The reload keeps running on the daemon's own
+ event loop either way; add_root/remove_root below already fire it the
+ same way for exactly this reason.
+ """
+ reload_fn = state.get("reload_fn")
+ if not reload_fn:
+ raise OpError("Reload not available", status=503)
+ asyncio.ensure_future(reload_fn())
+ return {"status": "reloading"}