authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 02:18:34-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 23:18:34-07:00
log2288d416aa3bcb5f2eeb4004228872593296971e
tree6895c05065c9deb643b8c8494fa76bd22fa781fa
parentddfe45c66eccd93c0adf61db92a16cbbbcde23e1
signature Signed by PGP key B5690EEEBB952194

fix(web): stop the "requests are slow" warning from firing on every provider update (#5570)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

4 files changed, 62 insertions(+), 9 deletions(-)

apps/web/src/components/SlowRpcRequestToastCoordinator.tsx+4-1
...@@ -5,7 +5,10 @@ import { toastManager } from "./ui/toast";...@@ -5,7 +5,10 @@ import { toastManager } from "./ui/toast";
55
6function describeSlowRequests(requests: ReadonlyArray<SlowRpcAckRequest>): string {6function describeSlowRequests(requests: ReadonlyArray<SlowRpcAckRequest>): string {
7 const count = requests.length;7 const count = requests.length;
8 const thresholdSeconds = Math.round((requests[0]?.thresholdMs ?? 0) / 1000);8 // Thresholds vary per method, so report the smallest one the batch has passed.
9 const thresholdSeconds = Math.round(
10 Math.min(...requests.map((request) => request.thresholdMs)) / 1000,
11 );
912
10 return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`;13 return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`;
11}14}
apps/web/src/connection/platform.ts+1-1
...@@ -590,7 +590,7 @@ const rpcRequestObserverLayer = Layer.succeed(...@@ -590,7 +590,7 @@ const rpcRequestObserverLayer = Layer.succeed(
590 Effect.sync(() => {590 Effect.sync(() => {
591 nextObservedRpcRequestId += 1;591 nextObservedRpcRequestId += 1;
592 const requestId = `${environmentId}:${nextObservedRpcRequestId}`;592 const requestId = `${environmentId}:${nextObservedRpcRequestId}`;
593 trackRpcRequestSent(requestId, `${method} · ${environmentId}`);593 trackRpcRequestSent(requestId, method, `${method} · ${environmentId}`);
594 return Effect.sync(() => {594 return Effect.sync(() => {
595 acknowledgeRpcRequest(requestId);595 acknowledgeRpcRequest(requestId);
596 });596 });
apps/web/src/rpc/requestLatencyState.test.ts+27
...@@ -6,6 +6,7 @@ import {...@@ -6,6 +6,7 @@ import {
6 getSlowRpcAckRequests,6 getSlowRpcAckRequests,
7 resetRequestLatencyStateForTests,7 resetRequestLatencyStateForTests,
8 trackRpcRequestSent,8 trackRpcRequestSent,
9 LONG_RUNNING_RPC_ACK_THRESHOLD_MS,
9 SLOW_RPC_ACK_THRESHOLD_MS,10 SLOW_RPC_ACK_THRESHOLD_MS,
10 MAX_TRACKED_RPC_ACK_REQUESTS,11 MAX_TRACKED_RPC_ACK_REQUESTS,
11} from "./requestLatencyState";12} from "./requestLatencyState";
...@@ -58,6 +59,32 @@ describe("requestLatencyState", () => {...@@ -58,6 +59,32 @@ describe("requestLatencyState", () => {
58 expect(getSlowRpcAckRequests()).toEqual([]);59 expect(getSlowRpcAckRequests()).toEqual([]);
59 });60 });
6061
62 it("keeps ignoring untracked methods when a display tag is supplied", () => {
63 trackRpcRequestSent(
64 "1",
65 WS_METHODS.previewAutomationConnect,
66 `${WS_METHODS.previewAutomationConnect} · env-1`,
67 );
68 vi.advanceTimersByTime(SLOW_RPC_ACK_THRESHOLD_MS * 2);
69
70 expect(getSlowRpcAckRequests()).toEqual([]);
71 });
72
73 it("gives provider updates a longer threshold before warning", () => {
74 trackRpcRequestSent("1", WS_METHODS.serverUpdateProvider, "server.updateProvider · env-1");
75 vi.advanceTimersByTime(LONG_RUNNING_RPC_ACK_THRESHOLD_MS - 1);
76 expect(getSlowRpcAckRequests()).toEqual([]);
77
78 vi.advanceTimersByTime(1);
79 expect(getSlowRpcAckRequests()).toMatchObject([
80 {
81 requestId: "1",
82 tag: "server.updateProvider · env-1",
83 thresholdMs: LONG_RUNNING_RPC_ACK_THRESHOLD_MS,
84 },
85 ]);
86 });
87
61 it("evicts the oldest pending requests once the tracker reaches capacity", () => {88 it("evicts the oldest pending requests once the tracker reaches capacity", () => {
62 for (let index = 0; index < MAX_TRACKED_RPC_ACK_REQUESTS + 1; index += 1) {89 for (let index = 0; index < MAX_TRACKED_RPC_ACK_REQUESTS + 1; index += 1) {
63 trackRpcRequestSent(String(index), "server.getConfig");90 trackRpcRequestSent(String(index), "server.getConfig");
apps/web/src/rpc/requestLatencyState.ts+30-7
...@@ -5,6 +5,12 @@ import { Atom } from "effect/unstable/reactivity";...@@ -5,6 +5,12 @@ import { Atom } from "effect/unstable/reactivity";
5import { appAtomRegistry } from "./atomRegistry";5import { appAtomRegistry } from "./atomRegistry";
66
7export const SLOW_RPC_ACK_THRESHOLD_MS = 15_000;7export const SLOW_RPC_ACK_THRESHOLD_MS = 15_000;
8/**
9 * Some requests are slow by design — they shell out to a package manager on the
10 * server and only respond once the install finishes. Warning about those after
11 * 15s is noise, so they get a much longer leash.
12 */
13export const LONG_RUNNING_RPC_ACK_THRESHOLD_MS = 120_000;
8export const MAX_TRACKED_RPC_ACK_REQUESTS = 256;14export const MAX_TRACKED_RPC_ACK_REQUESTS = 256;
9let slowRpcAckThresholdMs = SLOW_RPC_ACK_THRESHOLD_MS;15let slowRpcAckThresholdMs = SLOW_RPC_ACK_THRESHOLD_MS;
1016
...@@ -22,7 +28,12 @@ interface PendingRpcAckRequest {...@@ -22,7 +28,12 @@ interface PendingRpcAckRequest {
22}28}
2329
24const pendingRpcAckRequests = new Map<string, PendingRpcAckRequest>();30const pendingRpcAckRequests = new Map<string, PendingRpcAckRequest>();
25const untrackedRpcAckTags = new Set<string>([WS_METHODS.previewAutomationConnect]);31const untrackedRpcAckMethods = new Set<string>([WS_METHODS.previewAutomationConnect]);
32const longRunningRpcAckMethods = new Set<string>([
33 WS_METHODS.serverUpdateProvider,
34 WS_METHODS.serverRefreshProviders,
35 WS_METHODS.serverUpdateServer,
36]);
2637
27const slowRpcAckRequestsAtom = Atom.make<ReadonlyArray<SlowRpcAckRequest>>([]).pipe(38const slowRpcAckRequestsAtom = Atom.make<ReadonlyArray<SlowRpcAckRequest>>([]).pipe(
28 Atom.keepAlive,39 Atom.keepAlive,
...@@ -37,16 +48,27 @@ function getSlowRpcAckRequestsValue(): ReadonlyArray<SlowRpcAckRequest> {...@@ -37,16 +48,27 @@ function getSlowRpcAckRequestsValue(): ReadonlyArray<SlowRpcAckRequest> {
37 return appAtomRegistry.get(slowRpcAckRequestsAtom);48 return appAtomRegistry.get(slowRpcAckRequestsAtom);
38}49}
3950
40function shouldTrackRpcAck(tag: string): boolean {51function shouldTrackRpcAck(method: string): boolean {
41 return !tag.includes("subscribe") && !untrackedRpcAckTags.has(tag);52 return !method.includes("subscribe") && !untrackedRpcAckMethods.has(method);
53}
54
55function rpcAckThresholdMs(method: string): number {
56 return longRunningRpcAckMethods.has(method)
57 ? Math.max(slowRpcAckThresholdMs, LONG_RUNNING_RPC_ACK_THRESHOLD_MS)
58 : slowRpcAckThresholdMs;
42}59}
4360
44export function getSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> {61export function getSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> {
45 return getSlowRpcAckRequestsValue();62 return getSlowRpcAckRequestsValue();
46}63}
4764
48export function trackRpcRequestSent(requestId: string, tag: string): void {65/**
49 if (!shouldTrackRpcAck(tag)) {66 * Starts the slow-request timer for one in-flight unary RPC. `method` is the
67 * bare WS method (used to decide whether and how long to wait); `tag` is the
68 * human-readable label shown in the toast, which defaults to the method.
69 */
70export function trackRpcRequestSent(requestId: string, method: string, tag = method): void {
71 if (!shouldTrackRpcAck(method)) {
50 return;72 return;
51 }73 }
5274
...@@ -54,17 +76,18 @@ export function trackRpcRequestSent(requestId: string, tag: string): void {...@@ -54,17 +76,18 @@ export function trackRpcRequestSent(requestId: string, tag: string): void {
54 evictOldestPendingRpcRequestIfNeeded();76 evictOldestPendingRpcRequestIfNeeded();
5577
56 const startedAtMs = Date.now();78 const startedAtMs = Date.now();
79 const thresholdMs = rpcAckThresholdMs(method);
57 const request: SlowRpcAckRequest = {80 const request: SlowRpcAckRequest = {
58 requestId,81 requestId,
59 startedAt: new Date(startedAtMs).toISOString(),82 startedAt: new Date(startedAtMs).toISOString(),
60 startedAtMs,83 startedAtMs,
61 tag,84 tag,
62 thresholdMs: slowRpcAckThresholdMs,85 thresholdMs,
63 };86 };
64 const timeoutId = setTimeout(() => {87 const timeoutId = setTimeout(() => {
65 pendingRpcAckRequests.delete(requestId);88 pendingRpcAckRequests.delete(requestId);
66 appendSlowRpcAckRequest(request);89 appendSlowRpcAckRequest(request);
67 }, slowRpcAckThresholdMs);90 }, thresholdMs);
6891
69 pendingRpcAckRequests.set(requestId, {92 pendingRpcAckRequests.set(requestId, {
70 request,93 request,