Skip to content

Commit 097de41

Browse files
committed
feat:create-minute-exponential-backoff
1 parent 17e48a4 commit 097de41

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,64 @@ describe('ClientConfigApiService', () => {
192192
expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); // Initial + retries
193193
});
194194

195+
it('should retry at correct time intervals (1min, 2min, 4min)', async () => {
196+
jest.useFakeTimers();
197+
198+
const networkError = new Error('Network error');
199+
let callCount = 0;
200+
201+
// Mock fetch to fail 3 times then succeed, and track call times with fake Date.now()
202+
const mockFetch = jest.fn().mockImplementation(() => {
203+
callCount++;
204+
if (callCount <= 3) {
205+
return Promise.reject(networkError);
206+
}
207+
return Promise.resolve({
208+
ok: true,
209+
json: () => Promise.resolve([{ testFlag: true }]),
210+
});
211+
});
212+
213+
const clientConfigApiService = new ClientConfigApiService({
214+
fetch: mockFetch,
215+
retries: 3, // Allow 3 retries (4 total attempts)
216+
maximumConsecutiveFailures: 10, // Prevent circuit breaker from interfering
217+
config: {
218+
client: ClientType.Extension,
219+
distribution: DistributionType.Main,
220+
environment: EnvironmentType.Production,
221+
},
222+
});
223+
224+
// Start the operation
225+
const fetchPromise = clientConfigApiService.fetchRemoteFeatureFlags();
226+
227+
// Initial call should happen immediately
228+
await jest.advanceTimersByTimeAsync(0); // Process immediate promises
229+
expect(mockFetch).toHaveBeenCalledTimes(1);
230+
231+
// First retry should happen after 1 minute
232+
await jest.advanceTimersByTimeAsync(60 * 1000);
233+
expect(mockFetch).toHaveBeenCalledTimes(2);
234+
235+
// Second retry should happen after 2 more minutes
236+
await jest.advanceTimersByTimeAsync(2 * 60 * 1000);
237+
expect(mockFetch).toHaveBeenCalledTimes(3);
238+
239+
// Third retry should happen after 4 more minutes
240+
await jest.advanceTimersByTimeAsync(4 * 60 * 1000);
241+
expect(mockFetch).toHaveBeenCalledTimes(4);
242+
243+
// The final call should succeed
244+
const result = await fetchPromise;
245+
expect(result).toEqual({
246+
remoteFeatureFlags: { testFlag: true },
247+
cacheTimestamp: expect.any(Number),
248+
});
249+
250+
jest.useRealTimers();
251+
});
252+
195253
it('should call the onBreak callback when the circuit opens', async () => {
196254
const onBreak = jest.fn();
197255
const mockFetch = createMockFetch({ error: networkError });

packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
DEFAULT_CIRCUIT_BREAK_DURATION,
44
DEFAULT_MAX_CONSECUTIVE_FAILURES,
55
DEFAULT_MAX_RETRIES,
6+
ExponentialBackoff,
67
} from '@metamask/controller-utils';
78
import type { ServicePolicy } from '@metamask/controller-utils';
89

@@ -17,6 +18,22 @@ import type {
1718
ApiDataResponse,
1819
} from '../remote-feature-flag-controller-types';
1920

21+
/**
22+
* Custom backoff factory that implements exponential progression in minutes.
23+
* 1st retry: 1 minute, 2nd retry: 2 minutes, 3rd retry: 4 minutes, etc.
24+
* Uses Cockatiel's ExponentialBackoff with minute-based intervals.
25+
*/
26+
function createMinuteBasedExponentialBackoff() {
27+
return new ExponentialBackoff({
28+
// Start with 1 minute base delay (60,000 ms)
29+
initialDelay: 60 * 1000,
30+
// Use a multiplier of 2 for exponential progression (1min, 2min, 4min, 8min...)
31+
exponent: 2,
32+
// Maximum delay to prevent extremely long waits
33+
maxDelay: 15 * 60 * 1000, // 15 minutes max
34+
});
35+
}
36+
2037
/**
2138
* This service is responsible for fetching feature flags from the ClientConfig API.
2239
*/
@@ -138,6 +155,7 @@ export class ClientConfigApiService implements AbstractClientConfigApiService {
138155
maxRetries: retries,
139156
maxConsecutiveFailures: maximumConsecutiveFailures,
140157
circuitBreakDuration,
158+
backoff: createMinuteBasedExponentialBackoff(),
141159
});
142160
if (onBreak) {
143161
this.#policy.onBreak(onBreak);

0 commit comments

Comments
 (0)