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

fix: add backoff to the client features response

This commit is contained in:
Fredrik Oseberg 2022-09-22 15:14:09 +02:00
parent aa3e49f8c5
commit 7f83d3e224
4 changed files with 66 additions and 17 deletions

View File

@ -14,6 +14,9 @@ export const clientFeaturesSchema = {
type: 'object', type: 'object',
required: ['version', 'features'], required: ['version', 'features'],
properties: { properties: {
backOff: {
type: 'number',
},
version: { version: {
type: 'number', type: 'number',
}, },

View File

@ -25,6 +25,7 @@ import {
clientFeaturesSchema, clientFeaturesSchema,
ClientFeaturesSchema, ClientFeaturesSchema,
} from '../../openapi/spec/client-features-schema'; } from '../../openapi/spec/client-features-schema';
import RequestCounter from './request-counter';
const version = 2; const version = 2;
@ -46,6 +47,8 @@ export default class FeatureController extends Controller {
private readonly cache: boolean; private readonly cache: boolean;
private requestCounter: RequestCounter;
private cachedFeatures: any; private cachedFeatures: any;
constructor( constructor(
@ -70,6 +73,7 @@ export default class FeatureController extends Controller {
this.clientSpecService = clientSpecService; this.clientSpecService = clientSpecService;
this.openApiService = openApiService; this.openApiService = openApiService;
this.logger = config.getLogger('client-api/feature.js'); this.logger = config.getLogger('client-api/feature.js');
this.requestCounter = new RequestCounter();
this.route({ this.route({
method: 'get', method: 'get',
@ -201,23 +205,35 @@ export default class FeatureController extends Controller {
): Promise<void> { ): Promise<void> {
const query = await this.resolveQuery(req); const query = await this.resolveQuery(req);
const { appName } = req.body;
if (appName) {
this.requestCounter.recordRequest(appName);
}
const [features, segments] = this.cache const [features, segments] = this.cache
? await this.cachedFeatures(query) ? await this.cachedFeatures(query)
: await this.resolveFeaturesAndSegments(query); : await this.resolveFeaturesAndSegments(query);
let commonFields = { features, version };
if (this.requestCounter.isRPSOverTresholdForApp(appName)) {
commonFields.backOff = 10;
}
if (this.clientSpecService.requestSupportsSpec(req, 'segments')) { if (this.clientSpecService.requestSupportsSpec(req, 'segments')) {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
clientFeaturesSchema.$id, clientFeaturesSchema.$id,
{ version, features, query: { ...query }, segments }, { ...commonFields, query: { ...query }, segments },
); );
} else { } else {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
clientFeaturesSchema.$id, clientFeaturesSchema.$id,
{ version, features, query }, { ...commonFields, query },
); );
} }
} }

View File

@ -0,0 +1,22 @@
import RequestCounter from './request-counter';
test('Request counter can keep track of seen app names', async () => {
jest.useFakeTimers();
const counter = new RequestCounter();
counter.recordRequest('app1');
counter.recordRequest('app1');
counter.recordRequest('app1');
counter.recordRequest('app1');
counter.recordRequest('app1');
counter.recordRequest('app1');
counter.recordRequest('app2');
counter.recordRequest('app2');
jest.advanceTimersByTime(400000);
const buckets = counter.getBuckets();
expect(buckets).toHaveLength(1);
expect(buckets[0].apps['app1'].count).toBe(6);
expect(buckets[0].apps['app2'].count).toBe(2);
});

View File

@ -1,21 +1,16 @@
datefns;
export default class RequestCounter { export default class RequestCounter {
private requestCache: Object; private requestCache: Object;
private longTermCache: Object; private longTermCache: Object;
private interval: number; private interval: number;
constructor() { constructor() {
this.requestCache = {}; this.requestCache = this.createBucket();
this.longTermCache = []; this.longTermCache = [];
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.requestCache.endTime = new Date(); this.requestCache.endTime = new Date();
const longTermCacheObject = { const longTermCacheObject = this.calculateRPS(this.requestCache);
...this.requestCache,
rps: this.calculateRPS(),
};
this.longTermCache.push(longTermCacheObject); this.longTermCache.push(longTermCacheObject);
@ -25,23 +20,36 @@ export default class RequestCounter {
createBucket = () => { createBucket = () => {
const bucket = { const bucket = {
count: 0, apps: {},
startTime: new Date(), startTime: new Date(),
endTime: null, endTime: null,
}; };
return bucket; return bucket;
}; };
recordRequest = (appName: string): Promise<void> => { recordRequest = (appName: string): void => {
if (this.requestCache[appName]) { if (this.requestCache['apps'][appName]) {
this.requestCache[count] += 1; this.requestCache['apps'][appName].count += 1;
} else { } else {
this.requestCache[appName] = { count: 1 }; this.requestCache['apps'][appName] = { count: 1 };
} }
}; };
calculateRPS = () => { getBuckets = (): Object => {
const { count } = this.requestCache; return this.longTermCache;
return count / 300; };
calculateRPS = (requestCache: Object) => {
Object.keys(requestCache.apps).forEach((appName) => {
const app = requestCache.apps[appName];
const rps = app.count / 300;
app.rps = rps;
});
return requestCache;
};
isRPSOverTresholdForApp = (appName: string) => {
return true;
}; };
} }