You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RelayRoom keeps a revoked Burrow forwarding when its sweep's row read fails #910
Code:RelayRoom.#sweep in hosted/server/relay-room.ts; the alarm in RelayRoom.alarm re-arms RELAY_ROOM_SWEEP_MS (one hour, hosted/server/relay-room-contract.ts) out whatever the sweep did.
Failure path: a Burrow is removed or de-entitled by any path that does not reach closeBurrow (an entitlement change, or a removal whose closeBurrow RPC failed, which the DELETE route logs and answers 204 anyway). The next alarm calls #sweep. #rows throws (database error, or RELAY_ROW_READ_TIMEOUT_MS passes), the catch logs "could not sweep" and returns, and alarm() arms the next sweep an hour later. The socket keeps forwarding authenticated frames past the sweep it should have died at. If the database stays unreachable, it keeps forwarding through every later sweep too.
This breaks the docs/specs/security-hosted.md -> "Relay boundary" FAIL IF: "a removed or de-entitled Burrow's [socket] outlives the next sweep, at most RELAY_ROOM_SWEEP_MS while any Burrow socket is held". hosted/server/tests/relay-room.test.ts ("a stalled row read … leaves the sweep for later, the object's sockets open") pins the current behavior. Keeping sockets open on a failed read was a deliberate choice: #rows says a stalled read must not reset "the object and every socket it holds".
Suggested fix: decide the policy first, then change the code, test, and FAIL IF together. Options:
Fail closed. On a failed sweep read, retire every held Burrow with a retryable close code. Each one reconnects through the upgrade, which rechecks its row under blockConcurrencyWhile and is refused while the database is down. This meets the FAIL IF exactly, at the cost of dropping every session during a database blip.
Retry soon. On failure, arm the alarm at now + RELAY_ROW_READ_TIMEOUT_MS (or about a minute), not an hour, and fail closed only after N consecutive failures or once RELAY_ROOM_SWEEP_MS has passed since the last good sweep. Then reword the FAIL IF to "within RELAY_ROOM_SWEEP_MS of the last successful sweep".
Option 2 keeps the bound at one sweep interval, or a small multiple of it, without resetting sockets on a single slow query.
Found while trimming #904, which had parked this as a spec "Known gap".
Confirmed at d33cee4. I haven't opened a fix because, as the issue says, the policy has to be chosen first. A reproduction test would also just pin one answer before anyone has picked it.
What the code does today:
#sweep returns from its catch without closing anything.
alarm() then pushes now + RELAY_ROOM_SWEEP_MS whether or not the sweep succeeded. So each failed read gives a revoked Burrow another full hour.
The step at relay-room.test.ts:555-560 pins this: it fires the alarm under stallRows(true) and expects the held Burrow to keep forwarding.
Points that bear on the choice:
Option 2 fits the alarm code as written.alarm() already arms Math.min(...next), so a failed sweep can push a near deadline (for example now + RELAY_ROW_READ_TIMEOUT_MS) in place of the hourly one. The extra state is a stored "last good sweep" time. The fail-closed fallback would check that time against RELAY_ROOM_SWEEP_MS, which keeps a bound measured from the last good sweep.
Option 1 leads to a reconnect loop for as long as the database is down. Every retired Burrow comes back through the upgrade. That upgrade's #rows read fails as 503, so each Burrow keeps retrying. Whatever backoff the Burrow uses on a retryable close then decides how much load the outage creates.
Activity
dormouse-bot commentedon Oct 2, 2026
Confirmed at d33cee4. I haven't opened a fix because, as the issue says, the policy has to be chosen first. A reproduction test would also just pin one answer before anyone has picked it.
What the code does today:
#sweepreturns from itscatchwithout closing anything.alarm()then pushesnow + RELAY_ROOM_SWEEP_MSwhether or not the sweep succeeded. So each failed read gives a revoked Burrow another full hour.relay-room.test.ts:555-560pins this: it fires the alarm understallRows(true)and expects the held Burrow to keep forwarding.Points that bear on the choice:
alarm()already armsMath.min(...next), so a failed sweep can push a near deadline (for examplenow + RELAY_ROW_READ_TIMEOUT_MS) in place of the hourly one. The extra state is a stored "last good sweep" time. The fail-closed fallback would check that time againstRELAY_ROOM_SWEEP_MS, which keeps a bound measured from the last good sweep.#rowsread fails as 503, so each Burrow keeps retrying. Whatever backoff the Burrow uses on a retryable close then decides how much load the outage creates.docs/specs/security-hosted.md, and the sweep paragraph inhosted.md. Landing the policy change after Align Hosted and one-time reference contracts #904, or folding it into Align Hosted and one-time reference contracts #904, avoids a conflict on that line.If you pick an option, the change fits in
relay-room.tsand the stalled-read test inrelay-room.test.ts, plus the FAIL IF wording.