123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Throttler = exports.timeToWait = exports.backoff = void 0;
- const logger_1 = require("../logger");
- const retries_exhausted_error_1 = require("./errors/retries-exhausted-error");
- const timeout_error_1 = require("./errors/timeout-error");
- function backoff(retryNumber, delay, maxDelay) {
- return new Promise((resolve) => {
- setTimeout(resolve, timeToWait(retryNumber, delay, maxDelay));
- });
- }
- exports.backoff = backoff;
- function timeToWait(retryNumber, delay, maxDelay) {
- return Math.min(delay * Math.pow(2, retryNumber), maxDelay);
- }
- exports.timeToWait = timeToWait;
- function DEFAULT_HANDLER(task) {
- return task();
- }
- class Throttler {
- constructor(options) {
- this.name = "";
- this.concurrency = 200;
- this.handler = DEFAULT_HANDLER;
- this.active = 0;
- this.complete = 0;
- this.success = 0;
- this.errored = 0;
- this.retried = 0;
- this.total = 0;
- this.taskDataMap = new Map();
- this.waits = [];
- this.min = 9999999999;
- this.max = 0;
- this.avg = 0;
- this.retries = 0;
- this.backoff = 200;
- this.maxBackoff = 60000;
- this.closed = false;
- this.finished = false;
- this.startTime = 0;
- if (options.name) {
- this.name = options.name;
- }
- if (options.handler) {
- this.handler = options.handler;
- }
- if (typeof options.concurrency === "number") {
- this.concurrency = options.concurrency;
- }
- if (typeof options.retries === "number") {
- this.retries = options.retries;
- }
- if (typeof options.backoff === "number") {
- this.backoff = options.backoff;
- }
- if (typeof options.maxBackoff === "number") {
- this.maxBackoff = options.maxBackoff;
- }
- }
- wait() {
- const p = new Promise((resolve, reject) => {
- this.waits.push({ resolve, reject });
- });
- return p;
- }
- add(task, timeoutMillis) {
- this.addHelper(task, timeoutMillis);
- }
- run(task, timeoutMillis) {
- return new Promise((resolve, reject) => {
- this.addHelper(task, timeoutMillis, { resolve, reject });
- });
- }
- close() {
- this.closed = true;
- return this.finishIfIdle();
- }
- process() {
- if (this.finishIfIdle() || this.active >= this.concurrency || !this.hasWaitingTask()) {
- return;
- }
- this.active++;
- this.handle(this.nextWaitingTaskIndex());
- }
- async handle(cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- if (!taskData) {
- throw new Error(`taskData.get(${cursorIndex}) does not exist`);
- }
- const promises = [this.executeTask(cursorIndex)];
- if (taskData.timeoutMillis) {
- promises.push(this.initializeTimeout(cursorIndex));
- }
- let result;
- try {
- result = await Promise.race(promises);
- }
- catch (err) {
- this.errored++;
- this.complete++;
- this.active--;
- this.onTaskFailed(err, cursorIndex);
- return;
- }
- this.success++;
- this.complete++;
- this.active--;
- this.onTaskFulfilled(result, cursorIndex);
- }
- stats() {
- return {
- max: this.max,
- min: this.min,
- avg: this.avg,
- active: this.active,
- complete: this.complete,
- success: this.success,
- errored: this.errored,
- retried: this.retried,
- total: this.total,
- elapsed: Date.now() - this.startTime,
- };
- }
- taskName(cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- if (!taskData) {
- return "finished task";
- }
- return typeof taskData.task === "string" ? taskData.task : `index ${cursorIndex}`;
- }
- addHelper(task, timeoutMillis, wait) {
- if (this.closed) {
- throw new Error("Cannot add a task to a closed throttler.");
- }
- if (!this.startTime) {
- this.startTime = Date.now();
- }
- this.taskDataMap.set(this.total, {
- task,
- wait,
- timeoutMillis,
- retryCount: 0,
- isTimedOut: false,
- });
- this.total++;
- this.process();
- }
- finishIfIdle() {
- if (this.closed && !this.hasWaitingTask() && this.active === 0) {
- this.finish();
- return true;
- }
- return false;
- }
- finish(err) {
- this.waits.forEach((p) => {
- if (err) {
- return p.reject(err);
- }
- this.finished = true;
- return p.resolve();
- });
- }
- initializeTimeout(cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- const timeoutMillis = taskData.timeoutMillis;
- const timeoutPromise = new Promise((_, reject) => {
- taskData.timeoutId = setTimeout(() => {
- taskData.isTimedOut = true;
- reject(new timeout_error_1.default(this.taskName(cursorIndex), timeoutMillis));
- }, timeoutMillis);
- });
- return timeoutPromise;
- }
- async executeTask(cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- const t0 = Date.now();
- let result;
- try {
- result = await this.handler(taskData.task);
- }
- catch (err) {
- if (taskData.retryCount === this.retries) {
- throw new retries_exhausted_error_1.default(this.taskName(cursorIndex), this.retries, err);
- }
- await backoff(taskData.retryCount + 1, this.backoff, this.maxBackoff);
- if (taskData.isTimedOut) {
- throw new timeout_error_1.default(this.taskName(cursorIndex), taskData.timeoutMillis);
- }
- this.retried++;
- taskData.retryCount++;
- logger_1.logger.debug(`[${this.name}] Retrying task`, this.taskName(cursorIndex));
- return this.executeTask(cursorIndex);
- }
- if (taskData.isTimedOut) {
- throw new timeout_error_1.default(this.taskName(cursorIndex), taskData.timeoutMillis);
- }
- const dt = Date.now() - t0;
- this.min = Math.min(dt, this.min);
- this.max = Math.max(dt, this.max);
- this.avg = (this.avg * this.complete + dt) / (this.complete + 1);
- return result;
- }
- onTaskFulfilled(result, cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- if (taskData.wait) {
- taskData.wait.resolve(result);
- }
- this.cleanupTask(cursorIndex);
- this.process();
- }
- onTaskFailed(error, cursorIndex) {
- const taskData = this.taskDataMap.get(cursorIndex);
- logger_1.logger.debug(error);
- if (taskData.wait) {
- taskData.wait.reject(error);
- }
- this.cleanupTask(cursorIndex);
- this.finish(error);
- }
- cleanupTask(cursorIndex) {
- const { timeoutId } = this.taskDataMap.get(cursorIndex);
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- this.taskDataMap.delete(cursorIndex);
- }
- }
- exports.Throttler = Throttler;
|