Skip to content

Commit

Permalink
fix: Improve remote evaluation fetch retry logic (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiuhc authored Jan 29, 2024
1 parent f420d85 commit 9b8a559
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 4 deletions.
13 changes: 10 additions & 3 deletions packages/experiment-browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EvaluationApi,
EvaluationEngine,
EvaluationFlag,
FetchError,
FlagApi,
Poller,
SdkEvaluationApi,
Expand Down Expand Up @@ -37,7 +38,6 @@ import {
isLocalEvaluationMode,
isNullOrUndefined,
isNullUndefinedOrEmpty,
isRemoteEvaluationMode,
} from './util';
import { Backoff } from './util/backoff';
import {
Expand Down Expand Up @@ -627,7 +627,7 @@ export class ExperimentClient implements Client {

this.debug(`[Experiment] Fetch all: retry=${retry}`);

// Proactively cancel retries if active in order to avoid unecessary API
// Proactively cancel retries if active in order to avoid unnecessary API
// requests. A new failure will restart the retries.
if (retry) {
this.stopRetries();
Expand All @@ -638,7 +638,7 @@ export class ExperimentClient implements Client {
await this.storeVariants(variants, options);
return variants;
} catch (e) {
if (retry) {
if (retry && this.shouldRetryFetch(e)) {
void this.startRetries(user, options);
}
throw e;
Expand Down Expand Up @@ -819,6 +819,13 @@ export class ExperimentClient implements Client {
console.debug(message, ...optionalParams);
}
}

private shouldRetryFetch(e: Error): boolean {
if (e instanceof FetchError) {
return e.statusCode < 400 || e.statusCode >= 500 || e.statusCode === 429;
}
return true;
}
}

type SourceVariant = {
Expand Down
48 changes: 48 additions & 0 deletions packages/experiment-browser/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AnalyticsConnector } from '@amplitude/analytics-connector';
import { FetchError } from '@amplitude/experiment-core';

import {
ExperimentAnalyticsProvider,
Expand Down Expand Up @@ -259,9 +260,11 @@ class TestAnalyticsProvider
track(): void {
return;
}

setUserProperty(): void {
return;
}

unsetUserProperty(): void {
return;
}
Expand Down Expand Up @@ -1048,3 +1051,48 @@ describe('start', () => {
expect(fetchSpy).toBeCalledTimes(0);
});
});

describe('fetch retry with different response codes', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test.each([
[300, 'Fetch Exception 300', 1],
[400, 'Fetch Exception 400', 0],
[429, 'Fetch Exception 429', 1],
[500, 'Fetch Exception 500', 1],
[0, 'Other Exception', 1],
])(
'responseCode=%p, errorMessage=%p, retryCalled=%p',
async (responseCode, errorMessage, retryCalled) => {
const client = new ExperimentClient(API_KEY, {
retryFetchOnFailure: true,
});
mockClientStorage(client);

jest
.spyOn(ExperimentClient.prototype as any, 'doFetch')
.mockImplementation(
async (user?: ExperimentUser, options?: FetchOptions) => {
return new Promise<ExperimentClient>((resolve, reject) => {
if (responseCode === 0) {
reject(new Error(errorMessage));
} else {
reject(new FetchError(responseCode, errorMessage));
}
});
},
);
const retryMock = jest.spyOn(
ExperimentClient.prototype as any,
'startRetries',
);
try {
await client.fetch({ user_id: 'test_user' });
} catch (e) {
// catch error
}
expect(retryMock).toHaveBeenCalledTimes(retryCalled);
},
);
});
6 changes: 5 additions & 1 deletion packages/experiment-core/src/api/evaluation-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Base64 } from 'js-base64';

import { FetchError } from '../evaluation/error';
import { EvaluationVariant } from '../evaluation/flag';
import { HttpClient } from '../transport/http';

Expand Down Expand Up @@ -55,7 +56,10 @@ export class SdkEvaluationApi implements EvaluationApi {
timeoutMillis: options?.timeoutMillis,
});
if (response.status != 200) {
throw Error(`Fetch error response: status=${response.status}`);
throw new FetchError(
response.status,
`Fetch error response: status=${response.status}`,
);
}
return JSON.parse(response.body);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/experiment-core/src/evaluation/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class FetchError extends Error {
statusCode: number;

constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
Object.setPrototypeOf(this, FetchError.prototype);
}
}
1 change: 1 addition & 0 deletions packages/experiment-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { FlagApi, SdkFlagApi } from './api/flag-api';
export { HttpClient, HttpRequest, HttpResponse } from './transport/http';
export { Poller } from './util/poller';
export { safeGlobal } from './util/global';
export { FetchError } from './evaluation/error';

0 comments on commit 9b8a559

Please sign in to comment.