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:
parent
094d59ac4d
commit
6c5df9f2c7
@ -112,7 +112,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"frontendApi": {
|
||||
"refreshIntervalInMs": 10000,
|
||||
"refreshIntervalInMs": 20000,
|
||||
},
|
||||
"frontendApiOrigins": [
|
||||
"*",
|
||||
|
@ -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,
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,7 @@ type Services = Pick<
|
||||
| 'segmentService'
|
||||
| 'clientMetricsServiceV2'
|
||||
| 'settingService'
|
||||
| 'configurationRevisionService'
|
||||
>;
|
||||
|
||||
export class ProxyService {
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user