Skip to content

Commit 94c6f67

Browse files
authored
fix: Polling after initial cached config (#126)
* polling interval param, keep polling and check cache expiration in callback * version bump
1 parent 9454e89 commit 94c6f67

File tree

5 files changed

+65
-33
lines changed

5 files changed

+65
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.2.0",
3+
"version": "4.3.0",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client.spec.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { IAssignmentLogger } from '../assignment-logger';
1616
import { IConfigurationStore } from '../configuration-store/configuration-store';
1717
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
18-
import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
18+
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
1919
import { Flag, ObfuscatedFlag, VariationType } from '../interfaces';
2020

2121
import EppoClient, { FlagConfigurationRequestParameters, checkTypeMatch } from './eppo-client';
@@ -554,7 +554,7 @@ describe('EppoClient E2E test', () => {
554554
const subject = 'alice';
555555
const pi = 3.1415926;
556556

557-
const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
557+
const maxRetryDelay = DEFAULT_POLL_INTERVAL_MS * POLL_JITTER_PCT;
558558

559559
beforeAll(async () => {
560560
global.fetch = jest.fn(() => {
@@ -630,6 +630,38 @@ describe('EppoClient E2E test', () => {
630630
expect(variation).toBe(pi);
631631
});
632632

633+
describe('Poll after successful start', () => {
634+
it('Continues to poll when cache has not expired', async () => {
635+
class MockStore<T> extends MemoryOnlyConfigurationStore<T> {
636+
public static expired = false;
637+
638+
async isExpired(): Promise<boolean> {
639+
return MockStore.expired;
640+
}
641+
}
642+
643+
client = new EppoClient(new MockStore(), undefined, undefined, {
644+
...requestConfiguration,
645+
pollAfterSuccessfulInitialization: true,
646+
});
647+
client.setIsGracefulFailureMode(false);
648+
// no configuration loaded
649+
let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
650+
expect(variation).toBe(0.0);
651+
652+
// have client fetch configurations; cache is not expired so assignment stays
653+
await client.fetchFlagConfigurations();
654+
variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
655+
expect(variation).toBe(0.0);
656+
657+
// Expire the cache and advance time until a reload should happen
658+
MockStore.expired = true;
659+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS * 1.5);
660+
661+
variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
662+
expect(variation).toBe(pi);
663+
});
664+
});
633665
it('Does not fetch configurations if the configuration store is unexpired', async () => {
634666
class MockStore<T> extends MemoryOnlyConfigurationStore<T> {
635667
async isExpired(): Promise<boolean> {
@@ -698,7 +730,7 @@ describe('EppoClient E2E test', () => {
698730
expect(variation).toBe(pi);
699731
expect(callCount).toBe(2);
700732

701-
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
733+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS);
702734
// By default, no more polling
703735
expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
704736
});
@@ -760,7 +792,7 @@ describe('EppoClient E2E test', () => {
760792
expect(client.getNumericAssignment(flagKey, subject, {}, 10.0)).toBe(10.0);
761793

762794
// Advance timers so a post-init poll can take place
763-
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
795+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS * 1.5);
764796

765797
// if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
766798
expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);

src/client/eppo-client.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
1616
DEFAULT_REQUEST_TIMEOUT_MS,
1717
MAX_EVENT_QUEUE_SIZE,
18-
POLL_INTERVAL_MS,
18+
DEFAULT_POLL_INTERVAL_MS,
1919
} from '../constants';
2020
import { decodeFlag } from '../decoding';
2121
import { EppoValue } from '../eppo_value';
@@ -60,6 +60,7 @@ export type FlagConfigurationRequestParameters = {
6060
sdkName: string;
6161
baseUrl?: string;
6262
requestTimeoutMs?: number;
63+
pollingIntervalMs?: number;
6364
numInitialRequestRetries?: number;
6465
numPollRequestRetries?: number;
6566
pollAfterSuccessfulInitialization?: boolean;
@@ -128,19 +129,9 @@ export default class EppoClient {
128129
'Eppo SDK unable to fetch flag configurations without configuration request parameters',
129130
);
130131
}
132+
// if fetchFlagConfigurations() was previously called, stop any polling process from that call
133+
this.requestPoller?.stop();
131134

132-
if (this.requestPoller) {
133-
// if fetchFlagConfigurations() was previously called, stop any polling process from that call
134-
this.requestPoller.stop();
135-
}
136-
137-
const isExpired = await this.flagConfigurationStore.isExpired();
138-
if (!isExpired) {
139-
logger.info(
140-
'[Eppo SDK] Configuration store is not expired. Skipping fetching flag configurations',
141-
);
142-
return;
143-
}
144135
const {
145136
apiKey,
146137
sdkName,
@@ -154,6 +145,13 @@ export default class EppoClient {
154145
throwOnFailedInitialization = false,
155146
skipInitialPoll = false,
156147
} = this.configurationRequestParameters;
148+
149+
let { pollingIntervalMs = DEFAULT_POLL_INTERVAL_MS } = this.configurationRequestParameters;
150+
if (pollingIntervalMs <= 0) {
151+
logger.error('pollingIntervalMs must be greater than 0. Using default');
152+
pollingIntervalMs = DEFAULT_POLL_INTERVAL_MS;
153+
}
154+
157155
// todo: Inject the chain of dependencies below
158156
const apiEndpoints = new ApiEndpoints({
159157
baseUrl,
@@ -167,18 +165,20 @@ export default class EppoClient {
167165
this.banditModelConfigurationStore ?? null,
168166
);
169167

170-
this.requestPoller = initPoller(
171-
POLL_INTERVAL_MS,
172-
configurationRequestor.fetchAndStoreConfigurations.bind(configurationRequestor),
173-
{
174-
maxStartRetries: numInitialRequestRetries,
175-
maxPollRetries: numPollRequestRetries,
176-
pollAfterSuccessfulStart: pollAfterSuccessfulInitialization,
177-
pollAfterFailedStart: pollAfterFailedInitialization,
178-
errorOnFailedStart: throwOnFailedInitialization,
179-
skipInitialPoll: skipInitialPoll,
180-
},
181-
);
168+
const pollingCallback = async () => {
169+
if (await this.flagConfigurationStore.isExpired()) {
170+
return configurationRequestor.fetchAndStoreConfigurations();
171+
}
172+
};
173+
174+
this.requestPoller = initPoller(pollingIntervalMs, pollingCallback, {
175+
maxStartRetries: numInitialRequestRetries,
176+
maxPollRetries: numPollRequestRetries,
177+
pollAfterSuccessfulStart: pollAfterSuccessfulInitialization,
178+
pollAfterFailedStart: pollAfterFailedInitialization,
179+
errorOnFailedStart: throwOnFailedInitialization,
180+
skipInitialPoll: skipInitialPoll,
181+
});
182182

183183
await this.requestPoller.start();
184184
}

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
22
export const REQUEST_TIMEOUT_MILLIS = DEFAULT_REQUEST_TIMEOUT_MS; // for backwards compatibility
3-
export const POLL_INTERVAL_MS = 30000;
3+
export const DEFAULT_POLL_INTERVAL_MS = 30000;
44
export const POLL_JITTER_PCT = 0.1;
55
export const DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES = 1;
66
export const DEFAULT_POLL_CONFIG_REQUEST_RETRIES = 7;

src/poller.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as td from 'testdouble';
22

3-
import { POLL_INTERVAL_MS, POLL_JITTER_PCT } from './constants';
3+
import { DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from './constants';
44
import initPoller from './poller';
55

66
describe('poller', () => {
7-
const testIntervalMs = POLL_INTERVAL_MS;
7+
const testIntervalMs = DEFAULT_POLL_INTERVAL_MS;
88
const maxRetryDelay = testIntervalMs * POLL_JITTER_PCT;
99
const noOpCallback = td.func<() => Promise<void>>();
1010

0 commit comments

Comments
 (0)