diff --git a/server/src/internal/misc/rollouts/fullSubjectRolloutUtils.ts b/server/src/internal/misc/rollouts/fullSubjectRolloutUtils.ts index b0dfcf2f7..1343b1224 100644 --- a/server/src/internal/misc/rollouts/fullSubjectRolloutUtils.ts +++ b/server/src/internal/misc/rollouts/fullSubjectRolloutUtils.ts @@ -27,4 +27,6 @@ export const isRetryableFullSubjectRolloutError = ({ error: unknown; }) => isRetryableDbError({ error }) || - (error instanceof Error && RETRYABLE_REDIS_ERROR_NAMES.has(error.name)); + (error instanceof Error && + (RETRYABLE_REDIS_ERROR_NAMES.has(error.name) || + error.message === "Command timed out")); diff --git a/server/tests/unit/balances/check-v2/runCheckWithRollout.test.ts b/server/tests/unit/balances/check-v2/runCheckWithRollout.test.ts index e44935fbb..85149ca21 100644 --- a/server/tests/unit/balances/check-v2/runCheckWithRollout.test.ts +++ b/server/tests/unit/balances/check-v2/runCheckWithRollout.test.ts @@ -164,4 +164,40 @@ describe("runCheckWithRollout", () => { }, }); }); + + test("returns fail-open fallback when the v2 flow hits a redis command timeout", async () => { + mockState.legacyCalls = []; + mockState.v2Calls = []; + mockState.v2Error = new Error("Command timed out"); + mockState.warnCalls = []; + + const result = await runCheckWithRollout({ + ctx: { + apiVersion: { value: "2025-02-01" }, + features: [], + logger: { + warn: (...args: unknown[]) => mockState.warnCalls.push(args), + }, + rolloutSnapshot: { + rolloutId: "v2-cache", + enabled: true, + percent: 100, + previousPercent: 0, + changedAt: 1, + customerBucket: 10, + }, + } as never, + body: { customer_id: "cus_123", feature_id: "messages" } as never, + requiredBalance: 1, + }); + + expect(result).toMatchObject({ + checkData: null, + response: { + allowed: true, + customer_id: "cus_123", + required_balance: 1, + }, + }); + }); }); diff --git a/server/tests/unit/rollouts/fullSubjectRolloutUtils.test.ts b/server/tests/unit/rollouts/fullSubjectRolloutUtils.test.ts index 1b582ecf6..ce56bb65f 100644 --- a/server/tests/unit/rollouts/fullSubjectRolloutUtils.test.ts +++ b/server/tests/unit/rollouts/fullSubjectRolloutUtils.test.ts @@ -20,6 +20,14 @@ describe("fullSubjectRolloutUtils", () => { ).toBe(true); }); + test("treats ioredis command timeouts as retryable rollout errors", () => { + expect( + isRetryableFullSubjectRolloutError({ + error: new Error("Command timed out"), + }), + ).toBe(true); + }); + test("does not treat application errors as retryable rollout errors", () => { expect( isRetryableFullSubjectRolloutError({