nobilis answers one RPC at a time per client, so a slow call freezes the window #87

Closed
opened 2026-09-02 23:17:04 -04:00 by Salastil · 1 comment
Owner

rpc/mod.rs::handle_connection awaits handle_line inside its select loop, so while one request is being served that client gets no responses and no events — the window shows "Connecting…" and stops updating.

Found while adding the Sneedchat room list, which takes ~15s (Tor + the site's proof-of-work gate). That one is worked around by returning a cached answer and doing the real read in a background task, but the general problem stands: any slow handler does this. Matrix' searchMatrixRooms across several homeservers, fetchThread, backfill, and media fetches are all candidates.

Done looks like: requests are handled concurrently — spawn per request, write responses through a channel, and share subscriptions — so one slow call can no longer stop the socket. Request ids already exist, so out-of-order responses are fine.

`rpc/mod.rs::handle_connection` awaits `handle_line` inside its select loop, so while one request is being served that client gets no responses and **no events** — the window shows "Connecting…" and stops updating. Found while adding the Sneedchat room list, which takes ~15s (Tor + the site's proof-of-work gate). That one is worked around by returning a cached answer and doing the real read in a background task, but the general problem stands: any slow handler does this. Matrix' `searchMatrixRooms` across several homeservers, `fetchThread`, `backfill`, and media fetches are all candidates. **Done looks like:** requests are handled concurrently — spawn per request, write responses through a channel, and share `subscriptions` — so one slow call can no longer stop the socket. Request ids already exist, so out-of-order responses are fine.
Author
Owner

Finished. Requests are handled concurrently: each becomes a task, and answers come back through a channel the connection loop owns, so the loop is never blocked on anything but its own writes. Responses can now arrive out of order, which was always allowed — every response carries the id of the request it answers, and the client already matches on it.

Three things the change needed:

  • Subscriptions became shared (Arc<Mutex<HashSet>>), since subscribe is written from inside a task while the event arm reads it. A std mutex, held for a set insert and never across an await.
  • 32 requests in flight per client, so a client asking for ten thousand things queues instead of making the daemon hold ten thousand futures. Well above what a window does — a busy startup is a few dozen.
  • A panicking handler is caught. It used to take the connection down with it; now it would instead leave one request unanswered forever, which is a promise nothing settles. It answers with an error instead.

Also shutdown now waits 100ms before notifying, because its own "ok" is still on its way to the loop — a daemon that exits first answers the request to stop by dropping the socket, which a client cannot tell apart from a crash.

Measured live with a 4.6s Matrix directory search in flight (aimed partly at an unroutable server to make it slow): listBuffers and listAccounts both answered at 255ms, and a presenceChange event arrived at 282ms — during the slow call. All three would have waited the full 4.6s before.

One gap worth stating: there is no automated regression test for this. Exercising it needs a real socket and a fully built AppState, and I judged the harness heavier than the change; the verification above is manual.

nobilis cdfbfae

Finished. Requests are handled concurrently: each becomes a task, and answers come back through a channel the connection loop owns, so the loop is never blocked on anything but its own writes. Responses can now arrive out of order, which was always allowed — every response carries the id of the request it answers, and the client already matches on it. Three things the change needed: - **Subscriptions became shared** (`Arc<Mutex<HashSet>>`), since `subscribe` is written from inside a task while the event arm reads it. A std mutex, held for a set insert and never across an await. - **32 requests in flight per client**, so a client asking for ten thousand things queues instead of making the daemon hold ten thousand futures. Well above what a window does — a busy startup is a few dozen. - **A panicking handler is caught.** It used to take the connection down with it; now it would instead leave one request unanswered forever, which is a promise nothing settles. It answers with an error instead. Also `shutdown` now waits 100ms before notifying, because its own "ok" is still on its way to the loop — a daemon that exits first answers the request to stop by dropping the socket, which a client cannot tell apart from a crash. **Measured live** with a 4.6s Matrix directory search in flight (aimed partly at an unroutable server to make it slow): `listBuffers` and `listAccounts` both answered at **255ms**, and a `presenceChange` event arrived at **282ms** — during the slow call. All three would have waited the full 4.6s before. One gap worth stating: there is no automated regression test for this. Exercising it needs a real socket and a fully built AppState, and I judged the harness heavier than the change; the verification above is manual. nobilis cdfbfae
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#87