Add Pressure-based rate limiter (#17873)

* Start setting up @directus/pressure

* Build pressure middleware

* Add basic readme

* Install @directus/pressure

* Fix this binding

* Experiment

* Add defaults util

* Cleanup

* Fix export

* Use directus defaults

* Start tests

* Add random-utils package

* Finish testing for monitor

* Add prod deployment

* Stop building test files in prod

* My favorite

* Integrate pressure handler

* Add decent defaults

* Add retry header + custom error support

* Clean-up merge conflict & sort imports

* Fix build

* Remove default value for retry after

* Verify sampleInterval value

* ran eslint

* updated package lock

* updated vitest

* Create slimy-zebras-jam.md

* Added basic docs for config options

* updated pnpm lock and changeset

* Update & align new packages

* Update .changeset/slimy-zebras-jam.md

---------

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
Co-authored-by: Brainslug <tim@brainslug.nl>
This commit is contained in:
Rijk van Zanten
2023-05-10 10:17:53 -04:00
committed by GitHub
parent 3737d59c02
commit b56fc107a5
23 changed files with 597 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
import { handlePressure } from '@directus/pressure';
import cookieParser from 'cookie-parser';
import type { Request, RequestHandler, Response } from 'express';
import express from 'express';
@@ -45,6 +46,7 @@ import {
import emitter from './emitter.js';
import env from './env.js';
import { InvalidPayloadException } from './exceptions/invalid-payload.js';
import { ServiceUnavailableException } from './exceptions/service-unavailable.js';
import { getExtensionManager } from './extensions.js';
import { getFlowManager } from './flows.js';
import logger, { expressLogger } from './logger.js';
@@ -105,6 +107,26 @@ export default async function createApp(): Promise<express.Application> {
app.set('trust proxy', env['IP_TRUST_PROXY']);
app.set('query parser', (str: string) => qs.parse(str, { depth: 10 }));
if (env['PRESSURE_LIMITER_ENABLED']) {
const sampleInterval = Number(env['PRESSURE_LIMITER_SAMPLE_INTERVAL']);
if (Number.isNaN(sampleInterval) === true || Number.isFinite(sampleInterval) === false) {
throw new Error(`Invalid value for PRESSURE_LIMITER_SAMPLE_INTERVAL environment variable`);
}
app.use(
handlePressure({
sampleInterval,
maxEventLoopUtilization: env['PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION'],
maxEventLoopDelay: env['PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY'],
maxMemoryRss: env['PRESSURE_LIMITER_MAX_MEMORY_RSS'],
maxMemoryHeapUsed: env['PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED'],
error: new ServiceUnavailableException('Under pressure', { service: 'api' }),
retryAfter: env['PRESSURE_LIMITER_RETRY_AFTER'],
})
);
}
app.use(
helmet.contentSecurityPolicy(
merge(

View File

@@ -291,6 +291,14 @@ const defaults: Record<string, any> = {
FLOWS_EXEC_ALLOWED_MODULES: false,
FLOWS_ENV_ALLOW_LIST: false,
PRESSURE_LIMITER_ENABLED: true,
PRESSURE_LIMITER_SAMPLE_INTERVAL: 250,
PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION: 0.99,
PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY: 500,
PRESSURE_LIMITER_MAX_MEMORY_RSS: false,
PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED: false,
PRESSURE_LIMITER_RETRY_AFTER: false,
};
// Allows us to force certain environment variable into a type, instead of relying