1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-12 13:48:35 +02:00

feat: deleted feature names should come from event (#8978)

This is still raw and experimental.
We started to pull deleted features from event payload.
Now we put full query towards read model.

Co-Author:  @FredrikOseberg
This commit is contained in:
Jaanus Sellin 2024-12-13 14:54:15 +02:00 committed by GitHub
parent b211c9c33f
commit a257ca4474
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 54 deletions

View File

@ -1,24 +1,26 @@
import type { import type {
IEventStore, IEventStore,
IFeatureToggleClient, IFeatureToggleCacheQuery,
IFeatureToggleQuery, IFeatureToggleQuery,
IFlagResolver, IFlagResolver,
} from '../../../types'; } from '../../../types';
import type { FeatureConfigurationClient } from '../../feature-toggle/types/feature-toggle-strategies-store-type';
import type ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service'; import type ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service';
import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service'; import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service';
import { RevisionCache } from './revision-cache'; import { RevisionCache } from './revision-cache';
import type { IClientFeatureToggleCacheReadModel } from './client-feature-toggle-cache-read-model-type'; import type {
FeatureConfigurationCacheClient,
IClientFeatureToggleCacheReadModel,
} from './client-feature-toggle-cache-read-model-type';
type DeletedFeature = { type DeletedFeature = {
name: string; name: string;
project: string; project: string;
}; };
export type ClientFeatureChange = { export type RevisionCacheEntry = {
updated: IFeatureToggleClient[]; updated: FeatureConfigurationCacheClient[];
removed: DeletedFeature[];
revisionId: number; revisionId: number;
removed: DeletedFeature[];
}; };
export type Revision = { export type Revision = {
@ -132,9 +134,11 @@ export class ClientFeatureToggleCache {
async getDelta( async getDelta(
sdkRevisionId: number | undefined, sdkRevisionId: number | undefined,
environment: string, query: IFeatureToggleQuery,
projects: string[], ): Promise<RevisionCacheEntry | undefined> {
): Promise<ClientFeatureChange | undefined> { const projects = query.project ? query.project : ['*'];
const environment = query.environment ? query.environment : 'default';
// TODO: filter by tags, what is namePrefix? anything else?
const requiredRevisionId = sdkRevisionId || 0; const requiredRevisionId = sdkRevisionId || 0;
const hasCache = this.cache[environment] !== undefined; const hasCache = this.cache[environment] !== undefined;
@ -153,6 +157,7 @@ export class ClientFeatureToggleCache {
sdkRevisionId !== this.currentRevisionId && sdkRevisionId !== this.currentRevisionId &&
!this.cache[environment].hasRevision(sdkRevisionId)) !this.cache[environment].hasRevision(sdkRevisionId))
) { ) {
//TODO: populate cache based on this?
return { return {
revisionId: this.currentRevisionId, revisionId: this.currentRevisionId,
// @ts-ignore // @ts-ignore
@ -201,44 +206,40 @@ export class ClientFeatureToggleCache {
.map((event) => event.featureName!), .map((event) => event.featureName!),
), ),
]; ];
const newToggles = await this.getChangedToggles(
changedToggles,
latestRevision, // TODO: this should come back from the same query to not be out of sync
);
// TODO: Discussion point. Should we filter events by environment and only add revisions in the cache const removed = changeEvents
// for the environment that changed? If we do that we can also save CPU and memory, because we don't need .filter((event) => event.featureName && event.project)
// to talk to the database if the cache is not initialized for that environment .filter((event) => event.type === 'feature-deleted')
for (const key of keys) { .map((event) => ({
this.cache[key].addRevision(newToggles); name: event.featureName!,
project: event.project!,
}));
// TODO: we might want to only update the environments that had events changed for performance
for (const environment of keys) {
const newToggles = await this.getChangedToggles(
environment,
changedToggles,
);
this.cache[environment].addRevision({
updated: newToggles,
revisionId: latestRevision,
removed,
});
} }
this.currentRevisionId = latestRevision; this.currentRevisionId = latestRevision;
} }
async getChangedToggles( async getChangedToggles(
environment: string,
toggles: string[], toggles: string[],
revisionId: number, ): Promise<FeatureConfigurationCacheClient[]> {
): Promise<ClientFeatureChange> {
const foundToggles = await this.getClientFeatures({ const foundToggles = await this.getClientFeatures({
// @ts-ignore removed toggleNames from the type, we should not use this method at all,
toggleNames: toggles, toggleNames: toggles,
environment: 'development', environment,
}); });
return foundToggles;
const foundToggleNames = foundToggles.map((toggle) => toggle.name);
const removed = toggles
.filter((toggle) => !foundToggleNames.includes(toggle))
.map((name) => ({
name,
project: 'default', // TODO: this needs to be smart and figure out the project . IMPORTANT
}));
return {
updated: foundToggles as any, // impressionData is not on the type but should be
removed,
revisionId,
};
} }
public async initEnvironmentCache(environment: string) { public async initEnvironmentCache(environment: string) {
@ -262,8 +263,8 @@ export class ClientFeatureToggleCache {
} }
async getClientFeatures( async getClientFeatures(
query: IFeatureToggleQuery, query: IFeatureToggleCacheQuery,
): Promise<FeatureConfigurationClient[]> { ): Promise<FeatureConfigurationCacheClient[]> {
const result = const result =
await this.clientFeatureToggleCacheReadModel.getAll(query); await this.clientFeatureToggleCacheReadModel.getAll(query);
return result; return result;

View File

@ -10,7 +10,7 @@ import type { Logger } from '../../logger';
import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import type { import type {
ClientFeatureChange, RevisionCacheEntry,
ClientFeatureToggleCache, ClientFeatureToggleCache,
} from './cache/client-feature-toggle-cache'; } from './cache/client-feature-toggle-cache';
@ -43,15 +43,10 @@ export class ClientFeatureToggleService {
async getClientDelta( async getClientDelta(
revisionId: number | undefined, revisionId: number | undefined,
projects: string[], query: IFeatureToggleQuery,
environment: string, ): Promise<RevisionCacheEntry | undefined> {
): Promise<ClientFeatureChange | undefined> {
if (this.clientFeatureToggleCache !== null) { if (this.clientFeatureToggleCache !== null) {
return this.clientFeatureToggleCache.getDelta( return this.clientFeatureToggleCache.getDelta(revisionId, query);
revisionId,
environment,
projects,
);
} else { } else {
throw new Error( throw new Error(
'Calling the partial updates but the cache is not initialized', 'Calling the partial updates but the cache is not initialized',

View File

@ -33,7 +33,7 @@ import {
} from '../../openapi/spec/client-features-schema'; } from '../../openapi/spec/client-features-schema';
import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
import type { ClientFeatureToggleService } from './client-feature-toggle-service'; import type { ClientFeatureToggleService } from './client-feature-toggle-service';
import type { ClientFeatureChange } from './cache/client-feature-toggle-cache'; import type { RevisionCacheEntry } from './cache/client-feature-toggle-cache';
const version = 2; const version = 2;
@ -300,24 +300,20 @@ export default class FeatureController extends Controller {
async getDelta( async getDelta(
req: IAuthRequest, req: IAuthRequest,
res: Response<ClientFeatureChange>, res: Response<RevisionCacheEntry>,
): Promise<void> { ): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) { if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError(); throw new NotFoundError();
} }
const query = await this.resolveQuery(req); const query = await this.resolveQuery(req);
const etag = req.headers['if-none-match']; const etag = req.headers['if-none-match'];
const meta = await this.calculateMeta(query);
const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined; const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;
const projects = query.project ? query.project : ['*'];
const environment = query.environment ? query.environment : 'default';
const changedFeatures = const changedFeatures =
await this.clientFeatureToggleService.getClientDelta( await this.clientFeatureToggleService.getClientDelta(
currentSdkRevisionId, currentSdkRevisionId,
projects, query,
environment,
); );
if (!changedFeatures) { if (!changedFeatures) {