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
+29
View File
@@ -2,6 +2,35 @@
All notable changes to this project will be documented in this file.
## [0.9.0] - 2026-06-16
### Changed (resilience against a slow/stalled NAS)
Hardened `DsmClient` so a single slow or stalled DSM response can no longer
hang a tool — including read-only ones like `read_compose`. Previously every
tool shared one HTTP client with generous timeouts (FileStation download/upload
used 60 s) and no retry, so when DSM briefly stalled (FileStation busy, NAS
under load) a call blocked for up to a minute and follow-up calls stalled with
it, making the whole MCP appear dead.
- **Hard per-call ceiling** — every DSM round-trip is wrapped in
`asyncio.wait_for(…, HARD_CALL_TIMEOUT=25 s)`, a backstop that guarantees a
call returns even if DSM accepts a request but never responds.
- **Tighter timeouts** — default read timeout 30 s → 15 s; FileStation
download/upload 60 s → 20 s; `build_stream` per-read 60 s → 20 s.
- **Single retry on a fresh connection** for transient transport failures: any
connection-establishment error is retried (the request provably never reached
DSM, so it is safe even for non-idempotent POSTs), and read/protocol errors
are additionally retried for idempotent GETs. This also absorbs the keepalive
race where DSM closes an idle connection just as a request is sent.
- **Clean error instead of a raw exception** — when both attempts fail, tools
now return a `SynologyError` "NAS … did not respond, it may be busy — please
retry shortly" instead of a raw transport traceback or a long silent hang.
Note: each tool may still issue several DSM calls, so a sustained NAS outage is
bounded per-call (~1525 s) rather than per-tool; a per-tool ceiling is a
possible future addition.
## [0.8.0] - 2026-06-16
### Changed (fixes intermittent "MCP not responding" timeouts)