connect-qos
Version:
Connect middleware that helps maintain a high quality of service during heavy traffic
184 lines (183 loc) • 7.46 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "ConnectQOS", {
enumerable: true,
get: function() {
return ConnectQOS;
}
});
const _toobusyjs = /*#__PURE__*/ _interop_require_default(require("toobusy-js"));
const _metrics = require("./metrics");
const _util = require("./util");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
class ConnectQOS {
constructor(opts){
const { minLag = 70, maxLag = 300, errorStatusCode = 503, errorResponseDelay = 0, httpBehindProxy = false, httpsBehindProxy = false, ...metricOptions } = opts || {};
this.#minLag = minLag;
this.#maxLag = maxLag;
this.#lagRange = maxLag - minLag;
this.#errorStatusCode = errorStatusCode;
this.#httpBehindProxy = httpBehindProxy;
this.#httpsBehindProxy = httpsBehindProxy;
this.#errorResponseDelay = errorResponseDelay;
// we only require `toobusy.lag` feature and can ignore toobusy() via maxLag
// toobusy.maxLag(this.#maxLag);
this.#metrics = new _metrics.Metrics(metricOptions);
this.#hostRateRange = this.#metrics.maxHostRate - this.#metrics.minHostRate;
this.#ipRateRange = this.#metrics.maxIpRate - this.#metrics.minIpRate;
}
#minLag;
#maxLag;
#lagRange;
#hostRateRange;
#ipRateRange;
#errorStatusCode;
#httpBehindProxy;
#httpsBehindProxy;
#errorResponseDelay;
#metrics;
get minLag() {
return this.#minLag;
}
get maxLag() {
return this.#maxLag;
}
get errorStatusCode() {
return this.#errorStatusCode;
}
get httpBehindProxy() {
return this.#httpBehindProxy;
}
get httpsBehindProxy() {
return this.#httpsBehindProxy;
}
get metrics() {
return this.#metrics;
}
getMiddleware({ beforeThrottle, destroySocket = true } = {}) {
const self = this;
function sendError(res) {
res.statusCode = self.#errorStatusCode;
res.end();
if (destroySocket) {
if (res.stream) {
res.stream.session?.destroy();
} else if (res.socket?.destroyed === false) {
res.socket.destroySoon();
}
}
}
return function QOSMiddleware(req, res, next) {
const reason = self.shouldThrottleRequest(req);
if (reason) {
if (!beforeThrottle || beforeThrottle(self, req, reason) !== false) {
// if no throttle handler OR the throttle handler does not explicitly reject, do it
return void self.#errorResponseDelay ? setTimeout(sendError, self.#errorResponseDelay, res).unref() : sendError(res);
}
}
// continue
next();
};
}
shouldThrottleRequest(req) {
const host = this.resolveHost(req);
const hostStatus = this.getHostStatus(host, false); // defer tracking
const ipStatus = this.getIpStatus(req, false); // defer tracking
// never throttle whitelisted actors
if (hostStatus === _metrics.ActorStatus.Whitelisted || ipStatus === _metrics.ActorStatus.Whitelisted) return false;
if (hostStatus === _metrics.ActorStatus.Bad) return _metrics.BadActorType.badHost;
if (ipStatus === _metrics.ActorStatus.Bad) return _metrics.BadActorType.badIp;
// If host is exceeding host ratio and IP rate override is either not set or exceeded, return hostViolation status
const maxIpRateHostViolation = this.#metrics.maxIpRateHostViolation;
if (this.metrics.hostRatioViolations.has(host) && (!maxIpRateHostViolation || this.getIpStatus(req, false, Math.max(0, maxIpRateHostViolation - this.#metrics.minIpRate)) === _metrics.ActorStatus.Bad)) {
return _metrics.BadActorType.hostViolation;
}
// only track if NOT throttling
this.trackRequest(req);
// do not throttle user
return false;
}
get lag() {
return _toobusyjs.default.lag();
}
get lagRatio() {
// lagRatio = 0-1
const lag = _toobusyjs.default.lag();
// if lag exceeds maxLag will cap ratio at 1
return lag > this.#minLag ? Math.min(1, (lag - this.#minLag) / this.#lagRange) : 0;
}
resolveHost(source) {
return typeof source === 'string' ? (0, _util.normalizeHost)(source) : (0, _util.resolveHostFromRequest)(source);
}
getHostStatus(source, track = true) {
const sourceStr = this.resolveHost(source);
const sourceInfo = this.#metrics.getHostInfo(sourceStr);
const status = getStatus(sourceInfo, {
minRate: this.#metrics.minHostRate,
rateRange: this.#hostRateRange,
lagRatio: this.lagRatio
});
if (sourceInfo === _metrics.ActorStatus.Whitelisted) return _metrics.ActorStatus.Whitelisted;
if (track && status === _metrics.ActorStatus.Good) {
// only track if we're NOT throttling
// forward cache to avoid additional lookup
this.#metrics.trackHost(sourceStr, sourceInfo);
}
return status;
}
isBadHost(host, track = true) {
return this.getHostStatus(host, track) === _metrics.ActorStatus.Bad;
}
resolveIp(source) {
return typeof source === 'string' ? source : (0, _util.resolveIpFromRequest)(source, source.scheme === 'https' ? this.#httpsBehindProxy : this.#httpBehindProxy);
}
getIpStatus(source, track = true, rateRange = this.#ipRateRange) {
const sourceStr = this.resolveIp(source);
const sourceInfo = this.#metrics.getIpInfo(sourceStr);
const status = getStatus(sourceInfo, {
minRate: this.#metrics.minIpRate,
lagRatio: this.lagRatio,
rateRange
});
if (track && status === _metrics.ActorStatus.Good) {
// only track if we're NOT throttling
// forward cache to avoid additional lookup
this.#metrics.trackIp(sourceStr, sourceInfo);
}
return status;
}
isBadIp(ip, track = true) {
return this.getIpStatus(ip, track) === _metrics.ActorStatus.Bad;
}
trackRequest(req) {
const host = this.resolveHost(req);
this.#metrics.trackHost(host);
const ip = this.resolveIp(req);
this.#metrics.trackIp(ip);
}
}
function getStatus(sourceInfo, { minRate, rateRange, lagRatio }) {
if (sourceInfo === _metrics.ActorStatus.Whitelisted) return _metrics.ActorStatus.Whitelisted;
// if no history OR rate limiting disabled assume it's good
if (!sourceInfo || !minRate) return _metrics.ActorStatus.Good;
// lagRatio = 0-1 (min-max)
// minRate = 10
// maxRate = 30
// rateRange = (maxRate - minRate) = 20
// ((1-0.00) * 20) + 10 = 30 // minLag RPS
// ((1-0.25) * 20) + 10 = 25
// ((1-0.50) * 20) + 10 = 20
// ((1-0.75) * 20) + 10 = 15
// ((1-1.00) * 20) + 10 = 10 // maxLag RPS
const dynamicRate = (1 - lagRatio) * rateRange + minRate;
// min/max not required since lagRatio is guaranteed
// to be 0-1 regardless of (under|over)flow
return sourceInfo.rate > dynamicRate ? _metrics.ActorStatus.Bad : _metrics.ActorStatus.Good;
}
//# sourceMappingURL=connect.js.map