@@ -11,6 +11,7 @@ import {
1111 requestTokenWithRetry ,
1212 TOKEN_EXPIRY_MARGIN_MS ,
1313 TOKEN_FAILURE_STREAK_RETENTION_MS ,
14+ type TokenRequestFailure ,
1415} from '@/lib/credentials/token-request-resilience'
1516import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors'
1617import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks'
@@ -92,13 +93,15 @@ interface StoredApplicationToken {
9293/**
9394 * A remembered exchange failure, replayed until its row's `expiresAt`. Builds before failure
9495 * kinds stored `{ code, status }` only and ignore other keys, so `consecutiveFailures` is
95- * written only for outages and throttling, to grow their backoff while they continue, and
96- * `refusal` only for a refusal whose code those builds read as `provider_unavailable`.
96+ * written only for outages and throttling, to grow their backoff while they continue, `throttled`
97+ * only for throttling, so its streak is kept apart from an outage's, and `refusal` only for a
98+ * refusal whose code those builds read as `provider_unavailable`.
9799 */
98100interface StoredFailure {
99101 code : 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found' | 'provider_unavailable'
100102 status : number
101103 consecutiveFailures ?: number
104+ throttled ?: true
102105 refusal ?: true
103106}
104107
@@ -206,6 +209,7 @@ function decodeFailure(value: string, step: string): StoredFailure {
206209 ...( Number . isSafeInteger ( parsed . consecutiveFailures ) && Number ( parsed . consecutiveFailures ) > 0
207210 ? { consecutiveFailures : Number ( parsed . consecutiveFailures ) }
208211 : { } ) ,
212+ ...( parsed . throttled === true ? { throttled : true } : { } ) ,
209213 ...( parsed . refusal === true ? { refusal : true } : { } ) ,
210214 }
211215}
@@ -230,20 +234,24 @@ function replayedFailure(
230234}
231235
232236/**
233- * Outages and throttled answers in a row before this exchange, read from the application's last
234- * failure memo for this secret once its replay has ended. Anything unreadable starts a new streak.
237+ * The outage or throttling streak before this exchange, read from the application's last failure
238+ * memo for this secret once its replay has ended. A refusal, or anything unreadable, starts a new
239+ * streak.
235240 */
236- async function consecutiveFailuresBefore (
241+ async function failureStreakBefore (
237242 row : StoredRow | undefined ,
238243 step : string
239- ) : Promise < number > {
240- if ( ! row || Date . now ( ) - row . expiresAt . getTime ( ) > TOKEN_FAILURE_STREAK_RETENTION_MS ) return 0
244+ ) : Promise < Pick < TokenRequestFailure , 'kind' | 'consecutiveFailures' > | undefined > {
245+ if ( ! row || Date . now ( ) - row . expiresAt . getTime ( ) > TOKEN_FAILURE_STREAK_RETENTION_MS ) return
241246 try {
242247 const failure = decodeFailure ( ( await decryptSecret ( row . encryptedValue ) ) . decrypted , step )
243- if ( isRememberedRefusal ( failure ) ) return 0
244- return failure . consecutiveFailures ?? 1
248+ if ( isRememberedRefusal ( failure ) ) return
249+ return {
250+ kind : failure . throttled ? 'throttled' : 'transient' ,
251+ consecutiveFailures : failure . consecutiveFailures ?? 1 ,
252+ }
245253 } catch {
246- return 0
254+ return
247255 }
248256}
249257
@@ -418,7 +426,7 @@ export async function resolveCoordinatedClientToken(
418426 if ( ! ( error instanceof TokenServiceAccountValidationError ) ) throw error
419427 const assessed = assessTokenRequestFailure (
420428 error ,
421- await consecutiveFailuresBefore ( failureRow , step )
429+ await failureStreakBefore ( failureRow , step )
422430 )
423431 const replayMs = Math . max ( assessed . replayMs , input . minExchangeIntervalMs ?? 0 )
424432 const code = STORED_FAILURE_CODES . has ( error . code )
@@ -428,7 +436,10 @@ export async function resolveCoordinatedClientToken(
428436 code,
429437 status : error . status ,
430438 ...( assessed . kind !== 'permanent'
431- ? { consecutiveFailures : assessed . consecutiveFailures }
439+ ? {
440+ consecutiveFailures : assessed . consecutiveFailures ,
441+ ...( assessed . kind === 'throttled' ? { throttled : true } : { } ) ,
442+ }
432443 : isPermanentTokenFailureCode ( code )
433444 ? { }
434445 : { refusal : true } ) ,
0 commit comments