1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00

chore: remove optimal 304 flag (#3665)

## About the changes
This PR removes the optimal304 flag after being tested in production.
We're keeping the existing configuration that allows users to disable
cache mainly because it's useful for testing.
This commit is contained in:
Gastón Fournier 2023-05-05 09:18:04 +02:00 committed by GitHub
parent 8023fef711
commit 149bc8aab2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 50 additions and 151 deletions

View File

@ -20,7 +20,7 @@ exports[`should create default config 1`] = `
},
"clientFeatureCaching": {
"enabled": true,
"maxAge": 600,
"maxAge": 3600000,
},
"db": {
"acquireConnectionTimeout": 30000,
@ -79,8 +79,6 @@ exports[`should create default config 1`] = `
"maintenanceMode": false,
"messageBanner": false,
"migrationLock": false,
"optimal304": false,
"optimal304Differ": false,
"personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
@ -104,8 +102,6 @@ exports[`should create default config 1`] = `
"maintenanceMode": false,
"messageBanner": false,
"migrationLock": false,
"optimal304": false,
"optimal304Differ": false,
"personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,

View File

@ -378,7 +378,7 @@ test('Supports multiple domains comma separated in environment variables', () =>
test('Should enable client feature caching with .6 seconds max age by default', () => {
const config = createConfig({});
expect(config.clientFeatureCaching.enabled).toBe(true);
expect(config.clientFeatureCaching.maxAge).toBe(600);
expect(config.clientFeatureCaching.maxAge).toBe(3600000);
});
test('Should use overrides from options for client feature caching', () => {

View File

@ -22,7 +22,11 @@ import {
import { getDefaultLogProvider, LogLevel, validateLogProvider } from './logger';
import { defaultCustomAuthDenyAll } from './default-custom-auth-deny-all';
import { formatBaseUri } from './util/format-base-uri';
import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns';
import {
hoursToMilliseconds,
minutesToMilliseconds,
secondsToMilliseconds,
} from 'date-fns';
import EventEmitter from 'events';
import {
ApiTokenType,
@ -72,7 +76,7 @@ function loadExperimental(options: IUnleashOptions): IExperimentalOptions {
const defaultClientCachingOptions: IClientCachingOption = {
enabled: true,
maxAge: 600,
maxAge: hoursToMilliseconds(1),
};
function loadClientCachingOptions(

View File

@ -33,8 +33,8 @@ async function getSetup() {
const callGetAll = async (controller: FeatureController) => {
await controller.getAll(
// @ts-expect-error
{ query: {}, header: () => undefined },
{ json: () => {} },
{ query: {}, header: () => undefined, headers: {} },
{ json: () => {}, setHeader: () => undefined },
);
};
@ -82,16 +82,19 @@ test('if caching is enabled should memoize', async () => {
const openApiService = { respondWithValidation, validPath };
const featureToggleServiceV2 = { getClientFeatures };
const segmentService = { getActive };
const eventService = { getMaxRevisionId: () => 1 };
const controller = new FeatureController(
{
clientSpecService,
// @ts-expect-error
// @ts-expect-error due to partial implementation
openApiService,
// @ts-expect-error
// @ts-expect-error due to partial implementation
featureToggleServiceV2,
// @ts-expect-error
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
eventService,
},
{
getLogger,
@ -117,16 +120,19 @@ 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 controller = new FeatureController(
{
clientSpecService,
// @ts-expect-error
// @ts-expect-error due to partial implementation
openApiService,
// @ts-expect-error
// @ts-expect-error due to partial implementation
featureToggleServiceV2,
// @ts-expect-error
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
eventService,
},
{
getLogger,

View File

@ -2,10 +2,8 @@ import memoizee from 'memoizee';
import { Response } from 'express';
// eslint-disable-next-line import/no-extraneous-dependencies
import hashSum from 'hash-sum';
// eslint-disable-next-line import/no-extraneous-dependencies
import { diff } from 'deep-object-diff';
import Controller from '../controller';
import { IFlagResolver, IUnleashConfig, IUnleashServices } from '../../types';
import { IUnleashConfig, IUnleashServices } from '../../types';
import FeatureToggleService from '../../services/feature-toggle-service';
import { Logger } from '../../logger';
import { querySchema } from '../../schema/feature-schema';
@ -30,9 +28,6 @@ import {
} from '../../openapi/spec/client-features-schema';
import { ISegmentService } from 'lib/segments/segment-service-interface';
import { EventService } from 'lib/services';
import { hoursToMilliseconds } from 'date-fns';
import { isEmpty } from '../../util/isEmpty';
import EventEmitter from 'events';
const version = 2;
@ -50,8 +45,6 @@ interface IMeta {
export default class FeatureController extends Controller {
private readonly logger: Logger;
private eventBus: EventEmitter;
private featureToggleServiceV2: FeatureToggleService;
private segmentService: ISegmentService;
@ -62,13 +55,10 @@ export default class FeatureController extends Controller {
private eventService: EventService;
private readonly cache: boolean;
private cachedFeatures: any;
private cachedFeatures2: any;
private flagResolver: IFlagResolver;
private featuresAndSegments: (
query: IFeatureToggleQuery,
etag: string,
) => Promise<[FeatureConfigurationClient[], ISegment[]]>;
constructor(
{
@ -88,15 +78,13 @@ export default class FeatureController extends Controller {
config: IUnleashConfig,
) {
super(config);
const { clientFeatureCaching, flagResolver, eventBus } = config;
const { clientFeatureCaching } = config;
this.featureToggleServiceV2 = featureToggleServiceV2;
this.segmentService = segmentService;
this.clientSpecService = clientSpecService;
this.openApiService = openApiService;
this.flagResolver = flagResolver;
this.eventService = eventService;
this.logger = config.getLogger('client-api/feature.js');
this.eventBus = eventBus;
this.route({
method: 'get',
@ -130,39 +118,28 @@ export default class FeatureController extends Controller {
],
});
if (clientFeatureCaching?.enabled) {
this.cache = true;
this.cachedFeatures = memoizee(
(query) => this.resolveFeaturesAndSegments(query),
if (clientFeatureCaching.enabled) {
this.featuresAndSegments = memoizee(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(query: IFeatureToggleQuery, etag: string) =>
this.resolveFeaturesAndSegments(query),
{
promise: true,
maxAge: clientFeatureCaching.maxAge,
normalizer(args) {
// args is arguments object as accessible in memoized function
return JSON.stringify(args[0]);
return args[1];
},
},
);
} else {
this.featuresAndSegments = this.resolveFeaturesAndSegments;
}
this.cachedFeatures2 = memoizee(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(query: IFeatureToggleQuery, etag: string) =>
this.resolveFeaturesAndSegments(query),
{
promise: true,
maxAge: hoursToMilliseconds(1),
normalizer(args) {
// args is arguments object as accessible in memoized function
return args[1];
},
},
);
}
private async resolveFeaturesAndSegments(
query?: IFeatureToggleQuery,
): Promise<[FeatureConfigurationClient[], ISegment[]]> {
this.logger.debug('bypass cache');
return Promise.all([
this.featureToggleServiceV2.getClientFeatures(query),
this.segmentService.getActive(),
@ -239,93 +216,6 @@ export default class FeatureController extends Controller {
async getAll(
req: IAuthRequest,
res: Response<ClientFeaturesSchema>,
): Promise<void> {
if (this.flagResolver.isEnabled('optimal304')) {
return this.optimal304(req, res);
}
const query = await this.resolveQuery(req);
const [features, segments] = this.cache
? await this.cachedFeatures(query)
: await this.resolveFeaturesAndSegments(query);
if (this.flagResolver.isEnabled('optimal304Differ')) {
process.nextTick(async () =>
this.doOptimal304Diffing(features, query),
);
}
if (this.clientSpecService.requestSupportsSpec(req, 'segments')) {
this.openApiService.respondWithValidation(
200,
res,
clientFeaturesSchema.$id,
{ version, features, query: { ...query }, segments },
);
} else {
this.openApiService.respondWithValidation(
200,
res,
clientFeaturesSchema.$id,
{ version, features, query },
);
}
}
/**
* This helper method is used to validate that the new way of calculating
* cache-key based on query hash and revision id, with an internal memoization
* of 1hr still ends up producing the same result.
*
* It's worth to note that it is expected that a diff will occur immediately after
* a toggle changes due to the nature of two individual caches and how fast they
* detect the change. The diffs should however go away as soon as they both have
* the latest feature toggle configuration, which will happen within 600ms on the
* default configurations.
*
* This method is experimental and will only be used to validate our internal state
* to make sure our new way of caching is correct and stable.
*
* @deprecated
*/
async doOptimal304Diffing(
features: FeatureConfigurationClient[],
query: IFeatureToggleQuery,
): Promise<void> {
try {
const { etag } = await this.calculateMeta(query);
const [featuresNew] = await this.cachedFeatures2(query, etag);
const theDiffedObject = diff(features, featuresNew);
if (isEmpty(theDiffedObject)) {
this.logger.warn('The diff is: <Empty>');
this.eventBus.emit('optimal304Differ', { status: 'equal' });
} else {
this.logger.warn(
`The diff is: ${JSON.stringify(theDiffedObject)}`,
);
this.eventBus.emit('optimal304Differ', { status: 'diff' });
}
} catch (e) {
this.logger.error('The diff checker crashed', e);
this.eventBus.emit('optimal304Differ', { status: 'crash' });
}
}
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();
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const queryHash = hashSum(query);
const etag = `"${queryHash}:${revisionId}"`;
return { revisionId, etag, queryHash };
}
async optimal304(
req: IAuthRequest,
res: Response<ClientFeaturesSchema>,
): Promise<void> {
const query = await this.resolveQuery(req);
@ -345,7 +235,10 @@ export default class FeatureController extends Controller {
);
}
const [features, segments] = await this.cachedFeatures2(query, etag);
const [features, segments] = await this.featuresAndSegments(
query,
etag,
);
if (this.clientSpecService.requestSupportsSpec(req, 'segments')) {
this.openApiService.respondWithValidation(
@ -370,6 +263,16 @@ 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();
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const queryHash = hashSum(query);
const etag = `"${queryHash}:${revisionId}"`;
return { revisionId, etag, queryHash };
}
async getFeatureToggle(
req: IAuthRequest<{ featureName: string }, ClientFeaturesQuerySchema>,
res: Response<ClientFeatureSchema>,

View File

@ -50,14 +50,6 @@ const flags = {
false,
),
cleanClientApi: parseEnvVarBoolean(process.env.CLEAN_CLIENT_API, false),
optimal304: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304,
false,
),
optimal304Differ: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304_DIFFER,
false,
),
groupRootRoles: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ROOT_ROLE_GROUPS,
false,

View File

@ -38,8 +38,6 @@ process.nextTick(async () => {
embedProxyFrontend: true,
anonymiseEventLog: false,
responseTimeWithAppNameKillSwitch: false,
optimal304: true,
optimal304Differ: false,
},
},
authentication: {