connect-qos
Version:
Connect middleware that helps maintain a high quality of service during heavy traffic
264 lines (263 loc) • 9.14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
ActorStatus: function() {
return ActorStatus;
},
BadActorType: function() {
return BadActorType;
},
DEFAULT_HISTORY_SIZE: function() {
return DEFAULT_HISTORY_SIZE;
},
DEFAULT_HOST_WHITELIST: function() {
return DEFAULT_HOST_WHITELIST;
},
DEFAULT_IP_WHITELIST: function() {
return DEFAULT_IP_WHITELIST;
},
DEFAULT_MAX_AGE: function() {
return DEFAULT_MAX_AGE;
},
DEFAULT_MAX_HOST_RATE: function() {
return DEFAULT_MAX_HOST_RATE;
},
DEFAULT_MAX_HOST_RATIO: function() {
return DEFAULT_MAX_HOST_RATIO;
},
DEFAULT_MAX_IP_RATE: function() {
return DEFAULT_MAX_IP_RATE;
},
DEFAULT_MAX_IP_RATE_BUSY_HOST: function() {
return DEFAULT_MAX_IP_RATE_BUSY_HOST;
},
DEFAULT_MIN_HOST_RATE: function() {
return DEFAULT_MIN_HOST_RATE;
},
DEFAULT_MIN_IP_RATE: function() {
return DEFAULT_MIN_IP_RATE;
},
Metrics: function() {
return Metrics;
}
});
const _lrucache = /*#__PURE__*/ _interop_require_default(require("lru-cache"));
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var ActorStatus;
(function(ActorStatus) {
ActorStatus[ActorStatus["Good"] = 200] = "Good";
ActorStatus[ActorStatus["Whitelisted"] = 300] = "Whitelisted";
ActorStatus[ActorStatus["Bad"] = 400] = "Bad";
})(ActorStatus || (ActorStatus = {}));
var BadActorType;
(function(BadActorType) {
BadActorType["badHost"] = "badHost";
BadActorType["hostViolation"] = "hostViolation";
BadActorType["badIp"] = "badIp";
BadActorType["userLag"] = "userLag";
})(BadActorType || (BadActorType = {}));
const DEFAULT_HISTORY_SIZE = 200; // 0.5% hit rate enough to reside in LRU
const DEFAULT_MAX_AGE = 1000 * 10; // 10s is generally more than sufficient history
const DEFAULT_MIN_HOST_RATE = 20;
const DEFAULT_MAX_HOST_RATE = 40;
const DEFAULT_MAX_HOST_RATIO = 0; // disabled
const DEFAULT_MIN_IP_RATE = 0; // disabled
const DEFAULT_MAX_IP_RATE = 0; // disabled
const DEFAULT_MAX_IP_RATE_BUSY_HOST = 0; // disabled
const DEFAULT_HOST_WHITELIST = [
'localhost'
];
const DEFAULT_IP_WHITELIST = [];
class Metrics {
constructor(opts){
const { historySize = DEFAULT_HISTORY_SIZE, maxAge = DEFAULT_MAX_AGE, minHostRate = DEFAULT_MIN_HOST_RATE, maxHostRate = DEFAULT_MAX_HOST_RATE, maxHostRatio = DEFAULT_MAX_HOST_RATIO, minIpRate = DEFAULT_MIN_IP_RATE, maxIpRate = DEFAULT_MAX_IP_RATE, maxIpRateHostViolation = DEFAULT_MAX_IP_RATE_BUSY_HOST, hostWhitelist = new Set(DEFAULT_HOST_WHITELIST), ipWhitelist = new Set(DEFAULT_IP_WHITELIST) } = opts || {};
if (minHostRate > maxHostRate) throw new Error(`${minHostRate} minHostRate cannot exceed ${maxHostRate} maxHostRate`);
if (minIpRate > maxIpRate) throw new Error(`${minIpRate} minIpRate cannot exceed ${maxIpRate} maxIpRate`);
const lruOptions = {
max: historySize,
//ttl: maxAge, // do NOT use ttl due to performance and we handle stale purges
allowStale: false,
updateAgeOnGet: true,
updateAgeOnHas: false
};
this.#hosts = new _lrucache.default(lruOptions);
this.#ips = new _lrucache.default(lruOptions);
this.#historySize = historySize;
this.#maxAge = maxAge;
this.#minHostRate = minHostRate;
this.#maxHostRate = maxHostRate;
this.#minHostRequests = Math.ceil(minHostRate * (maxAge / 1000));
this.#maxHostRatio = Math.max(Math.min(maxHostRatio, 0.9), 0); // 90% is very high, but this cap is just to prevent invalid ratios
this.#hostRatioMaxCount = Math.ceil(this.#maxHostRatio * 100 * 10); // 10x requests compared to host ratio (10% * 10 = 100)
this.#minIpRate = minIpRate;
this.#maxIpRate = maxIpRate;
this.#maxIpRateHostViolation = maxIpRateHostViolation;
this.#minIpRequests = Math.round(minIpRate * (maxAge / 1000));
this.#hostWhitelist = hostWhitelist;
this.#ipWhitelist = ipWhitelist;
}
#hosts;
#ips;
#historySize;
#maxAge;
#minHostRate;
#maxHostRate;
#minHostRequests;
#maxHostRatio;
#hostRatioMaxCount;
#hostRatioRequestHistory = [];
#hostRatioViolations = new Set();
#hostRatioCounts = new Map();
#minIpRate;
#maxIpRate;
#maxIpRateHostViolation;
#minIpRequests;
#hostWhitelist;
#ipWhitelist;
get hosts() {
return this.#hosts;
}
get ips() {
return this.#ips;
}
get minHostRate() {
return this.#minHostRate;
}
get maxHostRate() {
return this.#maxHostRate;
}
get maxHostRatio() {
return this.#maxHostRatio;
}
get hostRatioViolations() {
return this.#hostRatioViolations;
}
get minIpRate() {
return this.#minIpRate;
}
get maxIpRate() {
return this.#maxIpRate;
}
get maxIpRateHostViolation() {
return this.#maxIpRateHostViolation;
}
get historySize() {
return this.#historySize;
}
get maxAge() {
return this.#maxAge;
}
get hostWhitelist() {
return this.#hostWhitelist;
}
get ipWhitelist() {
return this.#ipWhitelist;
}
getHostInfo(source) {
return getInfo(source, {
lru: this.#hosts,
whitelist: this.#hostWhitelist,
minRequests: this.#minHostRequests,
maxAge: this.#maxAge
});
}
trackHost(source, cache) {
if (this.#maxHostRatio) {
// only track if ratio limits enabled
if (!this.#hostWhitelist.has(source)) {
this.#hostRatioCounts.set(source, (this.#hostRatioCounts.get(source) || 0) + 1);
}
this.#hostRatioRequestHistory = this.#hostRatioRequestHistory.concat(Date.now()).filter((ts)=>ts >= Date.now() - 60000); // 1 minute history
if (this.#hostRatioRequestHistory.length >= this.#hostRatioMaxCount) {
// check for violations once we have sufficient history
const maxCount = Math.round(this.#hostRatioRequestHistory.length * this.#maxHostRatio);
this.#hostRatioViolations = [
...this.#hostRatioCounts.entries()
].reduce((violations, [source, count])=>{
if (count > maxCount) violations.add(source);
return violations;
}, new Set());
this.#hostRatioCounts.clear();
this.#hostRatioRequestHistory = [];
}
}
return track(source, {
lru: this.#hosts,
cache,
minRate: this.#minHostRate
});
}
getIpInfo(source) {
return getInfo(source, {
lru: this.#ips,
whitelist: this.#ipWhitelist,
minRequests: this.#minIpRequests,
maxAge: this.#maxAge
});
}
trackIp(source, cache) {
return track(source, {
lru: this.#ips,
cache,
minRate: this.#minIpRate
});
}
}
function getInfo(source, { lru, whitelist, minRequests, maxAge }) {
if (!minRequests) return 200; // if monitoring is disabled treat as Good
// reserved to indicate will never be a bad actor
if (whitelist.has(source)) return 300;
let cache = lru.get(source);
if (cache) {
// update rate
const now = Date.now();
const expiredAt = now - maxAge;
// always remove stale history before calculating rate
let expiredCount;
// slightly faster than shifting
for(expiredCount = 0; expiredCount < cache.history.length; expiredCount++){
if (cache.history[expiredCount] >= expiredAt) break;
}
if (expiredCount) {
cache.history = cache.history.slice(expiredCount);
}
if (cache.history.length < minRequests) {
cache.rate = 0; // insufficient history to measure
} else {
const eldest = cache.history[0];
// default to 1ms to avoid divide by zero errors since we do have adequate history
const age = now - eldest || 1;
cache.rate = cache.history.length / age * 1000;
}
}
return cache;
}
function track(source, options) {
const { lru, minRate } = options;
let { cache } = options;
if (!minRate) return void 0; // tracking disabled
if (!cache) cache = lru.get(source); // if not supplied grab from lru
if (!cache) {
cache = {
id: source,
history: new Array(),
rate: 0
};
lru.set(source, cache);
}
cache.history.push(Date.now()); // head=eldest, tail=newest
return cache;
}
//# sourceMappingURL=metrics.js.map