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:
parent
aa3e49f8c5
commit
7f83d3e224
@ -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',
|
||||||
},
|
},
|
||||||
|
@ -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 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
src/lib/routes/client-api/request-counter.test.ts
Normal file
22
src/lib/routes/client-api/request-counter.test.ts
Normal 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);
|
||||||
|
});
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user