UNPKG

connect-qos

Version:

Connect middleware that helps maintain a high quality of service during heavy traffic

264 lines (263 loc) 9.14 kB
"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