Harden DsmClient against a slow/stalled NAS so no tool can hang

A second, independent failure mode behind the intermittent "MCP not
responding" reports: every tool shares one HTTP client with generous
timeouts (FileStation download/upload used 60s) and no retry. When DSM
briefly stalled (FileStation busy, NAS under load) a single call blocked
for up to a minute, and follow-up calls - even read-only ones like
read_compose - stalled with it, so the whole MCP appeared dead. The path
to the NAS is direct LAN (dsm.gecheckt.de -> 192.168.0.2), so this is DSM
responsiveness, not a network/NAT-loopback issue.

Route every DSM round-trip through a new DsmClient._send that adds:

- a hard wall-clock ceiling (asyncio.wait_for, HARD_CALL_TIMEOUT=25s) so a
  call returns even if DSM accepts a request but never responds;
- a single retry on a fresh connection for transient transport failures -
  always for connection-establishment errors (request provably never
  reached DSM, safe even for POST), and for GETs also on read/protocol
  errors (also absorbs the keepalive race where DSM drops an idle conn);
- a clean SynologyError ("NAS did not respond, it may be busy") instead of
  a raw transport traceback or a long silent hang.

Also tighten timeouts: default read 30s->15s, FileStation 60s->20s,
build_stream per-read 60s->20s. build_stream stays out of _send (it
streams and manages its own budget).

- add 6 tests for retry/no-retry/ceiling behaviour (320 pass)
- update CLAUDE.md and CHANGELOG; bump 0.8.0 -> 0.9.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 07:58:34 +02:00
parent 4a49407883
commit 46b36f6b08
6 changed files with 270 additions and 15 deletions
+10
View File
@@ -130,6 +130,16 @@ Only a second consecutive failure is treated as a real auth problem.
`update_compose`, `delete_container`, `stop_container`,
`restart_container`, `pull_image`
- After compose changes: suggest `redeploy_project`
- **Transport resilience (DsmClient):** every DSM round-trip goes through
`DsmClient._send`, which enforces a hard wall-clock ceiling
(`HARD_CALL_TIMEOUT`, 25 s) via `asyncio.wait_for` and retries once on a
fresh connection for transient transport failures (always for
connection-establishment errors; for GETs also on read/protocol errors).
This is what stops a slow/stalled NAS from hanging a tool — including
read-only ones — for minutes. Read timeouts are deliberately tight (15 s
general, 20 s FileStation/build_stream). Keep `build_stream` out of `_send`:
it streams and manages its own budget. Don't widen these timeouts without a
reason — they are the guardrail against the "MCP not responding" hangs.
- DSM errors → human-readable message, no stack traces
- No secrets in stderr output
- Type hints and docstrings everywhere