diff --git a/src/lib/openapi/spec/client-features-schema.ts b/src/lib/openapi/spec/client-features-schema.ts index 29d935e64a..e70975e61d 100644 --- a/src/lib/openapi/spec/client-features-schema.ts +++ b/src/lib/openapi/spec/client-features-schema.ts @@ -14,6 +14,9 @@ export const clientFeaturesSchema = { type: 'object', required: ['version', 'features'], properties: { + backOff: { + type: 'number', + }, version: { type: 'number', }, diff --git a/src/lib/routes/client-api/feature.ts b/src/lib/routes/client-api/feature.ts index 04594e682c..3b5438ca26 100644 --- a/src/lib/routes/client-api/feature.ts +++ b/src/lib/routes/client-api/feature.ts @@ -25,6 +25,7 @@ import { clientFeaturesSchema, ClientFeaturesSchema, } from '../../openapi/spec/client-features-schema'; +import RequestCounter from './request-counter'; const version = 2; @@ -46,6 +47,8 @@ export default class FeatureController extends Controller { private readonly cache: boolean; + private requestCounter: RequestCounter; + private cachedFeatures: any; constructor( @@ -70,6 +73,7 @@ export default class FeatureController extends Controller { this.clientSpecService = clientSpecService; this.openApiService = openApiService; this.logger = config.getLogger('client-api/feature.js'); + this.requestCounter = new RequestCounter(); this.route({ method: 'get', @@ -201,23 +205,35 @@ export default class FeatureController extends Controller { ): Promise { const query = await this.resolveQuery(req); + const { appName } = req.body; + + if (appName) { + this.requestCounter.recordRequest(appName); + } + const [features, segments] = this.cache ? await this.cachedFeatures(query) : await this.resolveFeaturesAndSegments(query); + let commonFields = { features, version }; + + if (this.requestCounter.isRPSOverTresholdForApp(appName)) { + commonFields.backOff = 10; + } + if (this.clientSpecService.requestSupportsSpec(req, 'segments')) { this.openApiService.respondWithValidation( 200, res, clientFeaturesSchema.$id, - { version, features, query: { ...query }, segments }, + { ...commonFields, query: { ...query }, segments }, ); } else { this.openApiService.respondWithValidation( 200, res, clientFeaturesSchema.$id, - { version, features, query }, + { ...commonFields, query }, ); } } diff --git a/src/lib/routes/client-api/request-counter.test.ts b/src/lib/routes/client-api/request-counter.test.ts new file mode 100644 index 0000000000..51b5c2cc87 --- /dev/null +++ b/src/lib/routes/client-api/request-counter.test.ts @@ -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); +}); diff --git a/src/lib/routes/client-api/request-counter.ts b/src/lib/routes/client-api/request-counter.ts index 12500371c7..50e2beb73c 100644 --- a/src/lib/routes/client-api/request-counter.ts +++ b/src/lib/routes/client-api/request-counter.ts @@ -1,21 +1,16 @@ -datefns; - export default class RequestCounter { private requestCache: Object; private longTermCache: Object; private interval: number; constructor() { - this.requestCache = {}; + this.requestCache = this.createBucket(); this.longTermCache = []; this.interval = setInterval(() => { this.requestCache.endTime = new Date(); - const longTermCacheObject = { - ...this.requestCache, - rps: this.calculateRPS(), - }; + const longTermCacheObject = this.calculateRPS(this.requestCache); this.longTermCache.push(longTermCacheObject); @@ -25,23 +20,36 @@ export default class RequestCounter { createBucket = () => { const bucket = { - count: 0, + apps: {}, startTime: new Date(), endTime: null, }; return bucket; }; - recordRequest = (appName: string): Promise => { - if (this.requestCache[appName]) { - this.requestCache[count] += 1; + recordRequest = (appName: string): void => { + if (this.requestCache['apps'][appName]) { + this.requestCache['apps'][appName].count += 1; } else { - this.requestCache[appName] = { count: 1 }; + this.requestCache['apps'][appName] = { count: 1 }; } }; - calculateRPS = () => { - const { count } = this.requestCache; - return count / 300; + getBuckets = (): Object => { + return this.longTermCache; + }; + + 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; }; }