mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: stabilize global frontend api cache (#6466)
This commit is contained in:
		
							parent
							
								
									97a81162ac
								
							
						
					
					
						commit
						8f2631e418
					
				| @ -2,8 +2,10 @@ import { IClientSegment, IFeatureStrategySegment, ISegment } from '../../types'; | ||||
| import { ISegmentReadModel } from './segment-read-model-type'; | ||||
| 
 | ||||
| export class FakeSegmentReadModel implements ISegmentReadModel { | ||||
|     constructor(private segments: ISegment[] = []) {} | ||||
| 
 | ||||
|     async getAll(): Promise<ISegment[]> { | ||||
|         return []; | ||||
|         return this.segments; | ||||
|     } | ||||
| 
 | ||||
|     async getAllFeatureStrategySegments(): Promise<IFeatureStrategySegment[]> { | ||||
| @ -11,7 +13,7 @@ export class FakeSegmentReadModel implements ISegmentReadModel { | ||||
|     } | ||||
| 
 | ||||
|     async getActive(): Promise<ISegment[]> { | ||||
|         return []; | ||||
|         return this.segments; | ||||
|     } | ||||
| 
 | ||||
|     async getActiveForClient(): Promise<IClientSegment[]> { | ||||
|  | ||||
							
								
								
									
										5
									
								
								src/lib/proxy/client-feature-toggle-read-model-type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/lib/proxy/client-feature-toggle-read-model-type.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| import { IFeatureToggleClient } from '../types'; | ||||
| 
 | ||||
| export interface IClientFeatureToggleReadModel { | ||||
|     getClient(): Promise<Record<string, IFeatureToggleClient[]>>; | ||||
| } | ||||
| @ -13,6 +13,7 @@ import Raw = Knex.Raw; | ||||
| import metricsHelper from '../util/metrics-helper'; | ||||
| import { DB_TIME } from '../metric-events'; | ||||
| import EventEmitter from 'events'; | ||||
| import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type'; | ||||
| 
 | ||||
| export interface IGetAllFeatures { | ||||
|     featureQuery?: IFeatureToggleQuery; | ||||
| @ -20,7 +21,9 @@ export interface IGetAllFeatures { | ||||
|     userId?: number; | ||||
| } | ||||
| 
 | ||||
| export default class ClientFeatureToggleReadModel { | ||||
| export default class ClientFeatureToggleReadModel | ||||
|     implements IClientFeatureToggleReadModel | ||||
| { | ||||
|     private db: Db; | ||||
| 
 | ||||
|     private timer: Function; | ||||
|  | ||||
							
								
								
									
										16
									
								
								src/lib/proxy/fake-client-feature-toggle-read-model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/proxy/fake-client-feature-toggle-read-model.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| import { IFeatureToggleClient } from '../types'; | ||||
| import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type'; | ||||
| 
 | ||||
| export default class FakeClientFeatureToggleReadModel | ||||
|     implements IClientFeatureToggleReadModel | ||||
| { | ||||
|     constructor(private value: Record<string, IFeatureToggleClient[]>) {} | ||||
| 
 | ||||
|     getClient(): Promise<Record<string, IFeatureToggleClient[]>> { | ||||
|         return Promise.resolve(this.value); | ||||
|     } | ||||
| 
 | ||||
|     setValue(value: Record<string, IFeatureToggleClient[]>) { | ||||
|         this.value = value; | ||||
|     } | ||||
| } | ||||
| @ -9,7 +9,7 @@ import { IApiUser } from '../types/api-user'; | ||||
| import { IUnleashConfig } from '../types'; | ||||
| import { UnleashEvents } from 'unleash-client'; | ||||
| import { Logger } from '../logger'; | ||||
| import { GlobalFrontendApiRepository } from './global-frontend-api-repository'; | ||||
| import { GlobalFrontendApiCache } from './global-frontend-api-cache'; | ||||
| 
 | ||||
| type Config = Pick<IUnleashConfig, 'getLogger'>; | ||||
| 
 | ||||
| @ -23,13 +23,13 @@ export class FrontendApiRepository | ||||
| 
 | ||||
|     private readonly token: IApiUser; | ||||
| 
 | ||||
|     private globalFrontendApiRepository: GlobalFrontendApiRepository; | ||||
|     private globalFrontendApiRepository: GlobalFrontendApiCache; | ||||
| 
 | ||||
|     private running: boolean; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: Config, | ||||
|         globalFrontendApiRepository: GlobalFrontendApiRepository, | ||||
|         globalFrontendApiRepository: GlobalFrontendApiCache, | ||||
|         token: IApiUser, | ||||
|     ) { | ||||
|         super(); | ||||
|  | ||||
							
								
								
									
										169
									
								
								src/lib/proxy/global-frontend-api-cache.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/lib/proxy/global-frontend-api-cache.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | ||||
| import { | ||||
|     GlobalFrontendApiCache, | ||||
|     GlobalFrontendApiCacheState, | ||||
| } from './global-frontend-api-cache'; | ||||
| import noLogger from '../../test/fixtures/no-logger'; | ||||
| import { FakeSegmentReadModel } from '../features/segment/fake-segment-read-model'; | ||||
| import FakeClientFeatureToggleReadModel from './fake-client-feature-toggle-read-model'; | ||||
| import EventEmitter from 'events'; | ||||
| import { IApiUser, IFeatureToggleClient, ISegment } from '../types'; | ||||
| import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revision-service'; | ||||
| 
 | ||||
| const state = async ( | ||||
|     cache: GlobalFrontendApiCache, | ||||
|     state: GlobalFrontendApiCacheState, | ||||
| ) => { | ||||
|     await new Promise((resolve) => { | ||||
|         cache.on(state, () => { | ||||
|             resolve('done'); | ||||
|         }); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| const defaultFeature: IFeatureToggleClient = { | ||||
|     name: 'featureA', | ||||
|     enabled: true, | ||||
|     strategies: [], | ||||
|     variants: [], | ||||
|     project: 'projectA', | ||||
|     dependencies: [], | ||||
|     type: 'release', | ||||
|     stale: false, | ||||
|     description: '', | ||||
| }; | ||||
| const defaultSegment = { name: 'segment', id: 1 } as ISegment; | ||||
| 
 | ||||
| const createCache = ( | ||||
|     segment: ISegment = defaultSegment, | ||||
|     features: Record<string, IFeatureToggleClient[]> = {}, | ||||
| ) => { | ||||
|     const config = { getLogger: noLogger }; | ||||
|     const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]); | ||||
|     const clientFeatureToggleReadModel = new FakeClientFeatureToggleReadModel( | ||||
|         features, | ||||
|     ); | ||||
|     const configurationRevisionService = new EventEmitter(); | ||||
|     const cache = new GlobalFrontendApiCache( | ||||
|         config, | ||||
|         segmentReadModel, | ||||
|         clientFeatureToggleReadModel, | ||||
|         configurationRevisionService, | ||||
|     ); | ||||
| 
 | ||||
|     return { | ||||
|         cache, | ||||
|         configurationRevisionService, | ||||
|         clientFeatureToggleReadModel, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| test('Can read initial segment', async () => { | ||||
|     const { cache } = createCache({ name: 'segment', id: 1 } as ISegment); | ||||
| 
 | ||||
|     const segmentBeforeRead = cache.getSegment(1); | ||||
|     expect(segmentBeforeRead).toEqual(undefined); | ||||
| 
 | ||||
|     await state(cache, 'ready'); | ||||
| 
 | ||||
|     const segment = cache.getSegment(1); | ||||
|     expect(segment).toEqual({ name: 'segment', id: 1 }); | ||||
| }); | ||||
| 
 | ||||
| test('Can read initial features', async () => { | ||||
|     const { cache } = createCache(defaultSegment, { | ||||
|         development: [ | ||||
|             { | ||||
|                 ...defaultFeature, | ||||
|                 name: 'featureA', | ||||
|                 enabled: true, | ||||
|                 project: 'projectA', | ||||
|             }, | ||||
|             { | ||||
|                 ...defaultFeature, | ||||
|                 name: 'featureB', | ||||
|                 enabled: true, | ||||
|                 project: 'projectB', | ||||
|             }, | ||||
|         ], | ||||
|         production: [ | ||||
|             { | ||||
|                 ...defaultFeature, | ||||
|                 name: 'featureA', | ||||
|                 enabled: false, | ||||
|                 project: 'projectA', | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| 
 | ||||
|     const featuresBeforeRead = cache.getToggles({ | ||||
|         environment: 'development', | ||||
|         projects: ['projectA'], | ||||
|     } as IApiUser); | ||||
|     expect(featuresBeforeRead).toEqual([]); | ||||
| 
 | ||||
|     await state(cache, 'ready'); | ||||
| 
 | ||||
|     const features = cache.getToggles({ | ||||
|         environment: 'development', | ||||
|         projects: ['projectA'], | ||||
|     } as IApiUser); | ||||
|     expect(features).toEqual([ | ||||
|         { | ||||
|             ...defaultFeature, | ||||
|             name: 'featureA', | ||||
|             enabled: true, | ||||
|             impressionData: false, | ||||
|         }, | ||||
|     ]); | ||||
| 
 | ||||
|     const allProjectFeatures = cache.getToggles({ | ||||
|         environment: 'development', | ||||
|         projects: ['*'], | ||||
|     } as IApiUser); | ||||
|     expect(allProjectFeatures.length).toBe(2); | ||||
| 
 | ||||
|     const defaultProjectFeatures = cache.getToggles({ | ||||
|         environment: '*', | ||||
|         projects: ['*'], | ||||
|     } as IApiUser); | ||||
|     expect(defaultProjectFeatures.length).toBe(0); | ||||
| }); | ||||
| 
 | ||||
| test('Can refresh data on revision update', async () => { | ||||
|     const { | ||||
|         cache, | ||||
|         configurationRevisionService, | ||||
|         clientFeatureToggleReadModel, | ||||
|     } = createCache(); | ||||
| 
 | ||||
|     await state(cache, 'ready'); | ||||
| 
 | ||||
|     clientFeatureToggleReadModel.setValue({ | ||||
|         development: [ | ||||
|             { | ||||
|                 ...defaultFeature, | ||||
|                 name: 'featureA', | ||||
|                 enabled: false, | ||||
|                 strategies: [{ name: 'default' }], | ||||
|                 project: 'projectA', | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
|     configurationRevisionService.emit(UPDATE_REVISION); | ||||
| 
 | ||||
|     await state(cache, 'updated'); | ||||
| 
 | ||||
|     const features = cache.getToggles({ | ||||
|         environment: 'development', | ||||
|         projects: ['projectA'], | ||||
|     } as IApiUser); | ||||
|     expect(features).toMatchObject([ | ||||
|         { | ||||
|             ...defaultFeature, | ||||
|             name: 'featureA', | ||||
|             enabled: false, | ||||
|             strategies: [{ name: 'default' }], | ||||
|             impressionData: false, | ||||
|         }, | ||||
|     ]); | ||||
| }); | ||||
| @ -9,38 +9,36 @@ import { | ||||
| } from '../features/playground/offline-unleash-client'; | ||||
| import { ALL_ENVS } from '../util/constants'; | ||||
| import { Logger } from '../logger'; | ||||
| import ConfigurationRevisionService, { | ||||
|     UPDATE_REVISION, | ||||
| } from '../features/feature-toggle/configuration-revision-service'; | ||||
| import ClientFeatureToggleReadModel from './client-feature-toggle-read-model'; | ||||
| import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revision-service'; | ||||
| import { mapValues } from '../util'; | ||||
| import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type'; | ||||
| 
 | ||||
| type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi' | 'eventBus'>; | ||||
| type Config = Pick<IUnleashConfig, 'getLogger'>; | ||||
| 
 | ||||
| export class GlobalFrontendApiRepository extends EventEmitter { | ||||
| export type GlobalFrontendApiCacheState = 'starting' | 'ready' | 'updated'; | ||||
| 
 | ||||
| export class GlobalFrontendApiCache extends EventEmitter { | ||||
|     private readonly config: Config; | ||||
| 
 | ||||
|     private readonly logger: Logger; | ||||
| 
 | ||||
|     private readonly clientFeatureToggleReadModel: ClientFeatureToggleReadModel; | ||||
|     private readonly clientFeatureToggleReadModel: IClientFeatureToggleReadModel; | ||||
| 
 | ||||
|     private readonly segmentReadModel: ISegmentReadModel; | ||||
| 
 | ||||
|     private readonly configurationRevisionService: ConfigurationRevisionService; | ||||
|     private readonly configurationRevisionService: EventEmitter; | ||||
| 
 | ||||
|     private featuresByEnvironment: Record<string, FeatureInterface[]>; | ||||
|     private featuresByEnvironment: Record<string, FeatureInterface[]> = {}; | ||||
| 
 | ||||
|     private segments: Segment[]; | ||||
|     private segments: Segment[] = []; | ||||
| 
 | ||||
|     private interval: number; | ||||
| 
 | ||||
|     private running: boolean; | ||||
|     private status: GlobalFrontendApiCacheState = 'starting'; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: Config, | ||||
|         segmentReadModel: ISegmentReadModel, | ||||
|         clientFeatureToggleReadModel: ClientFeatureToggleReadModel, | ||||
|         configurationRevisionService: ConfigurationRevisionService, | ||||
|         clientFeatureToggleReadModel: IClientFeatureToggleReadModel, | ||||
|         configurationRevisionService: EventEmitter, | ||||
|     ) { | ||||
|         super(); | ||||
|         this.config = config; | ||||
| @ -49,7 +47,6 @@ export class GlobalFrontendApiRepository extends EventEmitter { | ||||
|         this.configurationRevisionService = configurationRevisionService; | ||||
|         this.segmentReadModel = segmentReadModel; | ||||
|         this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this); | ||||
|         this.interval = config.frontendApi.refreshIntervalInMs; | ||||
|         this.refreshData(); | ||||
|         this.configurationRevisionService.on( | ||||
|             UPDATE_REVISION, | ||||
| @ -62,6 +59,11 @@ export class GlobalFrontendApiRepository extends EventEmitter { | ||||
|     } | ||||
| 
 | ||||
|     getToggles(token: IApiUser): FeatureInterface[] { | ||||
|         if ( | ||||
|             this.featuresByEnvironment[this.environmentNameForToken(token)] == | ||||
|             null | ||||
|         ) | ||||
|             return []; | ||||
|         return this.featuresByEnvironment[ | ||||
|             this.environmentNameForToken(token) | ||||
|         ].filter( | ||||
| @ -88,6 +90,13 @@ export class GlobalFrontendApiRepository extends EventEmitter { | ||||
|         try { | ||||
|             this.featuresByEnvironment = await this.getAllFeatures(); | ||||
|             this.segments = await this.getAllSegments(); | ||||
|             if (this.status === 'starting') { | ||||
|                 this.status = 'ready'; | ||||
|                 this.emit('ready'); | ||||
|             } else if (this.status === 'ready' || this.status === 'updated') { | ||||
|                 this.status = 'updated'; | ||||
|                 this.emit('updated'); | ||||
|             } | ||||
|         } catch (e) { | ||||
|             this.logger.error('Cannot load data for token', e); | ||||
|         } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user