diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-14 10:17:49 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-14 10:17:49 +0200 |
| commit | 80ac882346b474bb7ea4e8c8afd4694bd13dd9dd (patch) | |
| tree | 3aebf475a23dc3e44dff1a6c577d8af82418d024 /packages/meshbay-node/src/meshbay_node/ui | |
| parent | af542a40aec6ffe0ac06798d99abd00a24eb9c50 (diff) | |
| download | meshbay-80ac882346b474bb7ea4e8c8afd4694bd13dd9dd.tar.gz | |
feat(node): skip a file the disk stops answering for, then pause its root
A Windows tester's scan stuck for good on one directory of a large disk:
one read that never returned held the indexer's only thread, and with it
every root behind that one.
- A disk call is timed by its silence, not its length: each directory
entry listed and each megabyte read counts as an answer. After 60 s of
silence the call is abandoned on its daemon thread, the file is noted
unread and the scan goes on.
- Three silent files in a row pause the root (not persisted). Resume
skips the files already known to give no answer; a restart, a replug
or a change to the file retries them.
- The walk is per directory with os.scandir: a subdirectory that cannot
be listed costs itself, and a listing that stops answering pauses the
root at once. A scan no longer stats anything from the event loop.
- The log names the file after 10 s of silence, the indexing band shows
it, and Group settings shows the paused root, where it stuck and the
unread files with their reason, on the node's machine only.
- generate_http_api.py writes LF on Windows too.
Verified with real kernel-blocked opens (an unacknowledged oplock) in the
installed frozen app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 25 |
1 files changed, 24 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 9933750..f5cd5d0 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -23,6 +23,9 @@ from meshbay_node import __version__, ops log = logging.getLogger(__name__) +# Unread files sent to the operator's page at once; `unread_total` says the rest. +UNREAD_LISTED = 100 + def _op(coro): """ @@ -490,17 +493,28 @@ def create_ui_app(state: dict) -> FastAPI: is only added to groups_ctx once that scan finishes (it is not yet authorized for member connections either way — see _reload_config) — this is precisely the window the wizard needs to watch. + + Also what the scan could not do, with paths, which is why it is here + and nowhere a member can ask: `unread`, the files it met and did not + index, each with the reason (`no_answer` when the disk said nothing); + and `stalled`, the folders the node paused by itself because their + disk stopped answering, each with where. """ indexer = state.get("indexers", {}).get(group_id) progress = indexer.progress if indexer else None if progress is None: return {"scanning": False, "scanned_bytes": 0, "total_bytes": 0, - "current_dir": ""} + "current_dir": "", "unread": [], "unread_total": 0, "stalled": []} return { "scanning": progress.scanning, "scanned_bytes": progress.scanned_bytes, "total_bytes": progress.total_bytes, "current_dir": progress.current_dir, + "unread": [{"root": u.root, "path": u.path, "reason": u.reason} + for u in list(indexer.unread.values())[:UNREAD_LISTED]], + "unread_total": indexer.unread_count(), + "stalled": [{"root": name, "path": where} + for name, where in indexer.stalled.items()], } @app.get("/api/index-status") @@ -512,12 +526,17 @@ def create_ui_app(state: dict) -> FastAPI: is on screen whatever page the operator is on, so it cannot ask per group. Names roots, like `current_dir` above: loopback only, the operator's own screen. Reads state["indexers"] for the same reason. + + `slow_path` and `slow_secs` say what the disk has given no answer on, + and for how long, once that passes ten seconds: a scan that sits still + names the file it is sitting on. """ config = state.get("config") names = {g.id: g.name for g in config.groups} if config else {} groups = [] for gid, indexer in list(state.get("indexers", {}).items()): p = indexer.progress + slow = indexer.disk_wait() groups.append({ "group_id": gid, "group_name": names.get(gid, gid[:8]), @@ -530,6 +549,10 @@ def create_ui_app(state: dict) -> FastAPI: "files_done": p.files_done, "files_total": p.files_total, "queued": list(p.queued), + "slow_path": slow[0] if slow else "", + "slow_secs": int(slow[1]) if slow else 0, + "unread": indexer.unread_count(), + "stalled": list(indexer.stalled), }) return {"groups": groups} |