Skip to content

Commit cb32185

Browse files
committed
feat: add request retry observability
1 parent 21dfa40 commit cb32185

2 files changed

Lines changed: 81 additions & 17 deletions

File tree

index.ts

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -350,22 +350,27 @@ let sessionAffinityWriteVersion = 0;
350350

351351
type RuntimeMetrics = {
352352
startedAt: number;
353-
totalRequests: number;
354-
successfulRequests: number;
355-
failedRequests: number;
356-
rateLimitedResponses: number;
357-
serverErrors: number;
358-
networkErrors: number;
359-
userAborts: number;
360-
authRefreshFailures: number;
361-
emptyResponseRetries: number;
362-
accountRotations: number;
363-
sameAccountRetries: number;
364-
streamFailoverAttempts: number;
365-
streamFailoverRecoveries: number;
366-
streamFailoverCrossAccountRecoveries: number;
367-
cumulativeLatencyMs: number;
368-
lastRequestAt: number | null;
353+
totalRequests: number;
354+
successfulRequests: number;
355+
failedRequests: number;
356+
outboundRequestAttemptBudget: number | null;
357+
outboundRequestAttemptsConsumed: number;
358+
requestAttemptBudgetExhaustions: number;
359+
rateLimitedResponses: number;
360+
serverErrors: number;
361+
networkErrors: number;
362+
userAborts: number;
363+
authRefreshFailures: number;
364+
emptyResponseRetries: number;
365+
accountRotations: number;
366+
sameAccountRetries: number;
367+
streamFailoverAttempts: number;
368+
streamFailoverCandidatesConsidered: number;
369+
lastStreamFailoverCandidateCount: number;
370+
streamFailoverRecoveries: number;
371+
streamFailoverCrossAccountRecoveries: number;
372+
cumulativeLatencyMs: number;
373+
lastRequestAt: number | null;
369374
lastError: string | null;
370375
};
371376

@@ -374,6 +379,9 @@ let sessionAffinityWriteVersion = 0;
374379
totalRequests: 0,
375380
successfulRequests: 0,
376381
failedRequests: 0,
382+
outboundRequestAttemptBudget: null,
383+
outboundRequestAttemptsConsumed: 0,
384+
requestAttemptBudgetExhaustions: 0,
377385
rateLimitedResponses: 0,
378386
serverErrors: 0,
379387
networkErrors: 0,
@@ -383,6 +391,8 @@ let sessionAffinityWriteVersion = 0;
383391
accountRotations: 0,
384392
sameAccountRetries: 0,
385393
streamFailoverAttempts: 0,
394+
streamFailoverCandidatesConsidered: 0,
395+
lastStreamFailoverCandidateCount: 0,
386396
streamFailoverRecoveries: 0,
387397
streamFailoverCrossAccountRecoveries: 0,
388398
cumulativeLatencyMs: 0,
@@ -935,21 +945,39 @@ let sessionAffinityWriteVersion = 0;
935945
emptyResponseMaxRetries,
936946
streamFailoverMax,
937947
});
948+
runtimeMetrics.outboundRequestAttemptBudget =
949+
maxOutboundRequestAttempts;
950+
logDebug("Configured outbound request attempt budget.", {
951+
budget: maxOutboundRequestAttempts,
952+
accountCount: accountManager.getAccountCount(),
953+
maxSameAccountRetries,
954+
emptyResponseMaxRetries,
955+
streamFailoverMax,
956+
});
938957
let outboundRequestAttemptsRemaining =
939958
maxOutboundRequestAttempts;
940959
const tryConsumeOutboundRequestAttempt = (
941960
reason: "primary" | "stream-failover",
942961
accountIndex: number,
943962
): boolean => {
944963
if (outboundRequestAttemptsRemaining <= 0) {
964+
runtimeMetrics.requestAttemptBudgetExhaustions += 1;
945965
runtimeMetrics.lastError =
946966
`Request attempt budget exhausted after ${maxOutboundRequestAttempts} outbound request(s)`;
947967
logWarn(
948-
`Stopping ${reason} replay after exhausting ${maxOutboundRequestAttempts} outbound request(s) on account ${accountIndex + 1}.`,
968+
"Request attempt budget exhausted.",
969+
{
970+
reason,
971+
accountIndex,
972+
budget: maxOutboundRequestAttempts,
973+
consumed:
974+
runtimeMetrics.outboundRequestAttemptsConsumed,
975+
},
949976
);
950977
return false;
951978
}
952979

980+
runtimeMetrics.outboundRequestAttemptsConsumed += 1;
953981
outboundRequestAttemptsRemaining -= 1;
954982
return true;
955983
};
@@ -2063,6 +2091,19 @@ let sessionAffinityWriteVersion = 0;
20632091
(candidate) => candidate.index,
20642092
),
20652093
);
2094+
runtimeMetrics.lastStreamFailoverCandidateCount =
2095+
streamFallbackCandidateOrder.length;
2096+
runtimeMetrics.streamFailoverCandidatesConsidered +=
2097+
streamFallbackCandidateOrder.length;
2098+
logDebug(
2099+
"Prepared stream failover candidates.",
2100+
{
2101+
primaryAccountIndex: account.index,
2102+
candidateCount:
2103+
streamFallbackCandidateOrder.length,
2104+
candidateIndices: streamFallbackCandidateOrder,
2105+
},
2106+
);
20662107
responseForSuccess = withStreamingFailover(
20672108
response,
20682109
async (failoverAttempt, emittedBytes) => {

test/index-retry.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,10 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
486486
});
487487

488488
it("stops after the bounded outbound request budget even when more accounts are available", async () => {
489+
const logger = await import("../lib/logger.js");
490+
const logDebugSpy = vi.spyOn(logger, "logDebug").mockImplementation(() => {});
491+
const logWarnSpy = vi.spyOn(logger, "logWarn").mockImplementation(() => {});
492+
489493
const accounts = Array.from({ length: 8 }, (_, index) =>
490494
createMockAccount({
491495
index,
@@ -540,6 +544,25 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
540544
"Request attempt budget exhausted after 6 outbound request(s). Try again after the current retries settle.",
541545
},
542546
});
547+
expect(logDebugSpy).toHaveBeenCalledWith(
548+
"Configured outbound request attempt budget.",
549+
expect.objectContaining({
550+
budget: 6,
551+
accountCount: 8,
552+
maxSameAccountRetries: 1,
553+
emptyResponseMaxRetries: 2,
554+
streamFailoverMax: 1,
555+
}),
556+
);
557+
expect(logWarnSpy).toHaveBeenCalledWith(
558+
"Request attempt budget exhausted.",
559+
expect.objectContaining({
560+
reason: "primary",
561+
accountIndex: 6,
562+
budget: 6,
563+
consumed: 6,
564+
}),
565+
);
543566
});
544567

545568
it("rebuilds request headers after rotating to the next workspace", async () => {

0 commit comments

Comments
 (0)