diff --git a/engine/packages/guard-core/src/utils.rs b/engine/packages/guard-core/src/utils.rs index d6afd21e87..97e4c95786 100644 --- a/engine/packages/guard-core/src/utils.rs +++ b/engine/packages/guard-core/src/utils.rs @@ -487,7 +487,6 @@ fn is_retryable_guard_http_error(code: &str) -> bool { | "tunnel_request_aborted" | "tunnel_message_timeout" | "tunnel_response_closed" - | "gateway_response_start_timeout" ) } diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/gateway-response-start-timeout.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/gateway-response-start-timeout.ts new file mode 100644 index 0000000000..b7689710ac --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/gateway-response-start-timeout.ts @@ -0,0 +1,23 @@ +import { actor } from "rivetkit"; + +export const responseStartTimeoutActor = actor({ + state: { runs: 0 }, + createVars: () => ({ waiting: [] as Array<() => void> }), + actions: { + // Only the first run waits for `release`, so a duplicate run reaches the + // caller as a result instead of a timeout. + waitForRelease: async (c) => { + c.state.runs += 1; + if (c.state.runs === 1) { + await new Promise((resolve) => + c.vars.waiting.push(resolve), + ); + } + return c.state.runs; + }, + release: (c) => { + for (const resolve of c.vars.waiting.splice(0)) resolve(); + }, + runs: (c) => c.state.runs, + }, +}); diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts index ac165dd402..58a3381df9 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts @@ -62,6 +62,7 @@ import { } from "./destroy"; import { customTimeoutActor, errorHandlingActor } from "./error-handling"; import { fileSystemHibernationCleanupActor } from "./file-system-hibernation-cleanup"; +import { responseStartTimeoutActor } from "./gateway-response-start-timeout"; import { hibernationActor, hibernationSleepWindowActor } from "./hibernation"; import { kvActor } from "./kv"; import { largePayloadActor, largePayloadConnActor } from "./large-payloads"; @@ -276,6 +277,8 @@ export const registry = setup({ manyQueueRunParentActor, // From action-inputs.ts inputActor, + // From gateway-response-start-timeout.ts + responseStartTimeoutActor, // From action-timeout.ts shortTimeoutActor, longTimeoutActor, diff --git a/rivetkit-typescript/packages/rivetkit/src/client/lifecycle-errors.ts b/rivetkit-typescript/packages/rivetkit/src/client/lifecycle-errors.ts index 4466aa3e01..0f726914ef 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/lifecycle-errors.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/lifecycle-errors.ts @@ -166,8 +166,7 @@ function isRetryableGuardGatewayHttpError(code: string): boolean { code === "actor_stopped_while_waiting" || code === "tunnel_request_aborted" || code === "tunnel_message_timeout" || - code === "tunnel_response_closed" || - code === "gateway_response_start_timeout" + code === "tunnel_response_closed" ); } diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-response-start-timeout.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-response-start-timeout.test.ts new file mode 100644 index 0000000000..f1d32f1590 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-response-start-timeout.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; +import { describeDriverMatrix } from "./shared-matrix"; +import { setupDriverTest } from "./shared-utils"; + +describeDriverMatrix( + "Gateway Response Start Timeout", + (driverTestConfig) => { + describe("gateway response start timeout", () => { + test("an action that outlives the timeout runs once, and the caller gets the timeout", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = client.responseStartTimeoutActor.getOrCreate([ + "runs-once", + ]); + + // The action reached the actor, so its result is unknown and + // neither the gateway nor the client may send it again. + await expect(actor.waitForRelease()).rejects.toMatchObject({ + group: "guard", + code: "gateway_response_start_timeout", + }); + await actor.release(); + expect(await actor.runs()).toBe(1); + }); + }); + }, + { + runtimes: ["native"], + encodings: ["bare"], + sqliteBackends: ["remote"], + config: { engine: { gatewayResponseStartTimeoutMs: 2_000 } }, + }, +); diff --git a/rivetkit-typescript/packages/rivetkit/tests/lifecycle-errors.test.ts b/rivetkit-typescript/packages/rivetkit/tests/lifecycle-errors.test.ts index 6d8b4c80de..b6ef15452a 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/lifecycle-errors.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/lifecycle-errors.test.ts @@ -10,7 +10,6 @@ describe("lifecycle error retry classification", () => { "tunnel_request_aborted", "tunnel_message_timeout", "tunnel_response_closed", - "gateway_response_start_timeout", ])("classifies guard.%s as retryable", (code) => { expect( isRetryableLifecycleRequestError( diff --git a/rivetkit-typescript/packages/rivetkit/tests/shared-engine.ts b/rivetkit-typescript/packages/rivetkit/tests/shared-engine.ts index b817c8abf5..ccad534a9b 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/shared-engine.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/shared-engine.ts @@ -40,6 +40,7 @@ export interface SharedTestEngine { export interface SharedTestEngineOptions { gateway3?: boolean; + gatewayResponseStartTimeoutMs?: number; } interface SharedEngineFiles { @@ -57,7 +58,10 @@ let sharedEngineProfile: string | undefined; let sharedEngineFiles: SharedEngineFiles | undefined; function resolveSharedEngineProfile(options: SharedTestEngineOptions): string { - return JSON.stringify({ gateway3: options.gateway3 ?? false }); + return JSON.stringify({ + gateway3: options.gateway3 ?? false, + gatewayResponseStartTimeoutMs: options.gatewayResponseStartTimeoutMs, + }); } function resolveSharedEngineFiles(profile: string): SharedEngineFiles { @@ -289,13 +293,19 @@ async function spawnSharedEngine( }, }, }, - pegboard: options.gateway3 - ? { - gateway_update_ping_interval_ms: 250, - gateway_tunnel_ping_timeout_ms: 2_000, - gateway_response_start_timeout_ms: 3_000, - } - : undefined, + // JSON.stringify drops undefined fields, and an empty pegboard + // section is the same as none. + pegboard: { + ...(options.gateway3 + ? { + gateway_update_ping_interval_ms: 250, + gateway_tunnel_ping_timeout_ms: 2_000, + } + : {}), + gateway_response_start_timeout_ms: + options.gatewayResponseStartTimeoutMs ?? + (options.gateway3 ? 3_000 : undefined), + }, features: options.gateway3 ? { guard_gateway_v3: {