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:
parent
b211c9c33f
commit
a257ca4474
@ -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;
|
||||||
|
@ -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',
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user