1
0
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:
Fredrik Strand Oseberg 2022-09-28 14:23:41 +02:00 committed by GitHub
parent 0086f2f19f
commit 7fbe227e0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 4 deletions

View File

@ -87,6 +87,9 @@ exports[`should create default config 1`] = `
"isEnabled": [Function],
},
},
"frontendApi": {
"refreshIntervalInMs": 10000,
},
"frontendApiOrigins": [
"*",
],

View File

@ -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,

View File

@ -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() {

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
});

View File

@ -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.

View File

@ -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.