Skip to content

fix: bounded notification writes — a non-reading MCP client must never wedge the embedder - #22

Merged
dfattal merged 2 commits into
mainfrom
fix/bounded-notification-writes
Aug 14, 2026
Merged

dfattal merged 2 commits into
mainfrom
fix/bounded-notification-writes

Conversation

@dfattal

@dfattal dfattal commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes the framework side of DisplayXR/displayxr-runtime#928 (launch-blocker: with any displayxr-voice client connected, every DisplayXR app launch wedged at xrSetMCPAppInfoDXR inside a blocking notification write; captured live with full symbols during the 2026-08-14 close gauntlet).

  • New mcp_conn_write_bounded (both transports) with frame-scoped deadline + abort-on-timeout (partial frames corrupt LSP framing — the stream must die, and its serve thread unwinds the slot).
  • broadcast_notification: 250 ms budget per client + trylock on write_mutex (drops the notification rather than queueing behind a serve thread stuck on the same dead peer).

Validation: MSVC lib+adapter build clean locally; POSIX smoke tests ride CI (they don't compile under MSVC — pre-existing strings.h include). End-to-end validation against the live wedge repro (runtime FetchContent pointed at this branch, voice client alive, app launch) is being run on the SR box and will be reported on runtime#928.

🤖 Generated with Claude Code

dfattal and others added 2 commits August 14, 2026 02:45
…r wedge the embedder

displayxr-runtime#928: broadcast_notification did a BLOCKING write per
connected client, on whatever thread the embedder called it from. The
DisplayXR runtime broadcasts from app main threads inside OpenXR calls
(xrSetMCPAppInfoDXR registers tools -> notify_tools_changed), so one
connected client that stopped reading (a displayxr-voice instance, live
or orphaned) filled the pipe buffer and froze every subsequently
launched app before its render loop started.

- mcp_conn_write_bounded(conn, buf, len, timeout_ms): frame-scoped
  deadline on both arms (Windows: overlapped wait + CancelIoEx on
  timeout; POSIX: poll(POLLOUT) with remaining-deadline before each
  send). A timed-out write ABORTS the connection — a partial frame
  corrupts Content-Length framing, so the stream must not be reused;
  the serve thread notices and unwinds the slot.
- broadcast_notification: 250 ms budget per client per write, and
  TRYlock on write_mutex — if the client's own serve thread is stuck
  writing a response to the same dead peer, the notification is
  dropped instead of queueing behind it (notifications are
  fire-and-forget by contract).

A slow consumer now costs the embedder at most ~500 ms once, then
loses its connection. Request/response writes on serve threads are
unchanged (self-contained; server stop already aborts them).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e OVERLAPPED on sync handles

Live validation of the bounded write caught it still blocking inside
ZwWriteFile: CreateNamedPipeA never passed FILE_FLAG_OVERLAPPED, so
every ReadFile/WriteFile on server-side conns ran synchronously and
ignored the OVERLAPPED structs the I/O paths were already passing —
the IO_PENDING branches (and the bounded write's timeout+CancelIoEx)
were dead code. The accept path moves to an overlapped
ConnectNamedPipe (NULL overlapped on an overlapped handle can
misreport completion); semantics unchanged — blocking accept, still
woken by mcp_listener_close's CancelIoEx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dfattal

dfattal commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Live validation on the SR box (runtime FetchContent pointed at f0f1e13, DisplayXRClient.dll deployed, displayxr-voice ALIVE — the deterministic wedge precondition): app launch sailed through xrSetMCPAppInfoDXR, session began, render loop running, main thread sampled clean (Win32 message pump). Under v0.5.0 this froze inside ZwWriteFile 100% of the time.

Note the second commit is load-bearing: the first (bounded write alone) still wedged live because the server pipes were synchronous handles — WriteFile ignored the OVERLAPPED entirely. FILE_FLAG_OVERLAPPED on CreateNamedPipe is what makes both the existing I/O paths and the new timeout real.

@dfattal
dfattal merged commit cc79d50 into main Aug 14, 2026
5 checks passed
@dfattal
dfattal deleted the fix/bounded-notification-writes branch August 14, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant