GSI - Employe Self Service Mobile
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
7.3 KiB

2 months ago
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.Throttler = exports.timeToWait = exports.backoff = void 0;
  4. const logger_1 = require("../logger");
  5. const retries_exhausted_error_1 = require("./errors/retries-exhausted-error");
  6. const timeout_error_1 = require("./errors/timeout-error");
  7. function backoff(retryNumber, delay, maxDelay) {
  8. return new Promise((resolve) => {
  9. setTimeout(resolve, timeToWait(retryNumber, delay, maxDelay));
  10. });
  11. }
  12. exports.backoff = backoff;
  13. function timeToWait(retryNumber, delay, maxDelay) {
  14. return Math.min(delay * Math.pow(2, retryNumber), maxDelay);
  15. }
  16. exports.timeToWait = timeToWait;
  17. function DEFAULT_HANDLER(task) {
  18. return task();
  19. }
  20. class Throttler {
  21. constructor(options) {
  22. this.name = "";
  23. this.concurrency = 200;
  24. this.handler = DEFAULT_HANDLER;
  25. this.active = 0;
  26. this.complete = 0;
  27. this.success = 0;
  28. this.errored = 0;
  29. this.retried = 0;
  30. this.total = 0;
  31. this.taskDataMap = new Map();
  32. this.waits = [];
  33. this.min = 9999999999;
  34. this.max = 0;
  35. this.avg = 0;
  36. this.retries = 0;
  37. this.backoff = 200;
  38. this.maxBackoff = 60000;
  39. this.closed = false;
  40. this.finished = false;
  41. this.startTime = 0;
  42. if (options.name) {
  43. this.name = options.name;
  44. }
  45. if (options.handler) {
  46. this.handler = options.handler;
  47. }
  48. if (typeof options.concurrency === "number") {
  49. this.concurrency = options.concurrency;
  50. }
  51. if (typeof options.retries === "number") {
  52. this.retries = options.retries;
  53. }
  54. if (typeof options.backoff === "number") {
  55. this.backoff = options.backoff;
  56. }
  57. if (typeof options.maxBackoff === "number") {
  58. this.maxBackoff = options.maxBackoff;
  59. }
  60. }
  61. wait() {
  62. const p = new Promise((resolve, reject) => {
  63. this.waits.push({ resolve, reject });
  64. });
  65. return p;
  66. }
  67. add(task, timeoutMillis) {
  68. this.addHelper(task, timeoutMillis);
  69. }
  70. run(task, timeoutMillis) {
  71. return new Promise((resolve, reject) => {
  72. this.addHelper(task, timeoutMillis, { resolve, reject });
  73. });
  74. }
  75. close() {
  76. this.closed = true;
  77. return this.finishIfIdle();
  78. }
  79. process() {
  80. if (this.finishIfIdle() || this.active >= this.concurrency || !this.hasWaitingTask()) {
  81. return;
  82. }
  83. this.active++;
  84. this.handle(this.nextWaitingTaskIndex());
  85. }
  86. async handle(cursorIndex) {
  87. const taskData = this.taskDataMap.get(cursorIndex);
  88. if (!taskData) {
  89. throw new Error(`taskData.get(${cursorIndex}) does not exist`);
  90. }
  91. const promises = [this.executeTask(cursorIndex)];
  92. if (taskData.timeoutMillis) {
  93. promises.push(this.initializeTimeout(cursorIndex));
  94. }
  95. let result;
  96. try {
  97. result = await Promise.race(promises);
  98. }
  99. catch (err) {
  100. this.errored++;
  101. this.complete++;
  102. this.active--;
  103. this.onTaskFailed(err, cursorIndex);
  104. return;
  105. }
  106. this.success++;
  107. this.complete++;
  108. this.active--;
  109. this.onTaskFulfilled(result, cursorIndex);
  110. }
  111. stats() {
  112. return {
  113. max: this.max,
  114. min: this.min,
  115. avg: this.avg,
  116. active: this.active,
  117. complete: this.complete,
  118. success: this.success,
  119. errored: this.errored,
  120. retried: this.retried,
  121. total: this.total,
  122. elapsed: Date.now() - this.startTime,
  123. };
  124. }
  125. taskName(cursorIndex) {
  126. const taskData = this.taskDataMap.get(cursorIndex);
  127. if (!taskData) {
  128. return "finished task";
  129. }
  130. return typeof taskData.task === "string" ? taskData.task : `index ${cursorIndex}`;
  131. }
  132. addHelper(task, timeoutMillis, wait) {
  133. if (this.closed) {
  134. throw new Error("Cannot add a task to a closed throttler.");
  135. }
  136. if (!this.startTime) {
  137. this.startTime = Date.now();
  138. }
  139. this.taskDataMap.set(this.total, {
  140. task,
  141. wait,
  142. timeoutMillis,
  143. retryCount: 0,
  144. isTimedOut: false,
  145. });
  146. this.total++;
  147. this.process();
  148. }
  149. finishIfIdle() {
  150. if (this.closed && !this.hasWaitingTask() && this.active === 0) {
  151. this.finish();
  152. return true;
  153. }
  154. return false;
  155. }
  156. finish(err) {
  157. this.waits.forEach((p) => {
  158. if (err) {
  159. return p.reject(err);
  160. }
  161. this.finished = true;
  162. return p.resolve();
  163. });
  164. }
  165. initializeTimeout(cursorIndex) {
  166. const taskData = this.taskDataMap.get(cursorIndex);
  167. const timeoutMillis = taskData.timeoutMillis;
  168. const timeoutPromise = new Promise((_, reject) => {
  169. taskData.timeoutId = setTimeout(() => {
  170. taskData.isTimedOut = true;
  171. reject(new timeout_error_1.default(this.taskName(cursorIndex), timeoutMillis));
  172. }, timeoutMillis);
  173. });
  174. return timeoutPromise;
  175. }
  176. async executeTask(cursorIndex) {
  177. const taskData = this.taskDataMap.get(cursorIndex);
  178. const t0 = Date.now();
  179. let result;
  180. try {
  181. result = await this.handler(taskData.task);
  182. }
  183. catch (err) {
  184. if (taskData.retryCount === this.retries) {
  185. throw new retries_exhausted_error_1.default(this.taskName(cursorIndex), this.retries, err);
  186. }
  187. await backoff(taskData.retryCount + 1, this.backoff, this.maxBackoff);
  188. if (taskData.isTimedOut) {
  189. throw new timeout_error_1.default(this.taskName(cursorIndex), taskData.timeoutMillis);
  190. }
  191. this.retried++;
  192. taskData.retryCount++;
  193. logger_1.logger.debug(`[${this.name}] Retrying task`, this.taskName(cursorIndex));
  194. return this.executeTask(cursorIndex);
  195. }
  196. if (taskData.isTimedOut) {
  197. throw new timeout_error_1.default(this.taskName(cursorIndex), taskData.timeoutMillis);
  198. }
  199. const dt = Date.now() - t0;
  200. this.min = Math.min(dt, this.min);
  201. this.max = Math.max(dt, this.max);
  202. this.avg = (this.avg * this.complete + dt) / (this.complete + 1);
  203. return result;
  204. }
  205. onTaskFulfilled(result, cursorIndex) {
  206. const taskData = this.taskDataMap.get(cursorIndex);
  207. if (taskData.wait) {
  208. taskData.wait.resolve(result);
  209. }
  210. this.cleanupTask(cursorIndex);
  211. this.process();
  212. }
  213. onTaskFailed(error, cursorIndex) {
  214. const taskData = this.taskDataMap.get(cursorIndex);
  215. logger_1.logger.debug(error);
  216. if (taskData.wait) {
  217. taskData.wait.reject(error);
  218. }
  219. this.cleanupTask(cursorIndex);
  220. this.finish(error);
  221. }
  222. cleanupTask(cursorIndex) {
  223. const { timeoutId } = this.taskDataMap.get(cursorIndex);
  224. if (timeoutId) {
  225. clearTimeout(timeoutId);
  226. }
  227. this.taskDataMap.delete(cursorIndex);
  228. }
  229. }
  230. exports.Throttler = Throttler;