@tramvai/module-request-limiter 2.70.1 → 2.72.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/lib/browser.js +2 -6
- package/lib/requestLimiter.es.js +85 -0
- package/lib/requestLimiter.js +96 -0
- package/lib/server.es.js +5 -148
- package/lib/server.js +23 -173
- package/lib/tokens.browser.js +7 -0
- package/lib/tokens.es.js +7 -0
- package/lib/tokens.js +13 -0
- package/lib/utils/doubleLinkedList.es.js +61 -0
- package/lib/utils/doubleLinkedList.js +65 -0
- package/package.json +8 -9
package/lib/browser.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import { Module } from '@tramvai/core';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const REQUESTS_LIMITER_TOKEN = createToken('requestsLimiterToken');
|
|
6
|
-
const REQUESTS_LIMITER_ACTIVATE_TOKEN = createToken('requestsLimiterActivateToken');
|
|
7
|
-
const REQUESTS_LIMITER_OPTIONS_TOKEN = createToken('requestsLimiterOptionsToken');
|
|
3
|
+
export { REQUESTS_LIMITER_ACTIVATE_TOKEN, REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN } from './tokens.browser.js';
|
|
8
4
|
|
|
9
5
|
let RequestLimiterModule = class RequestLimiterModule {
|
|
10
6
|
};
|
|
@@ -14,4 +10,4 @@ RequestLimiterModule = __decorate([
|
|
|
14
10
|
})
|
|
15
11
|
], RequestLimiterModule);
|
|
16
12
|
|
|
17
|
-
export {
|
|
13
|
+
export { RequestLimiterModule };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { HttpError } from '@tinkoff/errors';
|
|
3
|
+
import onFinished from 'on-finished';
|
|
4
|
+
import { monitorEventLoopDelay } from 'perf_hooks';
|
|
5
|
+
import { DoubleLinkedList } from './utils/doubleLinkedList.es.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OPTIONS = {
|
|
8
|
+
limit: 10,
|
|
9
|
+
queue: 100,
|
|
10
|
+
maxEventLoopDelay: 150,
|
|
11
|
+
error: { httpStatus: 429, message: 'Too Many Requests' },
|
|
12
|
+
};
|
|
13
|
+
const resolution = 10;
|
|
14
|
+
class RequestLimiter {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.currentActive = 0;
|
|
17
|
+
this.eventLoopDelay = 0;
|
|
18
|
+
this.queue = new DoubleLinkedList();
|
|
19
|
+
const { limit, queue, maxEventLoopDelay, error } = options;
|
|
20
|
+
this.activeRequestLimit = limit;
|
|
21
|
+
this.minimalActiveRequestLimit = Math.round(limit / 3);
|
|
22
|
+
this.queueLimit = queue;
|
|
23
|
+
this.error = error;
|
|
24
|
+
this.maxEventLoopDelay = maxEventLoopDelay;
|
|
25
|
+
this.eventLoopHistogram = monitorEventLoopDelay({ resolution });
|
|
26
|
+
this.eventLoopHistogram.enable();
|
|
27
|
+
const timer = setInterval(() => this.nextTick(), 1000);
|
|
28
|
+
timer.unref();
|
|
29
|
+
}
|
|
30
|
+
onResponse() {
|
|
31
|
+
this.currentActive--;
|
|
32
|
+
this.loop();
|
|
33
|
+
}
|
|
34
|
+
// General idea is change limits every second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
|
|
35
|
+
nextTick() {
|
|
36
|
+
this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
|
|
37
|
+
if (Number.isNaN(this.eventLoopDelay))
|
|
38
|
+
this.eventLoopDelay = Infinity;
|
|
39
|
+
this.eventLoopHistogram.reset();
|
|
40
|
+
if (this.currentActive >= this.activeRequestLimit) {
|
|
41
|
+
if (this.eventLoopDelay <= this.maxEventLoopDelay) {
|
|
42
|
+
this.activeRequestLimit++;
|
|
43
|
+
// We need to have minimalActiveRequestLimit
|
|
44
|
+
}
|
|
45
|
+
else if (this.activeRequestLimit > this.minimalActiveRequestLimit) {
|
|
46
|
+
this.activeRequestLimit--;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
add(request) {
|
|
51
|
+
if (this.currentActive < this.activeRequestLimit) {
|
|
52
|
+
this.run(request);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (this.queue.length >= this.queueLimit) {
|
|
56
|
+
const lastNode = this.queue.shift();
|
|
57
|
+
lastNode.next(new HttpError(this.error));
|
|
58
|
+
}
|
|
59
|
+
this.queue.push(request);
|
|
60
|
+
}
|
|
61
|
+
loop() {
|
|
62
|
+
while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
|
|
63
|
+
// better if we start with new requests. Because more opportunity to answer before client cancel request
|
|
64
|
+
this.run(this.queue.pop());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
run({ next, res }) {
|
|
68
|
+
this.currentActive++;
|
|
69
|
+
// onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
|
|
70
|
+
// fastify: it only works this way with on-finished, as using hook `onRequest` is not enough to handle all of the requests
|
|
71
|
+
// and some of the failed requests are getting disappeared
|
|
72
|
+
// and other hooks useless as well. [related issue](https://github.com/fastify/fastify/issues/1352)
|
|
73
|
+
onFinished(res.raw, () => {
|
|
74
|
+
this.onResponse();
|
|
75
|
+
});
|
|
76
|
+
next();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const fastifyRequestsLimiter = fp(async (fastify, { requestsLimiter }) => {
|
|
80
|
+
fastify.addHook('onRequest', (req, res, next) => {
|
|
81
|
+
requestsLimiter.add({ req, res, next });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export { DEFAULT_OPTIONS, RequestLimiter, fastifyRequestsLimiter };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var fp = require('fastify-plugin');
|
|
6
|
+
var errors = require('@tinkoff/errors');
|
|
7
|
+
var onFinished = require('on-finished');
|
|
8
|
+
var perf_hooks = require('perf_hooks');
|
|
9
|
+
var doubleLinkedList = require('./utils/doubleLinkedList.js');
|
|
10
|
+
|
|
11
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
12
|
+
|
|
13
|
+
var fp__default = /*#__PURE__*/_interopDefaultLegacy(fp);
|
|
14
|
+
var onFinished__default = /*#__PURE__*/_interopDefaultLegacy(onFinished);
|
|
15
|
+
|
|
16
|
+
const DEFAULT_OPTIONS = {
|
|
17
|
+
limit: 10,
|
|
18
|
+
queue: 100,
|
|
19
|
+
maxEventLoopDelay: 150,
|
|
20
|
+
error: { httpStatus: 429, message: 'Too Many Requests' },
|
|
21
|
+
};
|
|
22
|
+
const resolution = 10;
|
|
23
|
+
class RequestLimiter {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.currentActive = 0;
|
|
26
|
+
this.eventLoopDelay = 0;
|
|
27
|
+
this.queue = new doubleLinkedList.DoubleLinkedList();
|
|
28
|
+
const { limit, queue, maxEventLoopDelay, error } = options;
|
|
29
|
+
this.activeRequestLimit = limit;
|
|
30
|
+
this.minimalActiveRequestLimit = Math.round(limit / 3);
|
|
31
|
+
this.queueLimit = queue;
|
|
32
|
+
this.error = error;
|
|
33
|
+
this.maxEventLoopDelay = maxEventLoopDelay;
|
|
34
|
+
this.eventLoopHistogram = perf_hooks.monitorEventLoopDelay({ resolution });
|
|
35
|
+
this.eventLoopHistogram.enable();
|
|
36
|
+
const timer = setInterval(() => this.nextTick(), 1000);
|
|
37
|
+
timer.unref();
|
|
38
|
+
}
|
|
39
|
+
onResponse() {
|
|
40
|
+
this.currentActive--;
|
|
41
|
+
this.loop();
|
|
42
|
+
}
|
|
43
|
+
// General idea is change limits every second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
|
|
44
|
+
nextTick() {
|
|
45
|
+
this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
|
|
46
|
+
if (Number.isNaN(this.eventLoopDelay))
|
|
47
|
+
this.eventLoopDelay = Infinity;
|
|
48
|
+
this.eventLoopHistogram.reset();
|
|
49
|
+
if (this.currentActive >= this.activeRequestLimit) {
|
|
50
|
+
if (this.eventLoopDelay <= this.maxEventLoopDelay) {
|
|
51
|
+
this.activeRequestLimit++;
|
|
52
|
+
// We need to have minimalActiveRequestLimit
|
|
53
|
+
}
|
|
54
|
+
else if (this.activeRequestLimit > this.minimalActiveRequestLimit) {
|
|
55
|
+
this.activeRequestLimit--;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
add(request) {
|
|
60
|
+
if (this.currentActive < this.activeRequestLimit) {
|
|
61
|
+
this.run(request);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.queue.length >= this.queueLimit) {
|
|
65
|
+
const lastNode = this.queue.shift();
|
|
66
|
+
lastNode.next(new errors.HttpError(this.error));
|
|
67
|
+
}
|
|
68
|
+
this.queue.push(request);
|
|
69
|
+
}
|
|
70
|
+
loop() {
|
|
71
|
+
while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
|
|
72
|
+
// better if we start with new requests. Because more opportunity to answer before client cancel request
|
|
73
|
+
this.run(this.queue.pop());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
run({ next, res }) {
|
|
77
|
+
this.currentActive++;
|
|
78
|
+
// onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
|
|
79
|
+
// fastify: it only works this way with on-finished, as using hook `onRequest` is not enough to handle all of the requests
|
|
80
|
+
// and some of the failed requests are getting disappeared
|
|
81
|
+
// and other hooks useless as well. [related issue](https://github.com/fastify/fastify/issues/1352)
|
|
82
|
+
onFinished__default["default"](res.raw, () => {
|
|
83
|
+
this.onResponse();
|
|
84
|
+
});
|
|
85
|
+
next();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const fastifyRequestsLimiter = fp__default["default"](async (fastify, { requestsLimiter }) => {
|
|
89
|
+
fastify.addHook('onRequest', (req, res, next) => {
|
|
90
|
+
requestsLimiter.add({ req, res, next });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
95
|
+
exports.RequestLimiter = RequestLimiter;
|
|
96
|
+
exports.fastifyRequestsLimiter = fastifyRequestsLimiter;
|
package/lib/server.es.js
CHANGED
|
@@ -2,153 +2,10 @@ import { __decorate } from 'tslib';
|
|
|
2
2
|
import { Module, provide, Scope } from '@tramvai/core';
|
|
3
3
|
import { ENV_USED_TOKEN, ENV_MANAGER_TOKEN } from '@tramvai/tokens-common';
|
|
4
4
|
import { WEB_FASTIFY_APP_LIMITER_TOKEN } from '@tramvai/tokens-server-private';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import { createToken } from '@tinkoff/dippy';
|
|
10
|
-
|
|
11
|
-
class DoubleLinkedList {
|
|
12
|
-
constructor() {
|
|
13
|
-
this.length = 0;
|
|
14
|
-
this.start = null;
|
|
15
|
-
this.end = null;
|
|
16
|
-
}
|
|
17
|
-
push(value) {
|
|
18
|
-
const newNode = {
|
|
19
|
-
value,
|
|
20
|
-
next: null,
|
|
21
|
-
prev: null,
|
|
22
|
-
};
|
|
23
|
-
this.length++;
|
|
24
|
-
if (this.start === null) {
|
|
25
|
-
this.start = newNode;
|
|
26
|
-
this.end = newNode;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const currentEnd = this.end;
|
|
30
|
-
this.end = newNode;
|
|
31
|
-
this.end.prev = currentEnd;
|
|
32
|
-
currentEnd.next = this.end;
|
|
33
|
-
}
|
|
34
|
-
pop() {
|
|
35
|
-
if (this.end === null) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
this.length--;
|
|
39
|
-
// if equal we have only 1 node, so we just remove start
|
|
40
|
-
if (this.end === this.start) {
|
|
41
|
-
this.start = null;
|
|
42
|
-
}
|
|
43
|
-
const { value } = this.end;
|
|
44
|
-
this.end = this.end.prev;
|
|
45
|
-
if (this.end) {
|
|
46
|
-
this.end.next = null;
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
shift() {
|
|
51
|
-
if (this.start === null) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
this.length--;
|
|
55
|
-
// if equal we have only 1 node, so we just remove end
|
|
56
|
-
if (this.end === this.start) {
|
|
57
|
-
this.end = null;
|
|
58
|
-
}
|
|
59
|
-
const { value } = this.start;
|
|
60
|
-
this.start = this.start.next;
|
|
61
|
-
if (this.start) {
|
|
62
|
-
this.start.prev = null;
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
65
|
-
}
|
|
66
|
-
size() {
|
|
67
|
-
return this.length;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const DEFAULT_OPTIONS = {
|
|
72
|
-
limit: 10,
|
|
73
|
-
queue: 100,
|
|
74
|
-
maxEventLoopDelay: 150,
|
|
75
|
-
error: { httpStatus: 429, message: 'Too Many Requests' },
|
|
76
|
-
};
|
|
77
|
-
const resolution = 10;
|
|
78
|
-
class RequestLimiter {
|
|
79
|
-
constructor(options) {
|
|
80
|
-
this.currentActive = 0;
|
|
81
|
-
this.eventLoopDelay = 0;
|
|
82
|
-
this.queue = new DoubleLinkedList();
|
|
83
|
-
const { limit, queue, maxEventLoopDelay, error } = options;
|
|
84
|
-
this.activeRequestLimit = limit;
|
|
85
|
-
this.minimalActiveRequestLimit = Math.round(limit / 3);
|
|
86
|
-
this.queueLimit = queue;
|
|
87
|
-
this.error = error;
|
|
88
|
-
this.maxEventLoopDelay = maxEventLoopDelay;
|
|
89
|
-
this.eventLoopHistogram = monitorEventLoopDelay({ resolution });
|
|
90
|
-
this.eventLoopHistogram.enable();
|
|
91
|
-
const timer = setInterval(() => this.nextTick(), 1000);
|
|
92
|
-
timer.unref();
|
|
93
|
-
}
|
|
94
|
-
onResponse() {
|
|
95
|
-
this.currentActive--;
|
|
96
|
-
this.loop();
|
|
97
|
-
}
|
|
98
|
-
// General idea is change limits every second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
|
|
99
|
-
nextTick() {
|
|
100
|
-
this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
|
|
101
|
-
if (Number.isNaN(this.eventLoopDelay))
|
|
102
|
-
this.eventLoopDelay = Infinity;
|
|
103
|
-
this.eventLoopHistogram.reset();
|
|
104
|
-
if (this.currentActive >= this.activeRequestLimit) {
|
|
105
|
-
if (this.eventLoopDelay <= this.maxEventLoopDelay) {
|
|
106
|
-
this.activeRequestLimit++;
|
|
107
|
-
// We need to have minimalActiveRequestLimit
|
|
108
|
-
}
|
|
109
|
-
else if (this.activeRequestLimit > this.minimalActiveRequestLimit) {
|
|
110
|
-
this.activeRequestLimit--;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
add(request) {
|
|
115
|
-
if (this.currentActive < this.activeRequestLimit) {
|
|
116
|
-
this.run(request);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (this.queue.length >= this.queueLimit) {
|
|
120
|
-
const lastNode = this.queue.shift();
|
|
121
|
-
lastNode.next(new HttpError(this.error));
|
|
122
|
-
}
|
|
123
|
-
this.queue.push(request);
|
|
124
|
-
}
|
|
125
|
-
loop() {
|
|
126
|
-
while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
|
|
127
|
-
// better if we start with new requests. Because more opportunity to answer before client cancel request
|
|
128
|
-
this.run(this.queue.pop());
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
run({ next, res }) {
|
|
132
|
-
this.currentActive++;
|
|
133
|
-
// onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
|
|
134
|
-
// fastify: it only works this way with on-finished, as using hook `onRequest` is not enough to handle all of the requests
|
|
135
|
-
// and some of the failed requests are getting disappeared
|
|
136
|
-
// and other hooks useless as well. [related issue](https://github.com/fastify/fastify/issues/1352)
|
|
137
|
-
onFinished(res.raw, () => {
|
|
138
|
-
this.onResponse();
|
|
139
|
-
});
|
|
140
|
-
next();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const fastifyRequestsLimiter = fp(async (fastify, { requestsLimiter }) => {
|
|
144
|
-
fastify.addHook('onRequest', (req, res, next) => {
|
|
145
|
-
requestsLimiter.add({ req, res, next });
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const REQUESTS_LIMITER_TOKEN = createToken('requestsLimiterToken');
|
|
150
|
-
const REQUESTS_LIMITER_ACTIVATE_TOKEN = createToken('requestsLimiterActivateToken');
|
|
151
|
-
const REQUESTS_LIMITER_OPTIONS_TOKEN = createToken('requestsLimiterOptionsToken');
|
|
5
|
+
import { DEFAULT_OPTIONS, RequestLimiter, fastifyRequestsLimiter } from './requestLimiter.es.js';
|
|
6
|
+
export { DEFAULT_OPTIONS, RequestLimiter, fastifyRequestsLimiter } from './requestLimiter.es.js';
|
|
7
|
+
import { REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN, REQUESTS_LIMITER_ACTIVATE_TOKEN } from './tokens.es.js';
|
|
8
|
+
export { REQUESTS_LIMITER_ACTIVATE_TOKEN, REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN } from './tokens.es.js';
|
|
152
9
|
|
|
153
10
|
let RequestLimiterModule = class RequestLimiterModule {
|
|
154
11
|
};
|
|
@@ -234,4 +91,4 @@ RequestLimiterModule = __decorate([
|
|
|
234
91
|
})
|
|
235
92
|
], RequestLimiterModule);
|
|
236
93
|
|
|
237
|
-
export {
|
|
94
|
+
export { RequestLimiterModule };
|
package/lib/server.js
CHANGED
|
@@ -6,158 +6,8 @@ var tslib = require('tslib');
|
|
|
6
6
|
var core = require('@tramvai/core');
|
|
7
7
|
var tokensCommon = require('@tramvai/tokens-common');
|
|
8
8
|
var tokensServerPrivate = require('@tramvai/tokens-server-private');
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var onFinished = require('on-finished');
|
|
12
|
-
var perf_hooks = require('perf_hooks');
|
|
13
|
-
var dippy = require('@tinkoff/dippy');
|
|
14
|
-
|
|
15
|
-
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
16
|
-
|
|
17
|
-
var fp__default = /*#__PURE__*/_interopDefaultLegacy(fp);
|
|
18
|
-
var onFinished__default = /*#__PURE__*/_interopDefaultLegacy(onFinished);
|
|
19
|
-
|
|
20
|
-
class DoubleLinkedList {
|
|
21
|
-
constructor() {
|
|
22
|
-
this.length = 0;
|
|
23
|
-
this.start = null;
|
|
24
|
-
this.end = null;
|
|
25
|
-
}
|
|
26
|
-
push(value) {
|
|
27
|
-
const newNode = {
|
|
28
|
-
value,
|
|
29
|
-
next: null,
|
|
30
|
-
prev: null,
|
|
31
|
-
};
|
|
32
|
-
this.length++;
|
|
33
|
-
if (this.start === null) {
|
|
34
|
-
this.start = newNode;
|
|
35
|
-
this.end = newNode;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const currentEnd = this.end;
|
|
39
|
-
this.end = newNode;
|
|
40
|
-
this.end.prev = currentEnd;
|
|
41
|
-
currentEnd.next = this.end;
|
|
42
|
-
}
|
|
43
|
-
pop() {
|
|
44
|
-
if (this.end === null) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
this.length--;
|
|
48
|
-
// if equal we have only 1 node, so we just remove start
|
|
49
|
-
if (this.end === this.start) {
|
|
50
|
-
this.start = null;
|
|
51
|
-
}
|
|
52
|
-
const { value } = this.end;
|
|
53
|
-
this.end = this.end.prev;
|
|
54
|
-
if (this.end) {
|
|
55
|
-
this.end.next = null;
|
|
56
|
-
}
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
shift() {
|
|
60
|
-
if (this.start === null) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
this.length--;
|
|
64
|
-
// if equal we have only 1 node, so we just remove end
|
|
65
|
-
if (this.end === this.start) {
|
|
66
|
-
this.end = null;
|
|
67
|
-
}
|
|
68
|
-
const { value } = this.start;
|
|
69
|
-
this.start = this.start.next;
|
|
70
|
-
if (this.start) {
|
|
71
|
-
this.start.prev = null;
|
|
72
|
-
}
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
size() {
|
|
76
|
-
return this.length;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const DEFAULT_OPTIONS = {
|
|
81
|
-
limit: 10,
|
|
82
|
-
queue: 100,
|
|
83
|
-
maxEventLoopDelay: 150,
|
|
84
|
-
error: { httpStatus: 429, message: 'Too Many Requests' },
|
|
85
|
-
};
|
|
86
|
-
const resolution = 10;
|
|
87
|
-
class RequestLimiter {
|
|
88
|
-
constructor(options) {
|
|
89
|
-
this.currentActive = 0;
|
|
90
|
-
this.eventLoopDelay = 0;
|
|
91
|
-
this.queue = new DoubleLinkedList();
|
|
92
|
-
const { limit, queue, maxEventLoopDelay, error } = options;
|
|
93
|
-
this.activeRequestLimit = limit;
|
|
94
|
-
this.minimalActiveRequestLimit = Math.round(limit / 3);
|
|
95
|
-
this.queueLimit = queue;
|
|
96
|
-
this.error = error;
|
|
97
|
-
this.maxEventLoopDelay = maxEventLoopDelay;
|
|
98
|
-
this.eventLoopHistogram = perf_hooks.monitorEventLoopDelay({ resolution });
|
|
99
|
-
this.eventLoopHistogram.enable();
|
|
100
|
-
const timer = setInterval(() => this.nextTick(), 1000);
|
|
101
|
-
timer.unref();
|
|
102
|
-
}
|
|
103
|
-
onResponse() {
|
|
104
|
-
this.currentActive--;
|
|
105
|
-
this.loop();
|
|
106
|
-
}
|
|
107
|
-
// General idea is change limits every second. Because if DDOS was happened we need some time to get problem with event loop. And better if we slowly adapt
|
|
108
|
-
nextTick() {
|
|
109
|
-
this.eventLoopDelay = Math.max(0, this.eventLoopHistogram.mean / 1e6 - resolution);
|
|
110
|
-
if (Number.isNaN(this.eventLoopDelay))
|
|
111
|
-
this.eventLoopDelay = Infinity;
|
|
112
|
-
this.eventLoopHistogram.reset();
|
|
113
|
-
if (this.currentActive >= this.activeRequestLimit) {
|
|
114
|
-
if (this.eventLoopDelay <= this.maxEventLoopDelay) {
|
|
115
|
-
this.activeRequestLimit++;
|
|
116
|
-
// We need to have minimalActiveRequestLimit
|
|
117
|
-
}
|
|
118
|
-
else if (this.activeRequestLimit > this.minimalActiveRequestLimit) {
|
|
119
|
-
this.activeRequestLimit--;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
add(request) {
|
|
124
|
-
if (this.currentActive < this.activeRequestLimit) {
|
|
125
|
-
this.run(request);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (this.queue.length >= this.queueLimit) {
|
|
129
|
-
const lastNode = this.queue.shift();
|
|
130
|
-
lastNode.next(new errors.HttpError(this.error));
|
|
131
|
-
}
|
|
132
|
-
this.queue.push(request);
|
|
133
|
-
}
|
|
134
|
-
loop() {
|
|
135
|
-
while (this.queue.length > 0 && this.currentActive < this.activeRequestLimit) {
|
|
136
|
-
// better if we start with new requests. Because more opportunity to answer before client cancel request
|
|
137
|
-
this.run(this.queue.pop());
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
run({ next, res }) {
|
|
141
|
-
this.currentActive++;
|
|
142
|
-
// onFinished doesn't work OK in DEV mode. Just stuck with high load without any reasons
|
|
143
|
-
// fastify: it only works this way with on-finished, as using hook `onRequest` is not enough to handle all of the requests
|
|
144
|
-
// and some of the failed requests are getting disappeared
|
|
145
|
-
// and other hooks useless as well. [related issue](https://github.com/fastify/fastify/issues/1352)
|
|
146
|
-
onFinished__default["default"](res.raw, () => {
|
|
147
|
-
this.onResponse();
|
|
148
|
-
});
|
|
149
|
-
next();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const fastifyRequestsLimiter = fp__default["default"](async (fastify, { requestsLimiter }) => {
|
|
153
|
-
fastify.addHook('onRequest', (req, res, next) => {
|
|
154
|
-
requestsLimiter.add({ req, res, next });
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const REQUESTS_LIMITER_TOKEN = dippy.createToken('requestsLimiterToken');
|
|
159
|
-
const REQUESTS_LIMITER_ACTIVATE_TOKEN = dippy.createToken('requestsLimiterActivateToken');
|
|
160
|
-
const REQUESTS_LIMITER_OPTIONS_TOKEN = dippy.createToken('requestsLimiterOptionsToken');
|
|
9
|
+
var requestLimiter = require('./requestLimiter.js');
|
|
10
|
+
var tokens = require('./tokens.js');
|
|
161
11
|
|
|
162
12
|
exports.RequestLimiterModule = class RequestLimiterModule {
|
|
163
13
|
};
|
|
@@ -171,22 +21,22 @@ exports.RequestLimiterModule = tslib.__decorate([
|
|
|
171
21
|
{
|
|
172
22
|
key: 'REQUEST_LIMITER_MELD',
|
|
173
23
|
optional: true,
|
|
174
|
-
value: String(DEFAULT_OPTIONS.maxEventLoopDelay),
|
|
24
|
+
value: String(requestLimiter.DEFAULT_OPTIONS.maxEventLoopDelay),
|
|
175
25
|
},
|
|
176
26
|
{
|
|
177
27
|
key: 'REQUEST_LIMITER_QUEUE',
|
|
178
28
|
optional: true,
|
|
179
|
-
value: String(DEFAULT_OPTIONS.queue),
|
|
29
|
+
value: String(requestLimiter.DEFAULT_OPTIONS.queue),
|
|
180
30
|
},
|
|
181
31
|
{
|
|
182
32
|
key: 'REQUEST_LIMITER_LIMIT',
|
|
183
33
|
optional: true,
|
|
184
|
-
value: String(DEFAULT_OPTIONS.limit),
|
|
34
|
+
value: String(requestLimiter.DEFAULT_OPTIONS.limit),
|
|
185
35
|
},
|
|
186
36
|
],
|
|
187
37
|
}),
|
|
188
38
|
core.provide({
|
|
189
|
-
provide: REQUESTS_LIMITER_OPTIONS_TOKEN,
|
|
39
|
+
provide: tokens.REQUESTS_LIMITER_OPTIONS_TOKEN,
|
|
190
40
|
useFactory: ({ envManager }) => ({
|
|
191
41
|
maxEventLoopDelay: Number(envManager.get('REQUEST_LIMITER_MELD')),
|
|
192
42
|
queue: Number(envManager.get('REQUEST_LIMITER_QUEUE')),
|
|
@@ -197,7 +47,7 @@ exports.RequestLimiterModule = tslib.__decorate([
|
|
|
197
47
|
},
|
|
198
48
|
}),
|
|
199
49
|
core.provide({
|
|
200
|
-
provide: REQUESTS_LIMITER_TOKEN,
|
|
50
|
+
provide: tokens.REQUESTS_LIMITER_TOKEN,
|
|
201
51
|
scope: core.Scope.SINGLETON,
|
|
202
52
|
useFactory: ({ options, featureEnable, envManager }) => {
|
|
203
53
|
var _a, _b, _c, _d;
|
|
@@ -208,18 +58,18 @@ exports.RequestLimiterModule = tslib.__decorate([
|
|
|
208
58
|
const queue = envManager.get('REQUEST_LIMITER_QUEUE');
|
|
209
59
|
const limit = envManager.get('REQUEST_LIMITER_LIMIT');
|
|
210
60
|
const resultOptions = {
|
|
211
|
-
limit: limit ? Number(limit) : (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : DEFAULT_OPTIONS.limit,
|
|
212
|
-
queue: queue ? Number(queue) : (_b = options === null || options === void 0 ? void 0 : options.queue) !== null && _b !== void 0 ? _b : DEFAULT_OPTIONS.queue,
|
|
61
|
+
limit: limit ? Number(limit) : (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : requestLimiter.DEFAULT_OPTIONS.limit,
|
|
62
|
+
queue: queue ? Number(queue) : (_b = options === null || options === void 0 ? void 0 : options.queue) !== null && _b !== void 0 ? _b : requestLimiter.DEFAULT_OPTIONS.queue,
|
|
213
63
|
maxEventLoopDelay: meld
|
|
214
64
|
? Number(meld)
|
|
215
|
-
: (_c = options === null || options === void 0 ? void 0 : options.maxEventLoopDelay) !== null && _c !== void 0 ? _c : DEFAULT_OPTIONS.maxEventLoopDelay,
|
|
216
|
-
error: (_d = options === null || options === void 0 ? void 0 : options.error) !== null && _d !== void 0 ? _d : DEFAULT_OPTIONS.error,
|
|
65
|
+
: (_c = options === null || options === void 0 ? void 0 : options.maxEventLoopDelay) !== null && _c !== void 0 ? _c : requestLimiter.DEFAULT_OPTIONS.maxEventLoopDelay,
|
|
66
|
+
error: (_d = options === null || options === void 0 ? void 0 : options.error) !== null && _d !== void 0 ? _d : requestLimiter.DEFAULT_OPTIONS.error,
|
|
217
67
|
};
|
|
218
|
-
return new RequestLimiter(resultOptions);
|
|
68
|
+
return new requestLimiter.RequestLimiter(resultOptions);
|
|
219
69
|
},
|
|
220
70
|
deps: {
|
|
221
|
-
options: { token: REQUESTS_LIMITER_OPTIONS_TOKEN, optional: true },
|
|
222
|
-
featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
|
|
71
|
+
options: { token: tokens.REQUESTS_LIMITER_OPTIONS_TOKEN, optional: true },
|
|
72
|
+
featureEnable: { token: tokens.REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
|
|
223
73
|
envManager: tokensCommon.ENV_MANAGER_TOKEN,
|
|
224
74
|
},
|
|
225
75
|
}),
|
|
@@ -231,21 +81,21 @@ exports.RequestLimiterModule = tslib.__decorate([
|
|
|
231
81
|
if (featureEnable === false) {
|
|
232
82
|
return;
|
|
233
83
|
}
|
|
234
|
-
await fastifyRequestsLimiter(app, { requestsLimiter });
|
|
84
|
+
await requestLimiter.fastifyRequestsLimiter(app, { requestsLimiter });
|
|
235
85
|
};
|
|
236
86
|
},
|
|
237
87
|
deps: {
|
|
238
|
-
requestsLimiter: REQUESTS_LIMITER_TOKEN,
|
|
239
|
-
featureEnable: { token: REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
|
|
88
|
+
requestsLimiter: tokens.REQUESTS_LIMITER_TOKEN,
|
|
89
|
+
featureEnable: { token: tokens.REQUESTS_LIMITER_ACTIVATE_TOKEN, optional: true },
|
|
240
90
|
},
|
|
241
91
|
}),
|
|
242
92
|
],
|
|
243
93
|
})
|
|
244
94
|
], exports.RequestLimiterModule);
|
|
245
95
|
|
|
246
|
-
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
247
|
-
exports.
|
|
248
|
-
exports.
|
|
249
|
-
exports.
|
|
250
|
-
exports.
|
|
251
|
-
exports.
|
|
96
|
+
exports.DEFAULT_OPTIONS = requestLimiter.DEFAULT_OPTIONS;
|
|
97
|
+
exports.RequestLimiter = requestLimiter.RequestLimiter;
|
|
98
|
+
exports.fastifyRequestsLimiter = requestLimiter.fastifyRequestsLimiter;
|
|
99
|
+
exports.REQUESTS_LIMITER_ACTIVATE_TOKEN = tokens.REQUESTS_LIMITER_ACTIVATE_TOKEN;
|
|
100
|
+
exports.REQUESTS_LIMITER_OPTIONS_TOKEN = tokens.REQUESTS_LIMITER_OPTIONS_TOKEN;
|
|
101
|
+
exports.REQUESTS_LIMITER_TOKEN = tokens.REQUESTS_LIMITER_TOKEN;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createToken } from '@tinkoff/dippy';
|
|
2
|
+
|
|
3
|
+
const REQUESTS_LIMITER_TOKEN = createToken('requestsLimiterToken');
|
|
4
|
+
const REQUESTS_LIMITER_ACTIVATE_TOKEN = createToken('requestsLimiterActivateToken');
|
|
5
|
+
const REQUESTS_LIMITER_OPTIONS_TOKEN = createToken('requestsLimiterOptionsToken');
|
|
6
|
+
|
|
7
|
+
export { REQUESTS_LIMITER_ACTIVATE_TOKEN, REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN };
|
package/lib/tokens.es.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createToken } from '@tinkoff/dippy';
|
|
2
|
+
|
|
3
|
+
const REQUESTS_LIMITER_TOKEN = createToken('requestsLimiterToken');
|
|
4
|
+
const REQUESTS_LIMITER_ACTIVATE_TOKEN = createToken('requestsLimiterActivateToken');
|
|
5
|
+
const REQUESTS_LIMITER_OPTIONS_TOKEN = createToken('requestsLimiterOptionsToken');
|
|
6
|
+
|
|
7
|
+
export { REQUESTS_LIMITER_ACTIVATE_TOKEN, REQUESTS_LIMITER_OPTIONS_TOKEN, REQUESTS_LIMITER_TOKEN };
|
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var dippy = require('@tinkoff/dippy');
|
|
6
|
+
|
|
7
|
+
const REQUESTS_LIMITER_TOKEN = dippy.createToken('requestsLimiterToken');
|
|
8
|
+
const REQUESTS_LIMITER_ACTIVATE_TOKEN = dippy.createToken('requestsLimiterActivateToken');
|
|
9
|
+
const REQUESTS_LIMITER_OPTIONS_TOKEN = dippy.createToken('requestsLimiterOptionsToken');
|
|
10
|
+
|
|
11
|
+
exports.REQUESTS_LIMITER_ACTIVATE_TOKEN = REQUESTS_LIMITER_ACTIVATE_TOKEN;
|
|
12
|
+
exports.REQUESTS_LIMITER_OPTIONS_TOKEN = REQUESTS_LIMITER_OPTIONS_TOKEN;
|
|
13
|
+
exports.REQUESTS_LIMITER_TOKEN = REQUESTS_LIMITER_TOKEN;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
class DoubleLinkedList {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.length = 0;
|
|
4
|
+
this.start = null;
|
|
5
|
+
this.end = null;
|
|
6
|
+
}
|
|
7
|
+
push(value) {
|
|
8
|
+
const newNode = {
|
|
9
|
+
value,
|
|
10
|
+
next: null,
|
|
11
|
+
prev: null,
|
|
12
|
+
};
|
|
13
|
+
this.length++;
|
|
14
|
+
if (this.start === null) {
|
|
15
|
+
this.start = newNode;
|
|
16
|
+
this.end = newNode;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const currentEnd = this.end;
|
|
20
|
+
this.end = newNode;
|
|
21
|
+
this.end.prev = currentEnd;
|
|
22
|
+
currentEnd.next = this.end;
|
|
23
|
+
}
|
|
24
|
+
pop() {
|
|
25
|
+
if (this.end === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
this.length--;
|
|
29
|
+
// if equal we have only 1 node, so we just remove start
|
|
30
|
+
if (this.end === this.start) {
|
|
31
|
+
this.start = null;
|
|
32
|
+
}
|
|
33
|
+
const { value } = this.end;
|
|
34
|
+
this.end = this.end.prev;
|
|
35
|
+
if (this.end) {
|
|
36
|
+
this.end.next = null;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
shift() {
|
|
41
|
+
if (this.start === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
this.length--;
|
|
45
|
+
// if equal we have only 1 node, so we just remove end
|
|
46
|
+
if (this.end === this.start) {
|
|
47
|
+
this.end = null;
|
|
48
|
+
}
|
|
49
|
+
const { value } = this.start;
|
|
50
|
+
this.start = this.start.next;
|
|
51
|
+
if (this.start) {
|
|
52
|
+
this.start.prev = null;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
size() {
|
|
57
|
+
return this.length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { DoubleLinkedList };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
class DoubleLinkedList {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.length = 0;
|
|
8
|
+
this.start = null;
|
|
9
|
+
this.end = null;
|
|
10
|
+
}
|
|
11
|
+
push(value) {
|
|
12
|
+
const newNode = {
|
|
13
|
+
value,
|
|
14
|
+
next: null,
|
|
15
|
+
prev: null,
|
|
16
|
+
};
|
|
17
|
+
this.length++;
|
|
18
|
+
if (this.start === null) {
|
|
19
|
+
this.start = newNode;
|
|
20
|
+
this.end = newNode;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const currentEnd = this.end;
|
|
24
|
+
this.end = newNode;
|
|
25
|
+
this.end.prev = currentEnd;
|
|
26
|
+
currentEnd.next = this.end;
|
|
27
|
+
}
|
|
28
|
+
pop() {
|
|
29
|
+
if (this.end === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
this.length--;
|
|
33
|
+
// if equal we have only 1 node, so we just remove start
|
|
34
|
+
if (this.end === this.start) {
|
|
35
|
+
this.start = null;
|
|
36
|
+
}
|
|
37
|
+
const { value } = this.end;
|
|
38
|
+
this.end = this.end.prev;
|
|
39
|
+
if (this.end) {
|
|
40
|
+
this.end.next = null;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
shift() {
|
|
45
|
+
if (this.start === null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
this.length--;
|
|
49
|
+
// if equal we have only 1 node, so we just remove end
|
|
50
|
+
if (this.end === this.start) {
|
|
51
|
+
this.end = null;
|
|
52
|
+
}
|
|
53
|
+
const { value } = this.start;
|
|
54
|
+
this.start = this.start.next;
|
|
55
|
+
if (this.start) {
|
|
56
|
+
this.start.prev = null;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
size() {
|
|
61
|
+
return this.length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
exports.DoubleLinkedList = DoubleLinkedList;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tramvai/module-request-limiter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.0",
|
|
4
4
|
"description": "Enable different rendering modes for pages",
|
|
5
5
|
"main": "lib/server.js",
|
|
6
6
|
"module": "lib/server.es.js",
|
|
@@ -16,24 +16,23 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tramvai-build --
|
|
20
|
-
"watch": "tsc -w"
|
|
21
|
-
"build-for-publish": "true"
|
|
19
|
+
"build": "tramvai-build --forPublish --preserveModules",
|
|
20
|
+
"watch": "tsc -w"
|
|
22
21
|
},
|
|
23
22
|
"publishConfig": {
|
|
24
23
|
"registry": "https://registry.npmjs.org/"
|
|
25
24
|
},
|
|
26
25
|
"dependencies": {
|
|
27
|
-
"@tinkoff/errors": "0.3.
|
|
26
|
+
"@tinkoff/errors": "0.3.6",
|
|
28
27
|
"fastify-plugin": "^4.2.1",
|
|
29
28
|
"on-finished": "^2.3.0"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {},
|
|
32
31
|
"peerDependencies": {
|
|
33
|
-
"@tinkoff/dippy": "0.8.
|
|
34
|
-
"@tramvai/core": "2.
|
|
35
|
-
"@tramvai/tokens-common": "2.
|
|
36
|
-
"@tramvai/tokens-server-private": "2.
|
|
32
|
+
"@tinkoff/dippy": "0.8.13",
|
|
33
|
+
"@tramvai/core": "2.72.0",
|
|
34
|
+
"@tramvai/tokens-common": "2.72.0",
|
|
35
|
+
"@tramvai/tokens-server-private": "2.72.0",
|
|
37
36
|
"fastify": "^4.6.0",
|
|
38
37
|
"tslib": "^2.4.0"
|
|
39
38
|
}
|