-
Notifications
You must be signed in to change notification settings - Fork 255
Expand file tree
/
Copy pathconfig.js
More file actions
302 lines (286 loc) · 10.6 KB
/
config.js
File metadata and controls
302 lines (286 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
const Joi = require('@hapi/joi');
const { errors, ArsenalError } = require('arsenal');
const { rateLimitDefaultConfigCacheTTL, rateLimitDefaultBurstCapacity } = require('../../../../constants');
/**
* Full Rate Limiting Configuration Example
*
* Add this to your config.json to enable rate limiting:
*
* {
* "rateLimiting": {
* // Required: Enable/disable rate limiting
* "enabled": true,
*
* // Required: Service user ARN (bypasses rate limits)
* "serviceUserArn": "arn:aws:iam::123456789012:root",
*
* // Optional: Number of CloudServer nodes in distributed deployment
* // Default: 1
* // Used to calculate per-worker rate limit: limit / nodes / workers
* "nodes": 1,
*
* // Optional: Bucket-level rate limiting configuration
* "bucket": {
* // Optional: Global default rate limit for all buckets
* // Limit can be overridden per-bucket via PutBucketRateLimit API
* "defaultConfig": {
* "requestsPerSecond": {
* // Required: Requests per second limit (0 = unlimited)
* "limit": 100,
*
* // Optional: Burst capacity (allows temporary spikes)
* // Default: 1 (no burst allowed)
* // Higher values allow more requests in quick succession
* "burstCapacity": 2
* }
* },
*
* // Optional: How long to cache rate limit configs (milliseconds)
* // Default: 30000 (30 seconds)
* // Higher values = better performance, slower config updates
* "configCacheTTL": 30000,
*
* // Optional: Default burst capacity for per-bucket configs
* // Default: 1
* "defaultBurstCapacity": 1
* },
*
* // Optional: Account-level rate limiting configuration
* "account": {
* // Optional: Global default rate limit for all accounts
* // Limit can be overridden per-account via UpdateAccountLimits API
* "defaultConfig": {
* "requestsPerSecond": {
* // Required: Requests per second limit (0 = unlimited)
* "limit": 100,
*
* // Optional: Burst capacity (allows temporary spikes)
* // Default: 1 (no burst allowed)
* // Higher values allow more requests in quick succession
* "burstCapacity": 2
* }
* },
*
* // Optional: How long to cache rate limit configs (milliseconds)
* // Default: 30000 (30 seconds)
* // Higher values = better performance, slower config updates
* "configCacheTTL": 30000,
*
* // Optional: Default burst capacity for per-account configs
* // Default: 1
* "defaultBurstCapacity": 1
* },
*
* // Optional: Custom error response for rate limited requests
* "error": {
* // Optional: HTTP status code (400-599)
* // Default: 503 (SlowDown)
* "statusCode": 429,
*
* // Optional: Error code
* // Default: "SlowDown"
* "code": "TooManyRequests",
*
* // Optional: Error message
* // Default: "Please reduce your request rate."
* "message": "Please reduce your request rate"
* }
*
* // Optional tuning parameters
* "tokenBucketBufferSize": 50,
* "tokenBucketRefillThreshold": 20,
*
* // NOTE: Token reservation refills happen automatically every 100ms.
* // No additional configuration needed for worker coordination.
* }
* }
*
* Minimal Configuration (uses defaults):
* {
* "rateLimiting": {
* "enabled": true,
* "serviceUserArn": "arn:aws:iam::123456789012:root"
* }
* }
*
* Complete Configuration (all options):
* {
* "rateLimiting": {
* "enabled": true,
* "serviceUserArn": "arn:aws:iam::123456789012:root",
* "nodes": 3,
* "bucket": {
* "defaultConfig": {
* "requestsPerSecond": {
* "limit": 1000,
* "burstCapacity": 5
* }
* },
* "configCacheTTL": 60000,
* },
* "error": {
* "statusCode": 429,
* "code": "TooManyRequests",
* "message": "Rate limit exceeded. Please try again later."
* }
* }
* }
*
* Priority Order:
* 1. Per-bucket config (set via PutBucketRateLimit API) - Highest priority
* 2. Global default config (bucket.defaultConfig) - Fallback
* 3. No rate limiting (null) - If neither is configured
*
* Token Reservation Architecture:
* - Workers request tokens in advance from Redis (not per-request)
* - Tokens are consumed locally (in-memory, fast)
* - Background job refills tokens every 100ms (async, non-blocking)
* - Redis enforces node-level quota using GCRA at token grant time
* - Busy workers automatically get more tokens (dynamic work-stealing)
* - Total limit divided across nodes: limit / nodes = per-node quota
* - Example: 1000 req/s with 10 nodes = 100 req/s per node
* - Workers on same node share the node quota dynamically
*
* Burst Capacity:
* - Allows temporary spikes above the sustained rate
* - Value of 1 = 1 second of burst (can send 1s worth of requests immediately)
* - Value of 2 = 2 seconds of burst (can send 2s worth of requests immediately)
* - Enforced atomically in Redis during token grants using GCRA
*/
/**
* RateLimitClassConfig
* @typedef {Object} RateLimitClassConfig
* @property {object} defaultConfig - Default config applied if no resource specific configuration is found.
* @property {number} configCacheTTL - Number of milliseconds to cache per resource configs
* @property {number} defaultBurstCapacity - Default used if resource does not specify a burst capacity.
*/
const rateLimitClassConfigSchema = Joi.object({
defaultConfig: Joi.object({
requestsPerSecond: Joi.object({
limit: Joi.number().integer().min(0).required(),
burstCapacity: Joi.number().positive(),
}),
}),
configCacheTTL: Joi.number().integer().positive().default(rateLimitDefaultConfigCacheTTL),
defaultBurstCapacity: Joi.number().positive().default(rateLimitDefaultBurstCapacity),
}).default({
defaultConfig: undefined,
configCacheTTL: rateLimitDefaultConfigCacheTTL,
defaultBurstCapacity: rateLimitDefaultBurstCapacity,
});
const rateLimitConfigSchema = Joi.object({
enabled: Joi.boolean().default(false),
serviceUserArn: Joi.string().required(),
nodes: Joi.number().integer().positive().default(1),
tokenBucketBufferSize: Joi.number().integer().positive().default(50),
tokenBucketRefillThreshold: Joi.number().integer().positive().default(20),
bucket: rateLimitClassConfigSchema,
account: rateLimitClassConfigSchema,
error: Joi.object({
statusCode: Joi.number().integer().min(400).max(599).default(503),
code: Joi.string().default(errors.SlowDown.message),
message: Joi.string().default(errors.SlowDown.description),
}).default({
statusCode: 503,
code: errors.SlowDown.message,
message: errors.SlowDown.description,
}),
}).default({
enabled: false,
nodes: 1,
tokenBucketBufferSize: 50,
tokenBucketRefillThreshold: 20,
bucket: {
configCacheTTL: rateLimitDefaultConfigCacheTTL,
defaultBurstCapacity: rateLimitDefaultBurstCapacity,
},
account: {
configCacheTTL: rateLimitDefaultConfigCacheTTL,
defaultBurstCapacity: rateLimitDefaultBurstCapacity,
},
error: {
statusCode: 503,
code: errors.SlowDown.message,
message: errors.SlowDown.description,
},
});
/**
* Transform validated class config (bucket or account) by calculating intervals
* and applying business logic
*
* @param {string} resourceClass - Rate limit class name ('bucket' or 'account')
* @param {object} validatedCfg - Already validated config from Joi
* @param {number} nodes - Number of instances that requests will be load balanced across
* @returns {RateLimitClassConfig} Transformed rate limit config
*/
function transformClassConfig(resourceClass, validatedCfg, nodes) {
const defaultConfig = {
RequestsPerSecond: {
BurstCapacity: validatedCfg.defaultBurstCapacity,
},
};
if (validatedCfg.defaultConfig?.requestsPerSecond) {
const { limit, burstCapacity } = validatedCfg.defaultConfig.requestsPerSecond;
// Validate limit against nodes (business rule)
if (limit > 0 && limit < nodes) {
throw new Error(
`rateLimiting.${resourceClass}.defaultConfig.` +
`requestsPerSecond.limit (${limit}) must be >= ` +
`nodes (${nodes}) ` +
'or 0 (unlimited). Each node enforces limit/nodes locally. ' +
`With limit < ${nodes}, per-node rate would be < 1 req/s, effectively blocking traffic.`
);
}
// Store both the original limit and the calculated values
defaultConfig.RequestsPerSecond = {
Limit: limit,
BurstCapacity: burstCapacity || validatedCfg.defaultBurstCapacity,
};
}
return {
defaultConfig,
configCacheTTL: validatedCfg.configCacheTTL,
defaultBurstCapacity: validatedCfg.defaultBurstCapacity,
};
}
/**
* Parse and validate the complete rate limiting configuration
*
* @param {Object} rateLimitingConfig - config.rateLimiting object from config.json
* @returns {Object} Fully parsed and validated rate limiting configuration
* @throws {Error} If configuration is invalid
*/
function parseRateLimitConfig(rateLimitingConfig) {
// Validate configuration using Joi schema
const { error: validationError, value: validated } = rateLimitConfigSchema.validate(
rateLimitingConfig,
{
abortEarly: false, // Return all validation errors at once
allowUnknown: false, // Don't allow key not present in schema
convert: false, // Don't do type coercion (e.g. "1" -> 1)
}
);
if (validationError) {
const details = validationError.details.map(d => d.message).join('; ');
throw new Error(`rateLimiting configuration is invalid: ${details}`);
}
// Initialize parsed config with validated values (including defaults)
const parsed = {
enabled: validated.enabled,
serviceUserArn: validated.serviceUserArn,
nodes: validated.nodes,
tokenBucketBufferSize: validated.tokenBucketBufferSize,
tokenBucketRefillThreshold: validated.tokenBucketRefillThreshold,
error: new ArsenalError(
validated.error.code,
validated.error.statusCode,
validated.error.message,
),
};
parsed.bucket = transformClassConfig('bucket', validated.bucket, parsed.nodes);
parsed.account = transformClassConfig('account', validated.account, parsed.nodes);
return parsed;
}
module.exports = {
parseRateLimitConfig,
};