@tramvai/module-request-limiter 1.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @tramvai/module-request-limiter
2
+
3
+ This module provides a request limiter, designed to dynamically limit the number of requests handled concurrently by the application.
4
+ Request limiter monitors the application server health through event loop lag checks.
5
+
6
+ ## Installation
7
+
8
+ You need to install `@tramvai/module-request-limiter`
9
+
10
+ ```bash npm2yarn
11
+ yarn add @tramvai/module-request-limiter
12
+ ```
13
+
14
+ And connect in the project
15
+
16
+ ```tsx
17
+ import { createApp } from '@tramvai/core';
18
+ import { RequestLimiterModule } from '@tramvai/module-request-limiter';
19
+
20
+ createApp({
21
+ name: 'tincoin',
22
+ modules: [ RequestLimiterModule ],
23
+ });
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Activation
29
+
30
+ To activate the limiter, use token `REQUESTS_LIMITER_ACTIVATE_TOKEN` with `true` value:
31
+
32
+ ```ts
33
+ import { REQUESTS_LIMITER_ACTIVATE_TOKEN } from '@tramvai/module-request-limiter';
34
+
35
+ const provider = {
36
+ provide: REQUESTS_LIMITER_ACTIVATE_TOKEN,
37
+ useValue: true,
38
+ };
39
+ ```
40
+
41
+ ### Configuration
42
+
43
+ You can pass options to request limiter by `REQUESTS_LIMITER_OPTIONS_TOKEN` token:
44
+
45
+ ```ts
46
+ import { REQUESTS_LIMITER_OPTIONS_TOKEN } from '@tramvai/module-request-limiter';
47
+
48
+ const provider = {
49
+ provide: REQUESTS_LIMITER_OPTIONS_TOKEN,
50
+ // default options
51
+ useValue: {
52
+ limit: 10,
53
+ queue: 100,
54
+ maxEventLoopDelay: 150,
55
+ error: { httpStatus: 429, message: 'Too Many Requests' },
56
+ },
57
+ };
58
+ ```
59
+
60
+ ## Explanation
61
+
62
+ After the server starts, request limiter can handle `options.limit` parallel connections.
63
+ Requests over this limit will fall in queue, the size of which limited by `options.queue`.
64
+ Other connections that exceed the size of the queue will be terminated with an error `options.error`.
65
+
66
+ Requests from queue will be processed from the end of the queue.
67
+
68
+ Every second limiter checking the current event loop lag.
69
+ If current lag exceed the `options.maxEventLoopDelay`, limit number of parallel connections will be decremented.
70
+ Otherwise, this limit will be incremented.
@@ -0,0 +1,2 @@
1
+ export declare class RequestLimiterModule {
2
+ }
package/lib/browser.js ADDED
@@ -0,0 +1,12 @@
1
+ import { __decorate } from 'tslib';
2
+ import { Module } from '@tramvai/core';
3
+
4
+ let RequestLimiterModule = class RequestLimiterModule {
5
+ };
6
+ RequestLimiterModule = __decorate([
7
+ Module({
8
+ providers: [],
9
+ })
10
+ ], RequestLimiterModule);
11
+
12
+ export { RequestLimiterModule };
@@ -0,0 +1,31 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ export interface RequestLimiterOptions {
3
+ limit?: number;
4
+ queue?: number;
5
+ maxEventLoopDelay?: number;
6
+ error?: {
7
+ httpStatus: number;
8
+ message: string;
9
+ };
10
+ }
11
+ export interface RequestLimiterRequest {
12
+ req: Request;
13
+ res: Response;
14
+ next: NextFunction;
15
+ }
16
+ export declare class RequestLimiter {
17
+ private currentActive;
18
+ private eventLoopDelay;
19
+ private queue;
20
+ private activeRequestLimit;
21
+ private queueLimit;
22
+ private error;
23
+ private minimalActiveRequestLimit;
24
+ private maxEventLoopDelay;
25
+ private eventLoopHistogram;
26
+ constructor(options?: RequestLimiterOptions);
27
+ private nextTick;
28
+ add(request: RequestLimiterRequest): void;
29
+ private loop;
30
+ private run;
31
+ }
@@ -0,0 +1,4 @@
1
+ export * from './requestLimiter';
2
+ export * from './tokens';
3
+ export declare class RequestLimiterModule {
4
+ }
@@ -0,0 +1,181 @@
1
+ import { __decorate } from 'tslib';
2
+ import { Module, provide, Scope } from '@tramvai/core';
3
+ import { WEB_APP_LIMITER_TOKEN } from '@tramvai/tokens-server';
4
+ import onFinished from 'on-finished';
5
+ import { HttpError } from '@tinkoff/errors';
6
+ import { monitorEventLoopDelay } from 'perf_hooks';
7
+ import { createToken } from '@tinkoff/dippy';
8
+
9
+ class DoubleLinkedList {
10
+ constructor() {
11
+ this.length = 0;
12
+ this.start = null;
13
+ this.end = null;
14
+ }
15
+ push(value) {
16
+ const newNode = {
17
+ value,
18
+ next: null,
19
+ prev: null,
20
+ };
21
+ this.length++;
22
+ if (this.start === null) {
23
+ this.start = newNode;
24
+ this.end = newNode;
25
+ return;
26
+ }
27
+ const currentEnd = this.end;
28
+ this.end = newNode;
29
+ this.end.prev = currentEnd;
30
+ currentEnd.next = this.end;
31
+ }
32
+ pop() {
33
+ if (this.end === null) {
34
+ return null;
35
+ }
36
+ this.length--;
37
+ // if equal we have only 1 node, so we just remove start
38
+ if (this.end === this.start) {
39
+ this.start = null;
40
+ }
41
+ const { value } = this.end;
42
+ this.end = this.end.prev;
43
+ if (this.end) {
44
+ this.end.next = null;
45
+ }
46
+ return value;
47
+ }
48
+ shift() {
49
+ if (this.start === null) {
50
+ return null;
51
+ }
52
+ this.length--;
53
+ // if equal we have only 1 node, so we just remove end
54
+ if (this.end === this.start) {
55
+ this.end = null;
56
+ }
57
+ const { value } = this.start;
58
+ this.start = this.start.next;
59
+ if (this.start) {
60
+ this.start.prev = null;
61
+ }
62
+ return value;
63
+ }
64
+ size() {
65
+ return this.length;
66
+ }
67
+ }
68
+
69
+ const DEFAULT_OPTIONS = {
70
+ limit: 10,
71
+ queue: 100,
72
+ maxEventLoopDelay: 150,
73
+ error: { httpStatus: 429, message: 'Too Many Requests' },
74
+ };
75
+ const resolution = 10;
76
+ class RequestLimiter {
77
+ constructor(options = DEFAULT_OPTIONS) {
78
+ this.currentActive = 0;
79
+ this.eventLoopDelay = 0;
80
+ this.queue = new DoubleLinkedList();
81
+ const { limit = DEFAULT_OPTIONS.limit, queue = DEFAULT_OPTIONS.queue, maxEventLoopDelay = DEFAULT_OPTIONS.maxEventLoopDelay, error = DEFAULT_OPTIONS.error, } = options;
82
+ this.activeRequestLimit = limit;
83
+ this.minimalActiveRequestLimit = Math.floor(limit / 2);
84
+ this.queueLimit = queue;
85
+ this.error = error;
86
+ this.maxEventLoopDelay = maxEventLoopDelay;
87
+ this.eventLoopHistogram = monitorEventLoopDelay({ resolution });
88
+ this.eventLoopHistogram.enable();
89
+ const timer = setInterval(() => this.nextTick(), 1000);
90
+ timer.unref();
91
+ }
92
+ // General idea is change limits ever second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
93
+ nextTick() {
94
+ this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
95
+ if (Number.isNaN(this.eventLoopDelay))
96
+ this.eventLoopDelay = Infinity;
97
+ this.eventLoopHistogram.reset();
98
+ if (this.currentActive >= this.activeRequestLimit &&
99
+ this.activeRequestLimit > this.minimalActiveRequestLimit) {
100
+ if (this.eventLoopDelay <= this.maxEventLoopDelay) {
101
+ this.activeRequestLimit++;
102
+ }
103
+ else {
104
+ this.activeRequestLimit--;
105
+ }
106
+ }
107
+ }
108
+ add(request) {
109
+ if (this.currentActive < this.activeRequestLimit) {
110
+ this.run(request);
111
+ return;
112
+ }
113
+ if (this.queue.length >= this.queueLimit) {
114
+ const lastNode = this.queue.shift();
115
+ lastNode.next(new HttpError(this.error));
116
+ }
117
+ this.queue.push(request);
118
+ }
119
+ loop() {
120
+ while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
121
+ // better if we start with new requests. Because more opportunity to answer before client cancel request
122
+ this.run(this.queue.pop());
123
+ }
124
+ }
125
+ run({ req, res, next }) {
126
+ this.currentActive++;
127
+ // onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
128
+ onFinished(res, () => {
129
+ this.currentActive--;
130
+ this.loop();
131
+ });
132
+ next();
133
+ }
134
+ }
135
+
136
+ const REQUESTS_LIMITER_TOKEN = createToken('requestsLimiterToken');
137
+ const REQUESTS_LIMITER_ACTIVATE_TOKEN = createToken('requestsLimiterActivateToken');
138
+ const REQUESTS_LIMITER_OPTIONS_TOKEN = createToken('requestsLimiterOptionsToken');
139
+
140
+ let RequestLimiterModule = class RequestLimiterModule {
141
+ };
142
+ RequestLimiterModule = __decorate([
143
+ Module({
144
+ providers: [
145
+ provide({
146
+ provide: REQUESTS_LIMITER_TOKEN,
147
+ scope: Scope.SINGLETON,
148
+ useFactory: ({ options, featureEnable }) => {
149
+ if (featureEnable !== true) {
150
+ return;
151
+ }
152
+ return new RequestLimiter(options);
153
+ },
154
+ deps: {
155
+ options: { token: REQUESTS_LIMITER_OPTIONS_TOKEN, optional: true },
156
+ featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
157
+ },
158
+ }),
159
+ provide({
160
+ provide: WEB_APP_LIMITER_TOKEN,
161
+ multi: true,
162
+ useFactory: ({ requestsLimiter, featureEnable }) => {
163
+ return function addRequestsLimiterMiddleware(app) {
164
+ if (featureEnable !== true) {
165
+ return;
166
+ }
167
+ app.use((req, res, next) => {
168
+ requestsLimiter.add({ req, res, next });
169
+ });
170
+ };
171
+ },
172
+ deps: {
173
+ requestsLimiter: REQUESTS_LIMITER_TOKEN,
174
+ featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
175
+ },
176
+ }),
177
+ ],
178
+ })
179
+ ], RequestLimiterModule);
180
+
181
+ export { REQUESTS_LIMITER_ACTIVATE_TOKEN, REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN, RequestLimiter, RequestLimiterModule };
package/lib/server.js ADDED
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var tslib = require('tslib');
6
+ var core = require('@tramvai/core');
7
+ var tokensServer = require('@tramvai/tokens-server');
8
+ var onFinished = require('on-finished');
9
+ var errors = require('@tinkoff/errors');
10
+ var perf_hooks = require('perf_hooks');
11
+ var dippy = require('@tinkoff/dippy');
12
+
13
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
14
+
15
+ var onFinished__default = /*#__PURE__*/_interopDefaultLegacy(onFinished);
16
+
17
+ class DoubleLinkedList {
18
+ constructor() {
19
+ this.length = 0;
20
+ this.start = null;
21
+ this.end = null;
22
+ }
23
+ push(value) {
24
+ const newNode = {
25
+ value,
26
+ next: null,
27
+ prev: null,
28
+ };
29
+ this.length++;
30
+ if (this.start === null) {
31
+ this.start = newNode;
32
+ this.end = newNode;
33
+ return;
34
+ }
35
+ const currentEnd = this.end;
36
+ this.end = newNode;
37
+ this.end.prev = currentEnd;
38
+ currentEnd.next = this.end;
39
+ }
40
+ pop() {
41
+ if (this.end === null) {
42
+ return null;
43
+ }
44
+ this.length--;
45
+ // if equal we have only 1 node, so we just remove start
46
+ if (this.end === this.start) {
47
+ this.start = null;
48
+ }
49
+ const { value } = this.end;
50
+ this.end = this.end.prev;
51
+ if (this.end) {
52
+ this.end.next = null;
53
+ }
54
+ return value;
55
+ }
56
+ shift() {
57
+ if (this.start === null) {
58
+ return null;
59
+ }
60
+ this.length--;
61
+ // if equal we have only 1 node, so we just remove end
62
+ if (this.end === this.start) {
63
+ this.end = null;
64
+ }
65
+ const { value } = this.start;
66
+ this.start = this.start.next;
67
+ if (this.start) {
68
+ this.start.prev = null;
69
+ }
70
+ return value;
71
+ }
72
+ size() {
73
+ return this.length;
74
+ }
75
+ }
76
+
77
+ const DEFAULT_OPTIONS = {
78
+ limit: 10,
79
+ queue: 100,
80
+ maxEventLoopDelay: 150,
81
+ error: { httpStatus: 429, message: 'Too Many Requests' },
82
+ };
83
+ const resolution = 10;
84
+ class RequestLimiter {
85
+ constructor(options = DEFAULT_OPTIONS) {
86
+ this.currentActive = 0;
87
+ this.eventLoopDelay = 0;
88
+ this.queue = new DoubleLinkedList();
89
+ const { limit = DEFAULT_OPTIONS.limit, queue = DEFAULT_OPTIONS.queue, maxEventLoopDelay = DEFAULT_OPTIONS.maxEventLoopDelay, error = DEFAULT_OPTIONS.error, } = options;
90
+ this.activeRequestLimit = limit;
91
+ this.minimalActiveRequestLimit = Math.floor(limit / 2);
92
+ this.queueLimit = queue;
93
+ this.error = error;
94
+ this.maxEventLoopDelay = maxEventLoopDelay;
95
+ this.eventLoopHistogram = perf_hooks.monitorEventLoopDelay({ resolution });
96
+ this.eventLoopHistogram.enable();
97
+ const timer = setInterval(() => this.nextTick(), 1000);
98
+ timer.unref();
99
+ }
100
+ // General idea is change limits ever second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
101
+ nextTick() {
102
+ this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
103
+ if (Number.isNaN(this.eventLoopDelay))
104
+ this.eventLoopDelay = Infinity;
105
+ this.eventLoopHistogram.reset();
106
+ if (this.currentActive >= this.activeRequestLimit &&
107
+ this.activeRequestLimit > this.minimalActiveRequestLimit) {
108
+ if (this.eventLoopDelay <= this.maxEventLoopDelay) {
109
+ this.activeRequestLimit++;
110
+ }
111
+ else {
112
+ this.activeRequestLimit--;
113
+ }
114
+ }
115
+ }
116
+ add(request) {
117
+ if (this.currentActive < this.activeRequestLimit) {
118
+ this.run(request);
119
+ return;
120
+ }
121
+ if (this.queue.length >= this.queueLimit) {
122
+ const lastNode = this.queue.shift();
123
+ lastNode.next(new errors.HttpError(this.error));
124
+ }
125
+ this.queue.push(request);
126
+ }
127
+ loop() {
128
+ while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
129
+ // better if we start with new requests. Because more opportunity to answer before client cancel request
130
+ this.run(this.queue.pop());
131
+ }
132
+ }
133
+ run({ req, res, next }) {
134
+ this.currentActive++;
135
+ // onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
136
+ onFinished__default["default"](res, () => {
137
+ this.currentActive--;
138
+ this.loop();
139
+ });
140
+ next();
141
+ }
142
+ }
143
+
144
+ const REQUESTS_LIMITER_TOKEN = dippy.createToken('requestsLimiterToken');
145
+ const REQUESTS_LIMITER_ACTIVATE_TOKEN = dippy.createToken('requestsLimiterActivateToken');
146
+ const REQUESTS_LIMITER_OPTIONS_TOKEN = dippy.createToken('requestsLimiterOptionsToken');
147
+
148
+ exports.RequestLimiterModule = class RequestLimiterModule {
149
+ };
150
+ exports.RequestLimiterModule = tslib.__decorate([
151
+ core.Module({
152
+ providers: [
153
+ core.provide({
154
+ provide: REQUESTS_LIMITER_TOKEN,
155
+ scope: core.Scope.SINGLETON,
156
+ useFactory: ({ options, featureEnable }) => {
157
+ if (featureEnable !== true) {
158
+ return;
159
+ }
160
+ return new RequestLimiter(options);
161
+ },
162
+ deps: {
163
+ options: { token: REQUESTS_LIMITER_OPTIONS_TOKEN, optional: true },
164
+ featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
165
+ },
166
+ }),
167
+ core.provide({
168
+ provide: tokensServer.WEB_APP_LIMITER_TOKEN,
169
+ multi: true,
170
+ useFactory: ({ requestsLimiter, featureEnable }) => {
171
+ return function addRequestsLimiterMiddleware(app) {
172
+ if (featureEnable !== true) {
173
+ return;
174
+ }
175
+ app.use((req, res, next) => {
176
+ requestsLimiter.add({ req, res, next });
177
+ });
178
+ };
179
+ },
180
+ deps: {
181
+ requestsLimiter: REQUESTS_LIMITER_TOKEN,
182
+ featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
183
+ },
184
+ }),
185
+ ],
186
+ })
187
+ ], exports.RequestLimiterModule);
188
+
189
+ exports.REQUESTS_LIMITER_ACTIVATE_TOKEN = REQUESTS_LIMITER_ACTIVATE_TOKEN;
190
+ exports.REQUESTS_LIMITER_OPTIONS_TOKEN = REQUESTS_LIMITER_OPTIONS_TOKEN;
191
+ exports.REQUESTS_LIMITER_TOKEN = REQUESTS_LIMITER_TOKEN;
192
+ exports.RequestLimiter = RequestLimiter;
@@ -0,0 +1,4 @@
1
+ import type { RequestLimiter, RequestLimiterOptions } from './requestLimiter';
2
+ export declare const REQUESTS_LIMITER_TOKEN: RequestLimiter;
3
+ export declare const REQUESTS_LIMITER_ACTIVATE_TOKEN: boolean;
4
+ export declare const REQUESTS_LIMITER_OPTIONS_TOKEN: RequestLimiterOptions;
@@ -0,0 +1,15 @@
1
+ interface ListNode<Value> {
2
+ next: ListNode<Value>;
3
+ prev: ListNode<Value>;
4
+ value: Value;
5
+ }
6
+ export declare class DoubleLinkedList<Value> {
7
+ length: number;
8
+ start: null | ListNode<Value>;
9
+ end: null | ListNode<Value>;
10
+ push(value: Value): void;
11
+ pop(): Value;
12
+ shift(): Value;
13
+ size(): number;
14
+ }
15
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@tramvai/module-request-limiter",
3
+ "version": "1.75.0",
4
+ "description": "Enable different rendering modes for pages",
5
+ "main": "lib/server.js",
6
+ "module": "lib/server.es.js",
7
+ "browser": "lib/browser.js",
8
+ "typings": "lib/server.d.ts",
9
+ "files": [
10
+ "lib"
11
+ ],
12
+ "sideEffects": false,
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:Tinkoff/tramvai.git"
16
+ },
17
+ "license": "Apache-2.0",
18
+ "scripts": {
19
+ "build": "tramvai-build --for-publish",
20
+ "watch": "tsc -w",
21
+ "build-for-publish": "true"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "dependencies": {
27
+ "@tinkoff/errors": "0.2.18",
28
+ "on-finished": "^2.3.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^4.17.9"
32
+ },
33
+ "peerDependencies": {
34
+ "@tinkoff/dippy": "0.7.38",
35
+ "@tramvai/core": "1.75.0",
36
+ "@tramvai/tokens-server": "1.75.0",
37
+ "express": "^4.17.1",
38
+ "tslib": "^2.0.3"
39
+ }
40
+ }