1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: improve frontend config freshness to < 1s (#3749)

This PR reuses the revision Id information from the "optimal 304 for
server SDKs" to improve the freshness of the frontend API config data.

In addition it allows us to reduce the polling (and eventually remove it
when we are confident).

---------

Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
Ivar Conradi Østhus 2023-05-12 19:52:11 +02:00 committed by GitHub
parent 094d59ac4d
commit 6c5df9f2c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 89 additions and 40 deletions

View File

@ -112,7 +112,7 @@ exports[`should create default config 1`] = `
},
},
"frontendApi": {
"refreshIntervalInMs": 10000,
"refreshIntervalInMs": 20000,
},
"frontendApiOrigins": [
"*",

View File

@ -431,7 +431,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
const frontendApi = options.frontendApi || {
refreshIntervalInMs: parseEnvVarNumber(
process.env.FRONTEND_API_REFRESH_INTERVAL_MS,
10000,
20000,
),
};

View File

@ -0,0 +1,46 @@
import { EventEmitter } from 'stream';
import { Logger } from '../../logger';
import { IEventStore, IUnleashConfig, IUnleashStores } from '../../types';
export const UPDATE_REVISION = 'UPDATE_REVISION';
export default class ConfigurationRevisionService extends EventEmitter {
private logger: Logger;
private eventStore: IEventStore;
private revisionId: number;
constructor(
{ eventStore }: Pick<IUnleashStores, 'eventStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
super();
this.logger = getLogger('configuration-revision-service.ts');
this.eventStore = eventStore;
}
async getMaxRevisionId(): Promise<number> {
if (this.revisionId) {
return this.revisionId;
} else {
return this.updateMaxRevisionId();
}
}
async updateMaxRevisionId(): Promise<number> {
const revisionId = await this.eventStore.getMaxRevisionId(
this.revisionId,
);
if (this.revisionId !== revisionId) {
this.logger.debug(
'Updating feature configuration with new revision Id',
revisionId,
);
this.emit(UPDATE_REVISION, revisionId);
this.revisionId = revisionId;
}
return this.revisionId;
}
}

View File

@ -10,8 +10,10 @@ import {
} from '../util/offline-unleash-client';
import { ALL_ENVS, ALL_PROJECTS } from '../util/constants';
import { UnleashEvents } from 'unleash-client';
import { ANY_EVENT } from '../util/anyEventEmitter';
import { Logger } from '../logger';
import ConfigurationRevisionService, {
UPDATE_REVISION,
} from '../features/feature-toggle/configuration-revision-service';
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
@ -19,7 +21,7 @@ type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
type Services = Pick<
IUnleashServices,
'featureToggleServiceV2' | 'segmentService'
'featureToggleServiceV2' | 'segmentService' | 'configurationRevisionService'
>;
export class ProxyRepository
@ -34,6 +36,8 @@ export class ProxyRepository
private readonly services: Services;
private readonly configurationRevisionService: ConfigurationRevisionService;
private readonly token: ApiUser;
private features: FeatureInterface[];
@ -57,6 +61,8 @@ export class ProxyRepository
this.logger = config.getLogger('proxy-repository.ts');
this.stores = stores;
this.services = services;
this.configurationRevisionService =
services.configurationRevisionService;
this.token = token;
this.onAnyEvent = this.onAnyEvent.bind(this);
this.interval = config.frontendApi.refreshIntervalInMs;
@ -67,6 +73,7 @@ export class ProxyRepository
}
getToggle(name: string): FeatureInterface {
//@ts-ignore (we must update the node SDK to allow undefined)
return this.features.find((feature) => feature.name === name);
}
@ -80,14 +87,14 @@ export class ProxyRepository
// Reload cached token data whenever something relevant has changed.
// For now, simply reload all the data on any EventStore event.
this.stores.eventStore.on(ANY_EVENT, this.onAnyEvent);
this.configurationRevisionService.on(UPDATE_REVISION, this.onAnyEvent);
this.emit(UnleashEvents.Ready);
this.emit(UnleashEvents.Changed);
}
stop(): void {
this.stores.eventStore.off(ANY_EVENT, this.onAnyEvent);
this.configurationRevisionService.off(UPDATE_REVISION, this.onAnyEvent);
this.running = false;
}

View File

@ -82,7 +82,7 @@ test('if caching is enabled should memoize', async () => {
const openApiService = { respondWithValidation, validPath };
const featureToggleServiceV2 = { getClientFeatures };
const segmentService = { getActive };
const eventService = { getMaxRevisionId: () => 1 };
const configurationRevisionService = { getMaxRevisionId: () => 1 };
const controller = new FeatureController(
{
@ -94,7 +94,7 @@ test('if caching is enabled should memoize', async () => {
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
eventService,
configurationRevisionService,
},
{
getLogger,
@ -120,7 +120,7 @@ test('if caching is not enabled all calls goes to service', async () => {
const featureToggleServiceV2 = { getClientFeatures };
const segmentService = { getActive };
const openApiService = { respondWithValidation, validPath };
const eventService = { getMaxRevisionId: () => 1 };
const configurationRevisionService = { getMaxRevisionId: () => 1 };
const controller = new FeatureController(
{
@ -132,7 +132,7 @@ test('if caching is not enabled all calls goes to service', async () => {
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
eventService,
configurationRevisionService,
},
{
getLogger,

View File

@ -26,8 +26,8 @@ import {
clientFeaturesSchema,
ClientFeaturesSchema,
} from '../../openapi/spec/client-features-schema';
import { ISegmentService } from 'lib/segments/segment-service-interface';
import { EventService } from 'lib/services';
import { ISegmentService } from '../../segments/segment-service-interface';
import ConfigurationRevisionService from '../../features/feature-toggle/configuration-revision-service';
const version = 2;
@ -53,7 +53,7 @@ export default class FeatureController extends Controller {
private openApiService: OpenApiService;
private eventService: EventService;
private configurationRevisionService: ConfigurationRevisionService;
private featuresAndSegments: (
query: IFeatureToggleQuery,
@ -66,14 +66,14 @@ export default class FeatureController extends Controller {
segmentService,
clientSpecService,
openApiService,
eventService,
configurationRevisionService,
}: Pick<
IUnleashServices,
| 'featureToggleServiceV2'
| 'segmentService'
| 'clientSpecService'
| 'openApiService'
| 'eventService'
| 'configurationRevisionService'
>,
config: IUnleashConfig,
) {
@ -83,7 +83,7 @@ export default class FeatureController extends Controller {
this.segmentService = segmentService;
this.clientSpecService = clientSpecService;
this.openApiService = openApiService;
this.eventService = eventService;
this.configurationRevisionService = configurationRevisionService;
this.logger = config.getLogger('client-api/feature.js');
this.route({
@ -265,7 +265,8 @@ export default class FeatureController extends Controller {
async calculateMeta(query: IFeatureToggleQuery): Promise<IMeta> {
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const revisionId = await this.eventService.getMaxRevisionId();
const revisionId =
await this.configurationRevisionService.getMaxRevisionId();
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const queryHash = hashSum(query);

View File

@ -10,8 +10,6 @@ export default class EventService {
private eventStore: IEventStore;
private revisionId: number;
constructor(
{ eventStore }: Pick<IUnleashStores, 'eventStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
@ -37,21 +35,4 @@ export default class EventService {
totalEvents,
};
}
async getMaxRevisionId(): Promise<number> {
if (this.revisionId) {
return this.revisionId;
} else {
return this.updateMaxRevisionId();
}
}
async updateMaxRevisionId(): Promise<number> {
this.revisionId = await this.eventStore.getMaxRevisionId(
this.revisionId,
);
return this.revisionId;
}
}
module.exports = EventService;

View File

@ -56,6 +56,7 @@ import {
createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService,
} from '../features/change-request-access-service/createChangeRequestAccessReadModel';
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
// TODO: will be moved to scheduler feature directory
export const scheduleServices = (services: IUnleashServices): void => {
@ -66,7 +67,7 @@ export const scheduleServices = (services: IUnleashServices): void => {
clientInstanceService,
projectService,
projectHealthService,
eventService,
configurationRevisionService,
} = services;
schedulerService.schedule(
@ -102,7 +103,9 @@ export const scheduleServices = (services: IUnleashServices): void => {
);
schedulerService.schedule(
eventService.updateMaxRevisionId.bind(eventService),
configurationRevisionService.updateMaxRevisionId.bind(
configurationRevisionService,
),
secondsToMilliseconds(1),
);
};
@ -188,11 +191,18 @@ export const createServices = (
featureToggleServiceV2,
segmentService,
});
const configurationRevisionService = new ConfigurationRevisionService(
stores,
config,
);
const proxyService = new ProxyService(config, stores, {
featureToggleServiceV2,
clientMetricsServiceV2,
segmentService,
settingService,
configurationRevisionService,
});
const edgeService = new EdgeService(stores, config);
@ -264,6 +274,7 @@ export const createServices = (
exportImportService,
transactionalExportImportService,
schedulerService,
configurationRevisionService,
};
};

View File

@ -31,6 +31,7 @@ type Services = Pick<
| 'segmentService'
| 'clientMetricsServiceV2'
| 'settingService'
| 'configurationRevisionService'
>;
export class ProxyService {

View File

@ -40,7 +40,8 @@ import { AccountService } from '../services/account-service';
import { SchedulerService } from '../services/scheduler-service';
import { Knex } from 'knex';
import ExportImportService from '../features/export-import-toggles/export-import-service';
import { ISegmentService } from 'lib/segments/segment-service-interface';
import { ISegmentService } from '../segments/segment-service-interface';
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
export interface IUnleashServices {
accessService: AccessService;
@ -85,6 +86,7 @@ export interface IUnleashServices {
favoritesService: FavoritesService;
maintenanceService: MaintenanceService;
exportImportService: ExportImportService;
configurationRevisionService: ConfigurationRevisionService;
schedulerService: SchedulerService;
transactionalExportImportService: (
db: Knex.Transaction,

View File

@ -142,7 +142,7 @@ test('returns 200 when content updates and hash does not match anymore', async (
},
'test',
);
await app.services.eventService.updateMaxRevisionId();
await app.services.configurationRevisionService.updateMaxRevisionId();
const res = await app.request
.get('/api/client/features')