tower/dashboard: offline gh failures aren't negative-cached (10s hang per request) and per-tab SSE exhausts Chrome's 6-connection pool — dashboard becomes unloadable
Observed (on a plane with GitHub blackholed by the network)
Two compounding failures, measured live:
- No negative caching of forge fetch failures.
overview.ts fetchPRsCached/fetchIssuesCached etc. only cache on success (if (data !== null)). When gh can't reach GitHub, every fetch hangs until the TLS handshake times out (~10s measured) — and because the failure is never cached, every /api/overview request repays the full hang. Measured twice back-to-back: 10.2s, then 10.1s. The 30s success-TTL never engages.
- Chrome connection-pool exhaustion. Each dashboard tab holds one persistent SSE connection (
apps/web/src/hooks/useSSE.ts — the singleton-per-tab design is documented in-file, including the HTTP/1.1 hazard). Chrome caps HTTP/1.1 at 6 connections per host:port across ALL tabs. With several workspace tabs open, SSE streams plus the 10s-hanging overview fetches occupy the entire pool; any new page load (including hard refreshes) queues indefinitely. Observed: 20 Chrome connections to :4100; pages unloadable in Chrome while a fresh Safari (separate pool) loaded the same URL instantly. Hard refresh cannot help — the refresh request itself queues.
Impact
On any network where GitHub is blocked or degraded (in-flight WiFi, captive portals, firewalled offices, GitHub incidents), the dashboard degrades from "banners say PRs unavailable" (intended, graceful) to "workspace pages will not load at all in the user's main browser" (not graceful). The user-visible symptom — "Tower isn't responding" — misdirects diagnosis toward the server, which is healthy.
Proposed fixes (independent, all cheap)
- Negative-cache forge failures with a short TTL (15–30s): cache
null results too, so one hang per TTL window instead of one per request.
- Timeout the gh subprocess (e.g. 3–5s) so even the uncached failure path fails fast; the TLS-blackhole case currently rides gh's own ~10s handshake timeout.
- Optional, bigger: reduce per-tab connection pressure — e.g. serve Tower over HTTP/2, or share the SSE stream via BroadcastChannel/SharedWorker across same-origin tabs. The in-file comment in useSSE.ts shows the hazard was known; the offline-hang multiplier is what pushes it over the edge.
Area: area/tower (fixes 1–2 are Tower-side; fix 3 touches the web dashboard — split out if taken up).
tower/dashboard: offline gh failures aren't negative-cached (10s hang per request) and per-tab SSE exhausts Chrome's 6-connection pool — dashboard becomes unloadable
Observed (on a plane with GitHub blackholed by the network)
Two compounding failures, measured live:
overview.tsfetchPRsCached/fetchIssuesCachedetc. only cache on success (if (data !== null)). Whenghcan't reach GitHub, every fetch hangs until the TLS handshake times out (~10s measured) — and because the failure is never cached, every/api/overviewrequest repays the full hang. Measured twice back-to-back: 10.2s, then 10.1s. The 30s success-TTL never engages.apps/web/src/hooks/useSSE.ts— the singleton-per-tab design is documented in-file, including the HTTP/1.1 hazard). Chrome caps HTTP/1.1 at 6 connections per host:port across ALL tabs. With several workspace tabs open, SSE streams plus the 10s-hanging overview fetches occupy the entire pool; any new page load (including hard refreshes) queues indefinitely. Observed: 20 Chrome connections to :4100; pages unloadable in Chrome while a fresh Safari (separate pool) loaded the same URL instantly. Hard refresh cannot help — the refresh request itself queues.Impact
On any network where GitHub is blocked or degraded (in-flight WiFi, captive portals, firewalled offices, GitHub incidents), the dashboard degrades from "banners say PRs unavailable" (intended, graceful) to "workspace pages will not load at all in the user's main browser" (not graceful). The user-visible symptom — "Tower isn't responding" — misdirects diagnosis toward the server, which is healthy.
Proposed fixes (independent, all cheap)
nullresults too, so one hang per TTL window instead of one per request.Area:
area/tower(fixes 1–2 are Tower-side; fix 3 touches the web dashboard — split out if taken up).