Skip to content

Commit 4fc8fef

Browse files
committed
polling interval param, keep polling and check cache expiration in callback
1 parent 90dabbc commit 4fc8fef

File tree

4 files changed

+62
-32
lines changed

4 files changed

+62
-32
lines changed

src/client/eppo-client.spec.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { IAssignmentLogger } from '../assignment-logger';
1717
import ConfigurationRequestor from '../configuration-requestor';
1818
import { IConfigurationStore } from '../configuration-store/configuration-store';
1919
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
20-
import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
20+
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
2121
import FetchHttpClient from '../http-client';
2222
import { Flag, ObfuscatedFlag, VariationType } from '../interfaces';
2323

@@ -576,7 +576,7 @@ describe('EppoClient E2E test', () => {
576576
const subject = 'alice';
577577
const pi = 3.1415926;
578578

579-
const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
579+
const maxRetryDelay = DEFAULT_POLL_INTERVAL_MS * POLL_JITTER_PCT;
580580

581581
beforeAll(async () => {
582582
global.fetch = jest.fn(() => {
@@ -652,6 +652,38 @@ describe('EppoClient E2E test', () => {
652652
expect(variation).toBe(pi);
653653
});
654654

655+
describe('Poll after successful start', () => {
656+
it('Continues to poll when cache has not expired', async () => {
657+
class MockStore<T> extends MemoryOnlyConfigurationStore<T> {
658+
public static expired = false;
659+
660+
async isExpired(): Promise<boolean> {
661+
return MockStore.expired;
662+
}
663+
}
664+
665+
client = new EppoClient(new MockStore(), undefined, undefined, {
666+
...requestConfiguration,
667+
pollAfterSuccessfulInitialization: true,
668+
});
669+
client.setIsGracefulFailureMode(false);
670+
// no configuration loaded
671+
let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
672+
expect(variation).toBe(0.0);
673+
674+
// have client fetch configurations; cache is not expired so assignment stays
675+
await client.fetchFlagConfigurations();
676+
variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
677+
expect(variation).toBe(0.0);
678+
679+
// Expire the cache and advance time until a reload should happen
680+
MockStore.expired = true;
681+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS * 1.5);
682+
683+
variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
684+
expect(variation).toBe(pi);
685+
});
686+
});
655687
it('Does not fetch configurations if the configuration store is unexpired', async () => {
656688
class MockStore<T> extends MemoryOnlyConfigurationStore<T> {
657689
async isExpired(): Promise<boolean> {
@@ -720,7 +752,7 @@ describe('EppoClient E2E test', () => {
720752
expect(variation).toBe(pi);
721753
expect(callCount).toBe(2);
722754

723-
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
755+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS);
724756
// By default, no more polling
725757
expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
726758
});
@@ -782,7 +814,7 @@ describe('EppoClient E2E test', () => {
782814
expect(client.getNumericAssignment(flagKey, subject, {}, 10.0)).toBe(10.0);
783815

784816
// Advance timers so a post-init poll can take place
785-
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
817+
await jest.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL_MS * 1.5);
786818

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

src/client/eppo-client.ts

Lines changed: 23 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;
@@ -121,32 +122,27 @@ export default class EppoClient {
121122
'Eppo SDK unable to fetch flag configurations without configuration request parameters',
122123
);
123124
}
125+
// if fetchFlagConfigurations() was previously called, stop any polling process from that call
126+
this.requestPoller?.stop();
124127

125-
if (this.requestPoller) {
126-
// if fetchFlagConfigurations() was previously called, stop any polling process from that call
127-
this.requestPoller.stop();
128-
}
129-
130-
const isExpired = await this.flagConfigurationStore.isExpired();
131-
if (!isExpired) {
132-
logger.info(
133-
'[Eppo SDK] Configuration store is not expired. Skipping fetching flag configurations',
134-
);
135-
return;
136-
}
137128
const {
138129
apiKey,
139130
sdkName,
140131
sdkVersion,
141132
baseUrl, // Default is set in ApiEndpoints constructor if undefined
142133
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
134+
pollingIntervalMs = DEFAULT_POLL_INTERVAL_MS,
143135
numInitialRequestRetries = DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
144136
numPollRequestRetries = DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
145137
pollAfterSuccessfulInitialization = false,
146138
pollAfterFailedInitialization = false,
147139
throwOnFailedInitialization = false,
148140
skipInitialPoll = false,
149141
} = this.configurationRequestParameters;
142+
if (pollingIntervalMs <= 0) {
143+
logger.error('PollingIntervalMs must be greater than 0');
144+
}
145+
150146
// todo: Inject the chain of dependencies below
151147
const apiEndpoints = new ApiEndpoints({
152148
baseUrl,
@@ -160,18 +156,20 @@ export default class EppoClient {
160156
this.banditModelConfigurationStore ?? null,
161157
);
162158

163-
this.requestPoller = initPoller(
164-
POLL_INTERVAL_MS,
165-
configurationRequestor.fetchAndStoreConfigurations.bind(configurationRequestor),
166-
{
167-
maxStartRetries: numInitialRequestRetries,
168-
maxPollRetries: numPollRequestRetries,
169-
pollAfterSuccessfulStart: pollAfterSuccessfulInitialization,
170-
pollAfterFailedStart: pollAfterFailedInitialization,
171-
errorOnFailedStart: throwOnFailedInitialization,
172-
skipInitialPoll: skipInitialPoll,
173-
},
174-
);
159+
const pollingCallback = async () => {
160+
if (await this.flagConfigurationStore.isExpired()) {
161+
return configurationRequestor.fetchAndStoreConfigurations();
162+
}
163+
};
164+
165+
this.requestPoller = initPoller(pollingIntervalMs, pollingCallback, {
166+
maxStartRetries: numInitialRequestRetries,
167+
maxPollRetries: numPollRequestRetries,
168+
pollAfterSuccessfulStart: pollAfterSuccessfulInitialization,
169+
pollAfterFailedStart: pollAfterFailedInitialization,
170+
errorOnFailedStart: throwOnFailedInitialization,
171+
skipInitialPoll: skipInitialPoll,
172+
});
175173

176174
await this.requestPoller.start();
177175
}

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)