diff options
-rw-r--r-- | src/util/RequestThrottler.ts | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/src/util/RequestThrottler.ts b/src/util/RequestThrottler.ts new file mode 100644 index 000000000..48a607296 --- /dev/null +++ b/src/util/RequestThrottler.ts @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Implementation of token bucket throttling. + */ + +/** + * Imports. + */ +import { getTimestampNow, Timestamp } from "../walletTypes"; + +/** + * Maximum request per second, per origin. + */ +const MAX_PER_SECOND = 20; + +/** + * Maximum request per minute, per origin. + */ +const MAX_PER_MINUTE = 100; + +/** + * Maximum request per hour, per origin. + */ +const MAX_PER_HOUR = 5000; + + +/** + * Throttling state for one origin. + */ +class OriginState { + private tokensSecond: number = MAX_PER_SECOND; + private tokensMinute: number = MAX_PER_MINUTE; + private tokensHour: number = MAX_PER_HOUR; + private lastUpdate = getTimestampNow(); + + private refill(): void { + const now = getTimestampNow(); + const d = now.t_ms - this.lastUpdate.t_ms; + this.tokensSecond = Math.max(MAX_PER_SECOND, this.tokensSecond + (d / 1000)); + this.tokensMinute = Math.max(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 * 60)); + this.tokensHour = Math.max(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 * 60)); + this.lastUpdate = now; + } + + /** + * Return true if the request for this origin should be throttled. + * Otherwise, take a token out of the respective buckets. + */ + applyThrottle(): boolean { + this.refill(); + if (this.tokensSecond < 1) { + console.log("request throttled (per second limit exceeded)"); + return true; + } + if (this.tokensMinute < 1) { + console.log("request throttled (per minute limit exceeded)"); + return true; + } + if (this.tokensHour < 1) { + console.log("request throttled (per hour limit exceeded)"); + return true; + } + this.tokensSecond--; + this.tokensMinute--; + this.tokensHour--; + return false; + } +} + +/** + * Request throttler, used as a "last layer of defense" when some + * other part of the re-try logic is broken and we're sending too + * many requests to the same exchange/bank/merchant. + */ +export class RequestThrottler { + private perOriginInfo: { [origin: string]: OriginState } = {}; + + /** + * Get the throttling state for an origin, or + * initialize if no state is associated with the + * origin yet. + */ + private getState(origin: string): OriginState { + const s = this.perOriginInfo[origin]; + if (s) { + return s; + } + const ns = this.perOriginInfo[origin] = new OriginState(); + return ns; + } + + /** + * Apply throttling to a request. + * + * @returns whether the request should be throttled. + */ + applyThrottle(requestUrl: string): boolean { + const origin = new URL(requestUrl).origin; + return this.getState(origin).applyThrottle(); + } +} |