mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
Fix/frontendapi synchronization (#2100)
* feat: add db fetch polling for proxy endpoints * feat: add test for retrieving cache on interval * feat: configurable interval * fix: add config options * feat: docs * fix: add config to proxy-repository * fix: update snapshots * Update website/docs/reference/front-end-api.md Co-authored-by: Thomas Heartman <thomas@getunleash.ai> * fix: update docs * Update website/docs/deploy/configuring-unleash.md Co-authored-by: Thomas Heartman <thomas@getunleash.ai> * Update website/docs/reference/front-end-api.md Co-authored-by: Thomas Heartman <thomas@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
parent
0086f2f19f
commit
7fbe227e0f
@ -87,6 +87,9 @@ exports[`should create default config 1`] = `
|
||||
"isEnabled": [Function],
|
||||
},
|
||||
},
|
||||
"frontendApi": {
|
||||
"refreshIntervalInMs": 10000,
|
||||
},
|
||||
"frontendApiOrigins": [
|
||||
"*",
|
||||
],
|
||||
|
@ -405,6 +405,13 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
listen = { host: server.host || undefined, port: server.port };
|
||||
}
|
||||
|
||||
const frontendApi = options.frontendApi || {
|
||||
refreshIntervalInMs: parseEnvVarNumber(
|
||||
process.env.FRONTEND_API_REFRESH_INTERVAL_MS,
|
||||
10000,
|
||||
),
|
||||
};
|
||||
|
||||
const secureHeaders =
|
||||
options.secureHeaders ||
|
||||
parseEnvVarBoolean(process.env.SECURE_HEADERS, false);
|
||||
@ -449,6 +456,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||
import: importSetting,
|
||||
experimental,
|
||||
flagResolver,
|
||||
frontendApi,
|
||||
email,
|
||||
secureHeaders,
|
||||
enableOAS,
|
||||
|
@ -13,7 +13,7 @@ import { UnleashEvents } from 'unleash-client';
|
||||
import { ANY_EVENT } from '../util/anyEventEmitter';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
type Config = Pick<IUnleashConfig, 'getLogger'>;
|
||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
|
||||
|
||||
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
||||
|
||||
@ -40,6 +40,10 @@ export class ProxyRepository
|
||||
|
||||
private segments: Segment[];
|
||||
|
||||
private interval: number;
|
||||
|
||||
private timer: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
stores: Stores,
|
||||
@ -53,6 +57,7 @@ export class ProxyRepository
|
||||
this.services = services;
|
||||
this.token = token;
|
||||
this.onAnyEvent = this.onAnyEvent.bind(this);
|
||||
this.interval = config.frontendApi.refreshIntervalInMs;
|
||||
}
|
||||
|
||||
getSegment(id: number): Segment | undefined {
|
||||
@ -80,11 +85,24 @@ export class ProxyRepository
|
||||
|
||||
stop(): void {
|
||||
this.stores.eventStore.off(ANY_EVENT, this.onAnyEvent);
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
private async loadDataForToken() {
|
||||
this.features = await this.featuresForToken();
|
||||
this.segments = await this.segmentsForToken();
|
||||
this.timer = setTimeout(async () => {
|
||||
await this.loadDataForToken();
|
||||
}, this.randomizeDelay(this.interval, this.interval * 2)).unref();
|
||||
|
||||
try {
|
||||
this.features = await this.featuresForToken();
|
||||
this.segments = await this.segmentsForToken();
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private randomizeDelay(floor: number, ceiling: number): number {
|
||||
return Math.floor(Math.random() * (ceiling - floor) + floor);
|
||||
}
|
||||
|
||||
private async onAnyEvent() {
|
||||
|
@ -15,7 +15,7 @@ import assert from 'assert';
|
||||
import { ApiTokenType } from '../types/models/api-token';
|
||||
import { ProxyMetricsSchema } from '../openapi/spec/proxy-metrics-schema';
|
||||
|
||||
type Config = Pick<IUnleashConfig, 'getLogger'>;
|
||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
|
||||
|
||||
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
||||
|
||||
@ -118,6 +118,10 @@ export class ProxyService {
|
||||
this.clients.delete(secret);
|
||||
}
|
||||
|
||||
stopAll(): void {
|
||||
this.clients.forEach((client) => client.destroy());
|
||||
}
|
||||
|
||||
private static assertExpectedTokenType({ type }: ApiUser) {
|
||||
assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN);
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ export interface IUnleashOptions {
|
||||
versionCheck?: Partial<IVersionOption>;
|
||||
authentication?: Partial<IAuthOption>;
|
||||
ui?: object;
|
||||
frontendApi?: IFrontendApi;
|
||||
import?: Partial<IImportOption>;
|
||||
experimental?: Partial<IExperimentalOptions>;
|
||||
email?: Partial<IEmailOption>;
|
||||
@ -167,6 +168,10 @@ export interface ICspDomainConfig {
|
||||
imgSrc: string[];
|
||||
}
|
||||
|
||||
interface IFrontendApi {
|
||||
refreshIntervalInMs: number;
|
||||
}
|
||||
|
||||
export interface IUnleashConfig {
|
||||
db: IDBOption;
|
||||
session: ISessionOption;
|
||||
@ -191,6 +196,7 @@ export interface IUnleashConfig {
|
||||
eventBus: EventEmitter;
|
||||
disableLegacyFeaturesApi?: boolean;
|
||||
environmentEnableOverrides?: string[];
|
||||
frontendApi: IFrontendApi;
|
||||
inlineSegmentConstraints: boolean;
|
||||
segmentValuesLimit: number;
|
||||
strategySegmentsLimit: number;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from '../../../../lib/types/models/api-token';
|
||||
import { startOfHour } from 'date-fns';
|
||||
import { IConstraint, IStrategyConfig } from '../../../../lib/types/model';
|
||||
import { ProxyRepository } from '../../../../lib/proxy/proxy-repository';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
@ -20,6 +21,10 @@ beforeAll(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
app.services.proxyService.stopAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
@ -790,3 +795,65 @@ test('should filter features by segment', async () => {
|
||||
.expect(200)
|
||||
.expect((res) => expect(res.body).toEqual({ toggles: [] }));
|
||||
});
|
||||
|
||||
test('Should sync proxy for keys on an interval', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||
const user = await app.services.apiTokenService.getUserForToken(
|
||||
frontendToken.secret,
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(
|
||||
ProxyRepository.prototype as any,
|
||||
'featuresForToken',
|
||||
);
|
||||
const proxyRepository = new ProxyRepository(
|
||||
{
|
||||
getLogger,
|
||||
frontendApi: { refreshIntervalInMs: 5000 },
|
||||
},
|
||||
db.stores,
|
||||
app.services,
|
||||
user,
|
||||
);
|
||||
|
||||
await proxyRepository.start();
|
||||
|
||||
jest.advanceTimersByTime(60000);
|
||||
|
||||
proxyRepository.stop();
|
||||
expect(spy.mock.calls.length > 6).toBe(true);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('Should change fetch interval', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||
const user = await app.services.apiTokenService.getUserForToken(
|
||||
frontendToken.secret,
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(
|
||||
ProxyRepository.prototype as any,
|
||||
'featuresForToken',
|
||||
);
|
||||
const proxyRepository = new ProxyRepository(
|
||||
{
|
||||
getLogger,
|
||||
frontendApi: { refreshIntervalInMs: 1000 },
|
||||
},
|
||||
db.stores,
|
||||
app.services,
|
||||
user,
|
||||
);
|
||||
|
||||
await proxyRepository.start();
|
||||
|
||||
jest.advanceTimersByTime(60000);
|
||||
|
||||
proxyRepository.stop();
|
||||
expect(spy.mock.calls.length > 30).toBe(true);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
@ -129,6 +129,8 @@ unleash.start(unleashOptions);
|
||||
- **clientFeatureCaching** - configuring memoization of the /api/client/features endpoint
|
||||
- `enabled` - set to true by default - Overridable with (`CLIENT_FEATURE_CACHING_ENABLED`)
|
||||
- `maxAge` - the time to cache features, set to 600 milliseconds by default - Overridable with (`CLIENT_FEATURE_CACHING_MAXAGE`) ) (accepts milliseconds)
|
||||
- **frontendApi** - Configuration options for the [Unleash front-end API](../reference/front-end-api.md).
|
||||
- `refreshIntervalInMs` - how often (in milliseconds) front-end clients should refresh their data from the cache. Overridable with the `FRONTEND_API_REFRESH_INTERVAL_MS` environment variable.
|
||||
|
||||
You can also set the environment variable `ENABLED_ENVIRONMENTS` to a comma delimited string of environment names to override environments.
|
||||
|
||||
|
@ -47,3 +47,11 @@ The client needs to point to the correct API endpoint. The front-end API is avai
|
||||
### API token
|
||||
|
||||
You can create appropriate token, with type `FRONTEND` on `<YOUR_UNLEASH_URL>/admin/api/create-token` page or with a request to `/api/admin/api-tokens`. See our guide on [how to create API tokens](../user_guide/token.mdx) for more details.
|
||||
|
||||
### Refresh interval for tokens
|
||||
|
||||
Internally, Unleash creates a new Unleash client for each token it receives. Each client is configured with the project and environment specified in the token.
|
||||
|
||||
Each client updates its feature toggle configuration at a specified refresh interval plus a random offset between 0 and 10 seconds. By default, the refresh interval is set to 10 seconds. The random offset is used to stagger incoming requests to avoid a large number of clients all querying the database simultaneously. A new, random offset is used for every update.
|
||||
|
||||
The refresh interval is specified in milliseconds and can be set by using the `FRONTEND_API_REFRESH_INTERVAL_MS` environment variable or by using the `frontendApi.refreshIntervalInMs` configuration option in code.
|
Loading…
Reference in New Issue
Block a user