mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
do etag calculation when memoizing feature toggles
To prevent express from having to do etag calculation of all feature toggle responses even if hitting a memoized feature toggle this PR adds etag calculation to the memoized function. This will cost us an extra etag calculation for client SDK requests without ETag headers, but for SDK requests with ETag headers, it should reduce the number of etag calculations we do down to 1 per 0.6 seconds per query Co-authored-by: Gard Rimestad <gard@getunleash.io> Co-authored-by: Nuno Gois <nuno@getunleash.ai>
This commit is contained in:
parent
7ce5b3de64
commit
6e650289c8
@ -99,6 +99,7 @@
|
||||
"db-migrate-shared": "1.2.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"errorhandler": "^1.5.1",
|
||||
"etag": "^1.8.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.6.0",
|
||||
"express-session": "^1.17.1",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import memoizee from 'memoizee';
|
||||
import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import { IFlagResolver, IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { querySchema } from '../../schema/feature-schema';
|
||||
@ -25,14 +25,14 @@ import {
|
||||
clientFeaturesSchema,
|
||||
ClientFeaturesSchema,
|
||||
} from '../../openapi/spec/client-features-schema';
|
||||
|
||||
import etag from 'etag';
|
||||
const version = 2;
|
||||
|
||||
interface QueryOverride {
|
||||
project?: string[];
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
const FEATURE_TOGGLE_MEMOIZED_ETAGS = 'clientFeaturesMemoizedEtags';
|
||||
export default class FeatureController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
|
||||
@ -48,6 +48,8 @@ export default class FeatureController extends Controller {
|
||||
|
||||
private cachedFeatures: any;
|
||||
|
||||
private seenEtags: Map<string, string>;
|
||||
|
||||
constructor(
|
||||
{
|
||||
featureToggleServiceV2,
|
||||
@ -64,12 +66,13 @@ export default class FeatureController extends Controller {
|
||||
config: IUnleashConfig,
|
||||
) {
|
||||
super(config);
|
||||
const { clientFeatureCaching } = config;
|
||||
const { clientFeatureCaching, flagResolver } = config;
|
||||
this.featureToggleServiceV2 = featureToggleServiceV2;
|
||||
this.segmentService = segmentService;
|
||||
this.clientSpecService = clientSpecService;
|
||||
this.openApiService = openApiService;
|
||||
this.logger = config.getLogger('client-api/feature.js');
|
||||
this.seenEtags = new Map<string, string>();
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
@ -106,7 +109,7 @@ export default class FeatureController extends Controller {
|
||||
if (clientFeatureCaching?.enabled) {
|
||||
this.cache = true;
|
||||
this.cachedFeatures = memoizee(
|
||||
(query) => this.resolveFeaturesAndSegments(query),
|
||||
(query) => this.resolveFeaturesAndSegments(query, flagResolver),
|
||||
{
|
||||
promise: true,
|
||||
maxAge: clientFeatureCaching.maxAge,
|
||||
@ -116,16 +119,53 @@ export default class FeatureController extends Controller {
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.info('Cached features was enabled');
|
||||
if (flagResolver.isEnabled(FEATURE_TOGGLE_MEMOIZED_ETAGS)) {
|
||||
this.logger.info('Memoized etags was enabled');
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getAllCached,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
operationId: 'getAllClientFeatures',
|
||||
tags: ['Client'],
|
||||
responses: {
|
||||
200: createResponseSchema(
|
||||
'clientFeaturesSchema',
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveFeaturesAndSegments(
|
||||
query?: IFeatureToggleQuery,
|
||||
flagResolver?: IFlagResolver,
|
||||
): Promise<[FeatureConfigurationClient[], ISegment[]]> {
|
||||
return Promise.all([
|
||||
this.featureToggleServiceV2.getClientFeatures(query),
|
||||
this.segmentService.getActive(),
|
||||
]);
|
||||
]).then((data) => {
|
||||
if (flagResolver?.isEnabled(FEATURE_TOGGLE_MEMOIZED_ETAGS)) {
|
||||
this.seenEtags.set(
|
||||
JSON.stringify(query),
|
||||
etag(
|
||||
JSON.stringify({
|
||||
version,
|
||||
features: data[0],
|
||||
query: { ...query },
|
||||
segments: data[1],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveQuery(
|
||||
@ -138,7 +178,7 @@ export default class FeatureController extends Controller {
|
||||
if (!isAllProjects(user.projects)) {
|
||||
override.project = user.projects;
|
||||
}
|
||||
if (user.environment !== ALL) {
|
||||
if (user.environment != ALL) {
|
||||
override.environment = user.environment;
|
||||
}
|
||||
}
|
||||
@ -195,12 +235,44 @@ export default class FeatureController extends Controller {
|
||||
return query;
|
||||
}
|
||||
|
||||
async getAllCached(
|
||||
req: IAuthRequest,
|
||||
res: Response<ClientFeaturesSchema>,
|
||||
): Promise<void> {
|
||||
const query = await this.resolveQuery(req);
|
||||
const [features, segments] = await this.cachedFeatures(query);
|
||||
const modifiedSince = req.header('If-None-Match').substring(2);
|
||||
this.logger.debug(`ETag header from Client: ${modifiedSince}`);
|
||||
const cached = this.seenEtags.get(JSON.stringify(query));
|
||||
this.logger.debug(`ETag header from memoizee ${cached}`);
|
||||
if (modifiedSince !== undefined && modifiedSince === cached) {
|
||||
// We have a match. Return 304
|
||||
res.status(304);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getAll(
|
||||
req: IAuthRequest,
|
||||
res: Response<ClientFeaturesSchema>,
|
||||
): Promise<void> {
|
||||
const query = await this.resolveQuery(req);
|
||||
|
||||
const [features, segments] = this.cache
|
||||
? await this.cachedFeatures(query)
|
||||
: await this.resolveFeaturesAndSegments(query);
|
||||
|
@ -47,6 +47,10 @@ const flags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE,
|
||||
false,
|
||||
),
|
||||
clientFeaturesMemoizedEtags: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_CLIENT_FEATURES_MEMOIZED_ETAGS,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -3345,6 +3345,11 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
etag@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user