aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py22
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py18
2 files changed, 36 insertions, 4 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index c9c362a..932a90d 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -764,9 +764,27 @@ class NodeDaemon:
Serialised by _reload_lock: fire-and-forget reloads from config-mutating
endpoints can overlap with the wizard's explicit /api/reload call,
and two concurrent hot-loads of the same group corrupt the runtime state.
+
+ The reload belongs to the node, never to whoever asked for it. A root
+ added from a browser reaches here through the operator's WebRTC session,
+ whose tasks are all cancelled when that session closes — and on
+ 2026-09-14 one closed 47 s into the scan of a 900 GB root. The reload
+ died without a line in the log, the new root was in node.toml and in
+ the indexer but never in the group's context, and nothing ever tried
+ again: the lock was free, and nobody was waiting on it. So the work runs
+ in a task of its own, and a caller that goes away only stops waiting.
"""
- async with self._reload_lock:
- await self._reload_config_inner()
+ await asyncio.shield(spawn(self._reload_config_locked(), what="config reload"))
+
+ async def _reload_config_locked(self) -> None:
+ try:
+ async with self._reload_lock:
+ await self._reload_config_inner()
+ except asyncio.CancelledError:
+ log.warning("Config reload cancelled before it finished — the node may "
+ "be serving part of the previous configuration until the "
+ "next reload")
+ raise
async def _reload_config_inner(self) -> None:
log.info("Reloading config from %s", self._config_path)
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index 2b68550..c20902e 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -953,8 +953,22 @@ class DirectoryIndexer:
return
root.ejected = False
root.available = root.is_live()
- if root.available:
- log.info("Root %r plugged — rescanning", root.name)
+ if not root.available:
+ await self._finish_plug(None)
+ return
+ log.info("Root %r plugged — rescanning", root.name)
+ # `_rescan_root` drops the root's entries before it walks the disk, and
+ # this is called from an admin op running in the operator's WebRTC
+ # session — which cancels everything it started when it closes. So the
+ # rescan runs in a task this indexer owns, and a caller that goes away
+ # only stops waiting for it instead of leaving the root emptied.
+ task = asyncio.create_task(self._finish_plug(root))
+ self._scan_tasks.add(task)
+ task.add_done_callback(self._scan_tasks.discard)
+ await asyncio.shield(task)
+
+ async def _finish_plug(self, root: Root | None) -> None:
+ if root is not None:
await self._rescan_root(root)
self._restart_observer()
self._index.roots = self.roots.describe()