Skip to content

Commit e95b628

Browse files
committed
fix: address code review feedback from silesky
- Revert shared backoff.ts defaults to original values (minTimeout: 500, maxTimeout: Infinity) — node publisher already passes explicit params - Clamp maxRetryCount in both rateLimitConfig and backoffConfig (0-100) to prevent unbounded retries from bad CDN config - Enforce 1s minimum on Retry-After to prevent tight request loops on Retry-After: 0 - Remove no-op `enabled` field from ResolvedRateLimitConfig and ResolvedBackoffConfig — kept in input types for CDN compat - Tighten success range from >= 100 to >= 200 (1xx are not valid final responses) - Update tests for revised 1xx and Retry-After: 0 behavior
1 parent 25271b3 commit e95b628

6 files changed

Lines changed: 35 additions & 30 deletions

File tree

packages/browser/src/lib/priority-queue/backoff.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
type BackoffParams = {
2-
/** The number of milliseconds before starting the first retry. Default is 100 */
2+
/** The number of milliseconds before starting the first retry. Default is 500 */
33
minTimeout?: number
44

5-
/** The maximum number of milliseconds between two retries. Default is 60000 (1 minute) */
5+
/** The maximum number of milliseconds between two retries. Default is Infinity */
66
maxTimeout?: number
77

88
/** The exponential factor to use. Default is 2. */
@@ -14,6 +14,11 @@ type BackoffParams = {
1414

1515
export function backoff(params: BackoffParams): number {
1616
const random = Math.random() + 1
17-
const { minTimeout = 100, factor = 2, attempt, maxTimeout = 60000 } = params
17+
const {
18+
minTimeout = 500,
19+
factor = 2,
20+
attempt,
21+
maxTimeout = Infinity,
22+
} = params
1823
return Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)
1924
}

packages/browser/src/plugins/segmentio/__tests__/shared-dispatcher.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ describe('resolveHttpConfig', () => {
1313
const resolved = resolveHttpConfig(undefined)
1414

1515
expect(resolved.rateLimitConfig).toEqual({
16-
enabled: true,
1716
maxRetryCount: 10,
1817
maxRetryInterval: 300,
1918
maxRateLimitDuration: 43200,
2019
})
2120

2221
expect(resolved.backoffConfig).toEqual({
23-
enabled: true,
2422
maxRetryCount: 10,
2523
baseBackoffInterval: 0.5,
2624
maxBackoffInterval: 60,
@@ -43,9 +41,7 @@ describe('resolveHttpConfig', () => {
4341
it('applies all defaults when called with empty object', () => {
4442
const resolved = resolveHttpConfig({})
4543

46-
expect(resolved.rateLimitConfig.enabled).toBe(true)
4744
expect(resolved.rateLimitConfig.maxRetryCount).toBe(10)
48-
expect(resolved.backoffConfig.enabled).toBe(true)
4945
expect(resolved.backoffConfig.maxRetryCount).toBe(10)
5046
expect(resolved.backoffConfig.baseBackoffInterval).toBe(0.5)
5147
})
@@ -76,13 +72,11 @@ describe('resolveHttpConfig', () => {
7672
const resolved = resolveHttpConfig(config)
7773

7874
expect(resolved.rateLimitConfig).toEqual({
79-
enabled: false,
8075
maxRetryCount: 50,
8176
maxRetryInterval: 120,
8277
maxRateLimitDuration: 3600,
8378
})
8479

85-
expect(resolved.backoffConfig.enabled).toBe(false)
8680
expect(resolved.backoffConfig.maxRetryCount).toBe(25)
8781
expect(resolved.backoffConfig.baseBackoffInterval).toBe(1)
8882
expect(resolved.backoffConfig.maxBackoffInterval).toBe(60)
@@ -109,10 +103,8 @@ describe('resolveHttpConfig', () => {
109103
expect(resolved.backoffConfig.jitterPercent).toBe(5)
110104

111105
// Defaults for missing fields
112-
expect(resolved.rateLimitConfig.enabled).toBe(true)
113106
expect(resolved.rateLimitConfig.maxRetryInterval).toBe(300)
114107
expect(resolved.rateLimitConfig.maxRateLimitDuration).toBe(43200)
115-
expect(resolved.backoffConfig.enabled).toBe(true)
116108
expect(resolved.backoffConfig.maxRetryCount).toBe(10)
117109
expect(resolved.backoffConfig.baseBackoffInterval).toBe(0.5)
118110
expect(resolved.backoffConfig.maxBackoffInterval).toBe(60)

packages/browser/src/plugins/segmentio/shared-dispatcher.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,12 @@ export interface HttpConfig {
138138
// --- Resolved types (all fields required, no undefined checks needed by consumers) ---
139139

140140
export interface ResolvedRateLimitConfig {
141-
enabled: boolean
142141
maxRetryCount: number
143142
maxRetryInterval: number
144143
maxRateLimitDuration: number
145144
}
146145

147146
export interface ResolvedBackoffConfig {
148-
enabled: boolean
149147
maxRetryCount: number
150148
baseBackoffInterval: number
151149
maxBackoffInterval: number
@@ -288,14 +286,12 @@ export function resolveHttpConfig(
288286

289287
return {
290288
rateLimitConfig: {
291-
enabled: rate?.enabled ?? true,
292-
maxRetryCount: rate?.maxRetryCount ?? 10,
289+
maxRetryCount: clamp(rate?.maxRetryCount, 10, 0, 100),
293290
maxRetryInterval: clamp(rate?.maxRetryInterval, 300, 0.1, 86400),
294291
maxRateLimitDuration: clamp(rate?.maxRateLimitDuration, 43200, 10, 86400),
295292
},
296293
backoffConfig: {
297-
enabled: backoff?.enabled ?? true,
298-
maxRetryCount: backoff?.maxRetryCount ?? 10,
294+
maxRetryCount: clamp(backoff?.maxRetryCount, 10, 0, 100),
299295
baseBackoffInterval: clamp(backoff?.baseBackoffInterval, 0.5, 0.1, 300),
300296
maxBackoffInterval: clamp(backoff?.maxBackoffInterval, 60, 0.1, 86400),
301297
maxTotalBackoffDuration: clamp(

packages/core/src/priority-queue/backoff.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
type BackoffParams = {
2-
/** The number of milliseconds before starting the first retry. Default is 100 */
2+
/** The number of milliseconds before starting the first retry. Default is 500 */
33
minTimeout?: number
44

5-
/** The maximum number of milliseconds between two retries. Default is 60000 (1 minute) */
5+
/** The maximum number of milliseconds between two retries. Default is Infinity */
66
maxTimeout?: number
77

88
/** The exponential factor to use. Default is 2. */
@@ -14,6 +14,11 @@ type BackoffParams = {
1414

1515
export function backoff(params: BackoffParams): number {
1616
const random = Math.random() + 1
17-
const { minTimeout = 100, factor = 2, attempt, maxTimeout = 60000 } = params
17+
const {
18+
minTimeout = 500,
19+
factor = 2,
20+
attempt,
21+
maxTimeout = Infinity,
22+
} = params
1823
return Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)
1924
}

packages/node/src/plugins/segmentio/__tests__/publisher.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ describe('error handling', () => {
409409
expect(err.message).toEqual(expect.stringContaining('500'))
410410
})
411411

412-
it('treats 1xx (<200) statuses as success (no retry)', async () => {
412+
it('retries 1xx (<200) statuses (not a valid final response)', async () => {
413413
jest.useRealTimers()
414414

415415
makeReqSpy.mockReturnValue(
@@ -424,10 +424,9 @@ describe('error handling', () => {
424424
const context = new Context(eventFactory.alias('to', 'from'))
425425
const updatedContext = await segmentPlugin.alias(context)
426426

427-
expect(makeReqSpy).toHaveBeenCalledTimes(1)
428-
validateMakeReqInputs(context)
429-
expect(updatedContext).toBe(context)
430-
expect(updatedContext.failedDelivery()).toBeFalsy()
427+
// 1xx is not a valid final response — retried then failed after maxRetries
428+
expect(makeReqSpy).toHaveBeenCalledTimes(3) // 1 initial + 2 retries
429+
expect(updatedContext.failedDelivery()).toBeTruthy()
431430
})
432431
it('retries fetch errors', async () => {
433432
// Jest kept timing out when using fake timers despite advancing time.
@@ -1074,7 +1073,7 @@ describe('retry semantics', () => {
10741073
})
10751074

10761075
it('T21 Safety cap: persistent 429 with Retry-After eventually fails', async () => {
1077-
jest.useRealTimers()
1076+
jest.useFakeTimers()
10781077
const headers = new TestHeaders()
10791078
headers.set('Retry-After', '0')
10801079

@@ -1092,7 +1091,13 @@ describe('retry semantics', () => {
10921091
})
10931092

10941093
const ctx = trackEvent()
1095-
const updated = await segmentPlugin.track(ctx)
1094+
const pending = segmentPlugin.track(ctx)
1095+
1096+
// Advance past all retry-after delays (1s minimum each, up to safety cap)
1097+
for (let i = 0; i < 25; i++) {
1098+
await jest.advanceTimersByTimeAsync(1000)
1099+
}
1100+
const updated = await pending
10961101

10971102
expect(makeReqSpy.mock.calls.length).toBeGreaterThan(1)
10981103
expect(updated.failedDelivery()).toBeTruthy()

packages/node/src/plugins/segmentio/publisher.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,9 @@ export class Publisher {
285285
private _setRateLimitState(headers: HTTPResponse['headers']): void {
286286
const retryAfterSeconds = getRetryAfterInSeconds(headers)
287287
if (typeof retryAfterSeconds === 'number') {
288-
this._rateLimitedUntil = Date.now() + retryAfterSeconds * 1000
288+
// Enforce a minimum of 1 second to prevent tight loops on Retry-After: 0
289+
this._rateLimitedUntil =
290+
Date.now() + Math.max(retryAfterSeconds, 1) * 1000
289291
} else {
290292
// No Retry-After header — use a default backoff of 60s
291293
this._rateLimitedUntil = Date.now() + 60000
@@ -391,8 +393,8 @@ export class Publisher {
391393

392394
const response = await this._httpClient.makeRequest(request)
393395

394-
// Per SDD: status codes 100–399 are treated as successful delivery.
395-
if (response.status >= 100 && response.status < 400) {
396+
// 2xx and 3xx are treated as successful delivery.
397+
if (response.status >= 200 && response.status < 400) {
396398
// Success — clear rate-limit state
397399
this._clearRateLimitState()
398400
batch.resolveEvents()

0 commit comments

Comments
 (0)