From 6569d72d792c0885a02881504f97d2f906eaf5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Tue, 19 Aug 2025 15:40:04 +0200 Subject: [PATCH] chore: support different etags per environment --- .../client-feature-toggle.controller.ts | 4 +- src/lib/features/events/event-store.ts | 72 +++++++++---------- .../configuration-revision-service.ts | 26 ++++++- src/lib/types/stores/event-store.ts | 5 +- 4 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts b/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts index 03e105bf70..75f2a443ce 100644 --- a/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts +++ b/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts @@ -359,7 +359,9 @@ export default class FeatureController extends Controller { async calculateMeta(query: IFeatureToggleQuery): Promise { // TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?). const revisionId = - await this.configurationRevisionService.getMaxRevisionId(); + await this.configurationRevisionService.getMaxRevisionId( + environment, + ); // TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?). const queryHash = hashSum(query); diff --git a/src/lib/features/events/event-store.ts b/src/lib/features/events/event-store.ts index ede6faac4c..ed49723d70 100644 --- a/src/lib/features/events/event-store.ts +++ b/src/lib/features/events/event-store.ts @@ -191,29 +191,37 @@ export class EventStore implements IEventStore { } } - async getMaxRevisionId(largerThan: number = 0): Promise { - const stopTimer = this.metricTimer('getMaxRevisionId'); - const row = await this.db(TABLE) - .max('id') - .where((builder) => - builder - .andWhere((inner) => - inner - .whereNotNull('feature_name') - .whereNotIn('type', [ - FEATURE_CREATED, - FEATURE_TAGGED, - ]) - .whereNot('type', 'LIKE', 'change-%'), - ) - .orWhereIn('type', [ - SEGMENT_UPDATED, - FEATURE_IMPORT, - FEATURES_IMPORTED, - ]), + private typeIsInteresting = (builder: Knex.QueryBuilder) => + builder + .andWhere((inner) => + inner + .whereNotNull('feature_name') + .whereNotIn('type', [FEATURE_CREATED, FEATURE_TAGGED]) + .whereNot('type', 'LIKE', 'change-%'), ) - .andWhere('id', '>=', largerThan) - .first(); + .orWhereIn('type', [ + SEGMENT_UPDATED, + FEATURE_IMPORT, + FEATURES_IMPORTED, + SEGMENT_CREATED, + SEGMENT_DELETED, + ]); + + async getMaxRevisionId( + largerThan: number = 0, + environment?: string, + ): Promise { + const stopTimer = this.metricTimer('getMaxRevisionId'); + const query = this.db(TABLE) + .max('id') + .where(this.typeIsInteresting) + .andWhere('id', '>=', largerThan); + + if (environment) { + query.where('environment', environment); + } + + const row = await query.first(); stopTimer(); return row?.max ?? 0; } @@ -225,27 +233,11 @@ export class EventStore implements IEventStore { .from(TABLE) .where('id', '>', start) .andWhere('id', '<=', end) - .andWhere((builder) => - builder - .andWhere((inner) => - inner - .whereNotNull('feature_name') - .whereNotIn('type', [ - FEATURE_CREATED, - FEATURE_TAGGED, - ]), - ) - .orWhereIn('type', [ - SEGMENT_UPDATED, - FEATURE_IMPORT, - FEATURES_IMPORTED, - SEGMENT_CREATED, - SEGMENT_DELETED, - ]), - ) + .andWhere(this.typeIsInteresting) .orderBy('id', 'asc'); const rows = await query; + stopTimer(); return rows.map(this.rowToEvent); } diff --git a/src/lib/features/feature-toggle/configuration-revision-service.ts b/src/lib/features/feature-toggle/configuration-revision-service.ts index 6906d58229..7409101006 100644 --- a/src/lib/features/feature-toggle/configuration-revision-service.ts +++ b/src/lib/features/feature-toggle/configuration-revision-service.ts @@ -18,6 +18,8 @@ export default class ConfigurationRevisionService extends EventEmitter { private revisionId: number; + private maxRevisionId: Record = {}; + private flagResolver: IFlagResolver; private constructor( @@ -51,7 +53,17 @@ export default class ConfigurationRevisionService extends EventEmitter { return ConfigurationRevisionService.instance; } - async getMaxRevisionId(): Promise { + async getMaxRevisionId(environment?: string): Promise { + if (environment && !this.maxRevisionId[environment]) { + await this.updateMaxEnvironmentRevisionId(environment); + } + if ( + environment && + this.maxRevisionId[environment] && + this.maxRevisionId[environment] > 0 + ) { + return this.maxRevisionId[environment]; + } if (this.revisionId > 0) { return this.revisionId; } else { @@ -59,6 +71,18 @@ export default class ConfigurationRevisionService extends EventEmitter { } } + async updateMaxEnvironmentRevisionId(environment: string): Promise { + const envRevisionId = await this.eventStore.getMaxRevisionId( + this.revisionId, + environment, + ); + if (this.maxRevisionId[environment] < envRevisionId) { + this.maxRevisionId[environment] = envRevisionId; + } + + return this.maxRevisionId[environment]; + } + async updateMaxRevisionId(emit: boolean = true): Promise { if (this.flagResolver.isEnabled('disableUpdateMaxRevisionId')) { return 0; diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index d35f30809e..c28fe647cd 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -38,7 +38,10 @@ export interface IEventStore queryParams: IQueryParam[], options?: { withIp?: boolean }, ): Promise; - getMaxRevisionId(currentMax?: number): Promise; + getMaxRevisionId( + currentMax?: number, + environment?: string, + ): Promise; getRevisionRange(start: number, end: number): Promise; query(operations: IQueryOperations[]): Promise; queryCount(operations: IQueryOperations[]): Promise;