qdone 2.0.5-alpha → 2.0.7-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,9 +23,10 @@ exports.defaults = Object.freeze({
23
23
  // Enqueue
24
24
  groupId: (0, uuid_1.v1)(),
25
25
  groupIdPerMessage: false,
26
- deduplicationId: (0, uuid_1.v1)(),
26
+ deduplicationId: undefined,
27
27
  messageRetentionPeriod: 1209600,
28
28
  delay: 0,
29
+ sendRetries: 6,
29
30
  failDelay: 0,
30
31
  dlq: false,
31
32
  dlqSuffix: '_dead',
@@ -74,6 +75,7 @@ function getOptionsWithDefaults(options) {
74
75
  deduplicationId: options.deduplicationId || options['deduplication-id'] || exports.defaults.deduplicationId,
75
76
  messageRetentionPeriod: options.messageRetentionPeriod || options['message-retention-period'] || exports.defaults.messageRetentionPeriod,
76
77
  delay: options.delay || exports.defaults.delay,
78
+ sendRetries: options['send-retries'] || exports.defaults.sendRetries,
77
79
  failDelay: options.failDelay || options['fail-delay'] || exports.defaults.failDelay,
78
80
  dlq: dlq || exports.defaults.dlq,
79
81
  dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || exports.defaults.dlqSuffix,
@@ -1,10 +1,4 @@
1
1
  "use strict";
2
- // const Q = require('q')
3
- // const debug = require('debug')('qdone:enqueue')
4
- // const chalk = require('chalk')
5
- // const uuid = require('uuid')
6
- // const qrlCache = require('./qrlCache')
7
- // const AWS = require('aws-sdk')
8
2
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
3
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
4
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -46,6 +40,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
46
40
  };
47
41
  Object.defineProperty(exports, "__esModule", { value: true });
48
42
  exports.enqueueBatch = exports.enqueue = exports.addMessage = exports.flushMessages = exports.sendMessageBatch = exports.sendMessage = exports.formatMessage = exports.getQueueAttributes = exports.getOrCreateQueue = exports.getOrCreateFailQueue = exports.getOrCreateDLQ = void 0;
43
+ var node_1 = require("@sentry/node");
49
44
  var uuid_1 = require("uuid");
50
45
  var chalk_1 = __importDefault(require("chalk"));
51
46
  var debug_1 = __importDefault(require("debug"));
@@ -53,6 +48,7 @@ var client_sqs_1 = require("@aws-sdk/client-sqs");
53
48
  var qrlCache_js_1 = require("./qrlCache.js");
54
49
  var sqs_js_1 = require("./sqs.js");
55
50
  var defaults_js_1 = require("./defaults.js");
51
+ var exponentialBackoff_js_1 = require("./exponentialBackoff.js");
56
52
  var debug = (0, debug_1.default)('qdone:enqueue');
57
53
  function getOrCreateDLQ(queue, opt) {
58
54
  return __awaiter(this, void 0, void 0, function () {
@@ -258,29 +254,63 @@ function formatMessage(command, id) {
258
254
  return message;
259
255
  }
260
256
  exports.formatMessage = formatMessage;
257
+ // Retry happens within the context of the send functions
258
+ var retryableExceptions = [
259
+ client_sqs_1.RequestThrottled,
260
+ client_sqs_1.KmsThrottled,
261
+ client_sqs_1.QueueDoesNotExist // Queue could temporarily not exist due to eventual consistency, let it retry
262
+ ];
261
263
  function sendMessage(qrl, command, opt) {
262
264
  return __awaiter(this, void 0, void 0, function () {
263
- var params, client, cmd, data;
265
+ var params, client, cmd, backoff, send, shouldRetry, result;
266
+ var _this = this;
264
267
  return __generator(this, function (_a) {
265
268
  switch (_a.label) {
266
269
  case 0:
267
270
  debug('sendMessage(', qrl, command, ')');
268
271
  params = Object.assign({ QueueUrl: qrl }, formatMessage(command));
269
- // Add in group id if we're using fifo
270
272
  if (opt.fifo) {
271
273
  params.MessageGroupId = opt.groupId;
272
- params.MessageDeduplicationId = opt.deduplicationId;
274
+ params.MessageDeduplicationId = opt.deduplicationId || (0, uuid_1.v1)();
273
275
  }
274
276
  if (opt.delay)
275
277
  params.DelaySeconds = opt.delay;
276
278
  client = (0, sqs_js_1.getSQSClient)();
277
279
  cmd = new client_sqs_1.SendMessageCommand(params);
278
280
  debug({ cmd: cmd });
279
- return [4 /*yield*/, client.send(cmd)];
281
+ backoff = new exponentialBackoff_js_1.ExponentialBackoff(opt.sendRetries);
282
+ send = function (attemptNumber) { return __awaiter(_this, void 0, void 0, function () {
283
+ var data;
284
+ return __generator(this, function (_a) {
285
+ switch (_a.label) {
286
+ case 0:
287
+ cmd.input.attemptNumber = attemptNumber;
288
+ return [4 /*yield*/, client.send(cmd)];
289
+ case 1:
290
+ data = _a.sent();
291
+ debug('sendMessage returned', data);
292
+ return [2 /*return*/, data];
293
+ }
294
+ });
295
+ }); };
296
+ shouldRetry = function (result, error) { return __awaiter(_this, void 0, void 0, function () {
297
+ var _i, retryableExceptions_1, exceptionClass;
298
+ return __generator(this, function (_a) {
299
+ for (_i = 0, retryableExceptions_1 = retryableExceptions; _i < retryableExceptions_1.length; _i++) {
300
+ exceptionClass = retryableExceptions_1[_i];
301
+ if (error instanceof exceptionClass) {
302
+ debug({ sendMessageRetryingBecause: { error: error, result: result } });
303
+ return [2 /*return*/, true];
304
+ }
305
+ }
306
+ return [2 /*return*/, false];
307
+ });
308
+ }); };
309
+ return [4 /*yield*/, backoff.run(send, shouldRetry)];
280
310
  case 1:
281
- data = _a.sent();
282
- debug('sendMessage returned', data);
283
- return [2 /*return*/, data];
311
+ result = _a.sent();
312
+ debug({ sendMessageResult: result });
313
+ return [2 /*return*/, result];
284
314
  }
285
315
  });
286
316
  });
@@ -288,34 +318,84 @@ function sendMessage(qrl, command, opt) {
288
318
  exports.sendMessage = sendMessage;
289
319
  function sendMessageBatch(qrl, messages, opt) {
290
320
  return __awaiter(this, void 0, void 0, function () {
291
- var params, uuidFunction, client, cmd, data;
321
+ var params, uuidFunction, client, cmd, backoff, send, shouldRetry;
322
+ var _this = this;
292
323
  return __generator(this, function (_a) {
293
- switch (_a.label) {
294
- case 0:
295
- debug('sendMessageBatch(', qrl, messages.map(function (e) { return Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' }); }), ')');
296
- params = { Entries: messages, QueueUrl: qrl };
297
- uuidFunction = opt.uuidFunction || uuid_1.v1;
298
- // Add in group id if we're using fifo
299
- if (opt.fifo) {
300
- params.Entries = params.Entries.map(function (message) { return Object.assign({
301
- MessageGroupId: opt.groupIdPerMessage ? uuidFunction() : opt.groupId,
302
- MessageDeduplicationId: uuidFunction()
303
- }, message); });
324
+ debug('sendMessageBatch(', qrl, messages.map(function (e) { return Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' }); }), ')');
325
+ params = { Entries: messages, QueueUrl: qrl };
326
+ uuidFunction = opt.uuidFunction || uuid_1.v1;
327
+ // Add in group id if we're using fifo
328
+ if (opt.fifo) {
329
+ params.Entries = params.Entries.map(function (message) { return Object.assign({
330
+ MessageGroupId: opt.groupIdPerMessage ? uuidFunction() : opt.groupId,
331
+ MessageDeduplicationId: uuidFunction()
332
+ }, message); });
333
+ }
334
+ if (opt.delay) {
335
+ params.Entries = params.Entries.map(function (message) {
336
+ return Object.assign({ DelaySeconds: opt.delay }, message);
337
+ });
338
+ }
339
+ if (opt.sentryDsn) {
340
+ (0, node_1.addBreadcrumb)({ category: 'sendMessageBatch', message: JSON.stringify({ params: params }), level: 'debug' });
341
+ }
342
+ debug({ params: params });
343
+ client = (0, sqs_js_1.getSQSClient)();
344
+ cmd = new client_sqs_1.SendMessageBatchCommand(params);
345
+ debug({ cmd: cmd });
346
+ backoff = new exponentialBackoff_js_1.ExponentialBackoff(opt.sendRetries);
347
+ send = function (attemptNumber) { return __awaiter(_this, void 0, void 0, function () {
348
+ var data;
349
+ return __generator(this, function (_a) {
350
+ switch (_a.label) {
351
+ case 0:
352
+ debug({ sendMessageBatchSend: { attemptNumber: attemptNumber, params: params } });
353
+ return [4 /*yield*/, client.send(cmd)];
354
+ case 1:
355
+ data = _a.sent();
356
+ return [2 /*return*/, data];
304
357
  }
305
- if (opt.delay) {
306
- params.Entries = params.Entries.map(function (message) {
307
- return Object.assign({ DelaySeconds: opt.delay }, message);
308
- });
358
+ });
359
+ }); };
360
+ shouldRetry = function (result, error) {
361
+ debug({ shouldRetry: { error: error, result: result } });
362
+ if (result) {
363
+ // Handle failed result of one or more messages in the batch
364
+ if (result.Failed && result.Failed.length) {
365
+ var _loop_1 = function (failed) {
366
+ // Find corresponding messages
367
+ var original = params.Entries.find(function (e) { return e.Id === failed.Id; });
368
+ var info = { failed: failed, original: original, opt: opt };
369
+ if (opt.sentryDsn) {
370
+ (0, node_1.addBreadcrumb)({ category: 'sendMessageBatch', message: 'Failed message: ' + JSON.stringify(info), level: 'error' });
371
+ }
372
+ else {
373
+ console.error(info);
374
+ }
375
+ };
376
+ for (var _i = 0, _a = result.Failed; _i < _a.length; _i++) {
377
+ var failed = _a[_i];
378
+ _loop_1(failed);
379
+ }
380
+ throw new Error('One or more message failures: ' + JSON.stringify(result.Failed));
309
381
  }
310
- client = (0, sqs_js_1.getSQSClient)();
311
- cmd = new client_sqs_1.SendMessageBatchCommand(params);
312
- debug({ cmd: cmd });
313
- return [4 /*yield*/, client.send(cmd)];
314
- case 1:
315
- data = _a.sent();
316
- debug('sendMessageBatch returned', data);
317
- return [2 /*return*/, data];
318
- }
382
+ }
383
+ if (error) {
384
+ // Handle a failed result from an overall error on request
385
+ if (opt.sentryDsn) {
386
+ (0, node_1.addBreadcrumb)({ category: 'sendMessageBatch', message: JSON.stringify({ error: error }), level: 'error' });
387
+ }
388
+ for (var _b = 0, retryableExceptions_2 = retryableExceptions; _b < retryableExceptions_2.length; _b++) {
389
+ var exceptionClass = retryableExceptions_2[_b];
390
+ debug({ exceptionClass: exceptionClass, retryableExceptions: retryableExceptions });
391
+ if (error instanceof exceptionClass) {
392
+ debug({ sendMessageRetryingBecause: { error: error, result: result } });
393
+ return true;
394
+ }
395
+ }
396
+ }
397
+ };
398
+ return [2 /*return*/, backoff.run(send, shouldRetry)];
319
399
  });
320
400
  });
321
401
  }
@@ -396,6 +476,7 @@ function addMessage(qrl, command, opt) {
396
476
  message = formatMessage(command, messageIndex++);
397
477
  messages[qrl] = messages[qrl] || [];
398
478
  messages[qrl].push(message);
479
+ debug({ location: 'addMessage', messages: messages });
399
480
  if (messages[qrl].length >= 10) {
400
481
  return [2 /*return*/, flushMessages(qrl, opt)];
401
482
  }
@@ -431,9 +512,9 @@ exports.enqueue = enqueue;
431
512
  //
432
513
  function enqueueBatch(pairs, options) {
433
514
  return __awaiter(this, void 0, void 0, function () {
434
- var opt, normalizedPairs, uniqueQnames, createPromises, _i, uniqueQnames_1, qname, addMessagePromises, _a, normalizedPairs_1, _b, qname, command, qrl, flushCounts, initialFlushTotal, extraFlushPromises, qrl, extraFlushCounts, extraFlushTotal, totalFlushed;
435
- return __generator(this, function (_c) {
436
- switch (_c.label) {
515
+ var opt, normalizedPairs, uniqueQnames, createPromises, _i, uniqueQnames_1, qname, initialFlushTotal, _a, normalizedPairs_1, _b, qname, command, qrl, _c, extraFlushPromises, qrl, extraFlushCounts, extraFlushTotal, totalFlushed;
516
+ return __generator(this, function (_d) {
517
+ switch (_d.label) {
437
518
  case 0:
438
519
  debug('enqueueBatch(', pairs, ')');
439
520
  opt = (0, defaults_js_1.getOptionsWithDefaults)(options);
@@ -455,39 +536,35 @@ function enqueueBatch(pairs, options) {
455
536
  // so go back through the list of pairs and fire off messages
456
537
  ];
457
538
  case 1:
458
- _c.sent();
539
+ _d.sent();
459
540
  // After we've prefetched, all qrls are in cache
460
541
  // so go back through the list of pairs and fire off messages
461
542
  requestCount = 0;
462
- addMessagePromises = [];
543
+ initialFlushTotal = 0;
463
544
  _a = 0, normalizedPairs_1 = normalizedPairs;
464
- _c.label = 2;
545
+ _d.label = 2;
465
546
  case 2:
466
- if (!(_a < normalizedPairs_1.length)) return [3 /*break*/, 5];
547
+ if (!(_a < normalizedPairs_1.length)) return [3 /*break*/, 6];
467
548
  _b = normalizedPairs_1[_a], qname = _b.qname, command = _b.command;
468
549
  return [4 /*yield*/, getOrCreateQueue(qname, opt)];
469
550
  case 3:
470
- qrl = _c.sent();
471
- addMessagePromises.push(addMessage(qrl, command, opt));
472
- _c.label = 4;
551
+ qrl = _d.sent();
552
+ _c = initialFlushTotal;
553
+ return [4 /*yield*/, addMessage(qrl, command, opt)];
473
554
  case 4:
555
+ initialFlushTotal = _c + _d.sent();
556
+ _d.label = 5;
557
+ case 5:
474
558
  _a++;
475
559
  return [3 /*break*/, 2];
476
- case 5: return [4 /*yield*/, Promise.all(addMessagePromises)
477
- // Count up how many were flushed during add
478
- ];
479
560
  case 6:
480
- flushCounts = _c.sent();
481
- // Count up how many were flushed during add
482
- debug('flushCounts', flushCounts);
483
- initialFlushTotal = flushCounts.reduce(function (a, b) { return a + b; }, 0);
484
561
  extraFlushPromises = [];
485
562
  for (qrl in messages) {
486
563
  extraFlushPromises.push(flushMessages(qrl, opt));
487
564
  }
488
565
  return [4 /*yield*/, Promise.all(extraFlushPromises)];
489
566
  case 7:
490
- extraFlushCounts = _c.sent();
567
+ extraFlushCounts = _d.sent();
491
568
  extraFlushTotal = extraFlushCounts.reduce(function (a, b) { return a + b; }, 0);
492
569
  totalFlushed = initialFlushTotal + extraFlushTotal;
493
570
  debug({ initialFlushTotal: initialFlushTotal, extraFlushTotal: extraFlushTotal, totalFlushed: totalFlushed });
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /**
3
+ * Exponential backoff controller.
4
+ * usage:
5
+ * const exp = new ExponentialBackoff()
6
+ * const result = await exp.run(
7
+ * function action (attemptNumber) {
8
+ * console.log(attemptNumber) // 1, 2, 3, ...
9
+ * return axios.post(...)
10
+ * },
11
+ * function shouldRetry (returnValue, error) {
12
+ * if (returnValue && return value.code = 500) return true
13
+ * if (error && error.message === 'Internal Server Error') return true
14
+ * }
15
+ * )
16
+ */
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
28
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.ExponentialBackoff = void 0;
55
+ var ExponentialBackoff = /** @class */ (function () {
56
+ /**
57
+ * Creates various behaviors for backoff.
58
+ * @param {number} maxRetries - Number of times to attempt the action before
59
+ * throwing an error. Defaults to 3.
60
+ * @param {number} maxJitterPercent - Jitter as a percentage of the delay.
61
+ * For example, if the exponential delay is 2 seconds, then a jitter of
62
+ * 0.5 could lead to a delay as low as 1 second and as high as 3 seconds,
63
+ * since 0.5 * 2 = 1. Defaults to 0.5.
64
+ * @param {number} exponentBase - The base for the exponent. Defaults to 2,
65
+ * which means the delay doubles every attempt.
66
+ */
67
+ function ExponentialBackoff(maxRetries, maxJitterPercent, exponentBase) {
68
+ if (maxRetries === void 0) { maxRetries = 3; }
69
+ if (maxJitterPercent === void 0) { maxJitterPercent = 0.5; }
70
+ if (exponentBase === void 0) { exponentBase = 2; }
71
+ if (maxRetries < 1)
72
+ throw new Error('maxRetries must be >= 1');
73
+ if (maxJitterPercent < 0.1 || maxJitterPercent > 1)
74
+ throw new Error('maxJitterPercent must be in the interval [0.1, 1]');
75
+ if (exponentBase < 1 || exponentBase > 10)
76
+ throw new Error('exponentBase must be in the range [1, 10]');
77
+ this.maxRetries = parseInt(maxRetries);
78
+ this.maxJitterPercent = parseFloat(maxJitterPercent);
79
+ this.exponentBase = parseFloat(exponentBase);
80
+ this.attemptNumber = 0;
81
+ }
82
+ /**
83
+ * Calculates how many ms to delay based on the current attempt number.
84
+ */
85
+ ExponentialBackoff.prototype.calculateDelayMs = function (attemptNumber) {
86
+ var secondsRaw = Math.pow(this.exponentBase, attemptNumber); // 2, 4, 8, 16, ....
87
+ var jitter = this.maxJitterPercent * (Math.random() - 0.5); // [-0.5, 0.5]
88
+ var delayMs = Math.round(secondsRaw * (1 + jitter) * 1000);
89
+ // console.log({ secondsRaw, jitter, delayMs })
90
+ return delayMs;
91
+ };
92
+ /**
93
+ * Resolves after a delay set by the current attempt.
94
+ */
95
+ ExponentialBackoff.prototype.delay = function (attemptNumber) {
96
+ return __awaiter(this, void 0, void 0, function () {
97
+ var delay;
98
+ return __generator(this, function (_a) {
99
+ delay = this.calculateDelayMs(attemptNumber);
100
+ // console.log({ function: 'delay', attemptNumber, delay })
101
+ return [2 /*return*/, new Promise(function (resolve, reject) { return setTimeout(resolve, delay); })];
102
+ });
103
+ });
104
+ };
105
+ /**
106
+ * Call another function repeatedly, retrying with exponential backoff and
107
+ * jitter if not successful.
108
+ * @param {ExponentialBackoff~action} action - Callback that does the action
109
+ * to be attempted (web request, rpc, database call, etc). Will be called
110
+ * again after the exponential dealy if shouldRetry() returns true.
111
+ * @param {ExponentialBackoff~shouldRetry} shouldRetry - Callback that gets
112
+ * to look at the return value of action() and any potential exception. If
113
+ * this returns true then the action will be retried with the appropriate
114
+ * backoff delay. Defaults to a function that returns true if an exception
115
+ * is thrown.
116
+ */
117
+ ExponentialBackoff.prototype.run = function (action, shouldRetry) {
118
+ var _this = this;
119
+ if (action === void 0) { action = function (attemptNumber) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
120
+ return [2 /*return*/, undefined];
121
+ }); }); }; }
122
+ if (shouldRetry === void 0) { shouldRetry = function (returnValue, error) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
123
+ return [2 /*return*/, !!error];
124
+ }); }); }; }
125
+ return __awaiter(this, void 0, void 0, function () {
126
+ var attemptNumber, result, e_1;
127
+ return __generator(this, function (_a) {
128
+ switch (_a.label) {
129
+ case 0:
130
+ attemptNumber = 0;
131
+ _a.label = 1;
132
+ case 1:
133
+ if (!(attemptNumber++ < this.maxRetries)) return [3 /*break*/, 14];
134
+ _a.label = 2;
135
+ case 2:
136
+ _a.trys.push([2, 8, , 13]);
137
+ return [4 /*yield*/, action(attemptNumber)];
138
+ case 3:
139
+ result = _a.sent();
140
+ return [4 /*yield*/, shouldRetry(result, undefined)];
141
+ case 4:
142
+ if (!_a.sent()) return [3 /*break*/, 6];
143
+ if (attemptNumber >= this.maxRetries)
144
+ throw new Error('Maximum number of attempts reached');
145
+ return [4 /*yield*/, this.delay(attemptNumber)];
146
+ case 5:
147
+ _a.sent();
148
+ return [3 /*break*/, 7];
149
+ case 6: return [2 /*return*/, result];
150
+ case 7: return [3 /*break*/, 13];
151
+ case 8:
152
+ e_1 = _a.sent();
153
+ return [4 /*yield*/, shouldRetry(undefined, e_1)];
154
+ case 9:
155
+ if (!_a.sent()) return [3 /*break*/, 11];
156
+ if (attemptNumber >= this.maxRetries)
157
+ throw e_1;
158
+ return [4 /*yield*/, this.delay(attemptNumber)];
159
+ case 10:
160
+ _a.sent();
161
+ return [3 /*break*/, 12];
162
+ case 11: throw e_1;
163
+ case 12: return [3 /*break*/, 13];
164
+ case 13: return [3 /*break*/, 1];
165
+ case 14: return [2 /*return*/];
166
+ }
167
+ });
168
+ });
169
+ };
170
+ return ExponentialBackoff;
171
+ }());
172
+ exports.ExponentialBackoff = ExponentialBackoff;
@@ -328,7 +328,7 @@ function processQueuePair(qname, qrl, opt) {
328
328
  isFifo = qname.endsWith('.fifo');
329
329
  normalizeOptions = Object.assign({}, opt, { fifo: isFifo });
330
330
  fqname = (0, qrlCache_js_1.normalizeFailQueueName)(qname, normalizeOptions);
331
- fqrl = (0, qrlCache_js_1.normalizeFailQueueName)(qrl, normalizeOptions);
331
+ fqrl = (0, qrlCache_js_1.normalizeFailQueueName)(fqname, normalizeOptions);
332
332
  return [4 /*yield*/, checkIdle(qname, qrl, opt)];
333
333
  case 1:
334
334
  result = _b.sent();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.0.5-alpha",
3
+ "version": "2.0.7-alpha",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "qdone",
9
- "version": "2.0.5-alpha",
9
+ "version": "2.0.7-alpha",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@aws-sdk/client-cloudwatch": "^3.465.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.0.5-alpha",
3
+ "version": "2.0.7-alpha",
4
4
  "description": "Language agnostic job queue for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/defaults.js CHANGED
@@ -22,9 +22,10 @@ export const defaults = Object.freeze({
22
22
  // Enqueue
23
23
  groupId: uuidv1(),
24
24
  groupIdPerMessage: false,
25
- deduplicationId: uuidv1(),
25
+ deduplicationId: undefined,
26
26
  messageRetentionPeriod: 1209600,
27
27
  delay: 0,
28
+ sendRetries: 6,
28
29
  failDelay: 0,
29
30
  dlq: false,
30
31
  dlqSuffix: '_dead',
@@ -79,6 +80,7 @@ export function getOptionsWithDefaults (options) {
79
80
  deduplicationId: options.deduplicationId || options['deduplication-id'] || defaults.deduplicationId,
80
81
  messageRetentionPeriod: options.messageRetentionPeriod || options['message-retention-period'] || defaults.messageRetentionPeriod,
81
82
  delay: options.delay || defaults.delay,
83
+ sendRetries: options['send-retries'] || defaults.sendRetries,
82
84
  failDelay: options.failDelay || options['fail-delay'] || defaults.failDelay,
83
85
  dlq: dlq || defaults.dlq,
84
86
  dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || defaults.dlqSuffix,
package/src/enqueue.js CHANGED
@@ -1,10 +1,4 @@
1
- // const Q = require('q')
2
- // const debug = require('debug')('qdone:enqueue')
3
- // const chalk = require('chalk')
4
- // const uuid = require('uuid')
5
- // const qrlCache = require('./qrlCache')
6
- // const AWS = require('aws-sdk')
7
-
1
+ import { addBreadcrumb } from '@sentry/node'
8
2
  import { v1 as uuidV1 } from 'uuid'
9
3
  import chalk from 'chalk'
10
4
  import Debug from 'debug'
@@ -13,7 +7,9 @@ import {
13
7
  GetQueueAttributesCommand,
14
8
  SendMessageCommand,
15
9
  SendMessageBatchCommand,
16
- QueueDoesNotExist
10
+ QueueDoesNotExist,
11
+ RequestThrottled,
12
+ KmsThrottled
17
13
  } from '@aws-sdk/client-sqs'
18
14
 
19
15
  import {
@@ -25,6 +21,7 @@ import {
25
21
  } from './qrlCache.js'
26
22
  import { getSQSClient } from './sqs.js'
27
23
  import { getOptionsWithDefaults } from './defaults.js'
24
+ import { ExponentialBackoff } from './exponentialBackoff.js'
28
25
 
29
26
  const debug = Debug('qdone:enqueue')
30
27
 
@@ -162,21 +159,45 @@ export function formatMessage (command, id) {
162
159
  return message
163
160
  }
164
161
 
162
+ // Retry happens within the context of the send functions
163
+ const retryableExceptions = [
164
+ RequestThrottled,
165
+ KmsThrottled,
166
+ QueueDoesNotExist // Queue could temporarily not exist due to eventual consistency, let it retry
167
+ ]
168
+
165
169
  export async function sendMessage (qrl, command, opt) {
166
170
  debug('sendMessage(', qrl, command, ')')
167
171
  const params = Object.assign({ QueueUrl: qrl }, formatMessage(command))
168
- // Add in group id if we're using fifo
169
172
  if (opt.fifo) {
170
173
  params.MessageGroupId = opt.groupId
171
- params.MessageDeduplicationId = opt.deduplicationId
174
+ params.MessageDeduplicationId = opt.deduplicationId || uuidV1()
172
175
  }
173
176
  if (opt.delay) params.DelaySeconds = opt.delay
177
+
178
+ // Send it
174
179
  const client = getSQSClient()
175
180
  const cmd = new SendMessageCommand(params)
176
181
  debug({ cmd })
177
- const data = await client.send(cmd)
178
- debug('sendMessage returned', data)
179
- return data
182
+ const backoff = new ExponentialBackoff(opt.sendRetries)
183
+ const send = async (attemptNumber) => {
184
+ cmd.input.attemptNumber = attemptNumber
185
+ const data = await client.send(cmd)
186
+ debug('sendMessage returned', data)
187
+ return data
188
+ }
189
+ const shouldRetry = async (result, error) => {
190
+ for (const exceptionClass of retryableExceptions) {
191
+ if (error instanceof exceptionClass) {
192
+ debug({ sendMessageRetryingBecause: { error, result } })
193
+ return true
194
+ }
195
+ }
196
+ return false
197
+ }
198
+ const result = await backoff.run(send, shouldRetry)
199
+ debug({ sendMessageResult: result })
200
+ return result
180
201
  }
181
202
 
182
203
  export async function sendMessageBatch (qrl, messages, opt) {
@@ -196,12 +217,54 @@ export async function sendMessageBatch (qrl, messages, opt) {
196
217
  params.Entries = params.Entries.map(message =>
197
218
  Object.assign({ DelaySeconds: opt.delay }, message))
198
219
  }
220
+ if (opt.sentryDsn) {
221
+ addBreadcrumb({ category: 'sendMessageBatch', message: JSON.stringify({ params }), level: 'debug' })
222
+ }
223
+ debug({ params })
224
+
225
+ // Send them
199
226
  const client = getSQSClient()
200
227
  const cmd = new SendMessageBatchCommand(params)
201
228
  debug({ cmd })
202
- const data = await client.send(cmd)
203
- debug('sendMessageBatch returned', data)
204
- return data
229
+ const backoff = new ExponentialBackoff(opt.sendRetries)
230
+ const send = async (attemptNumber) => {
231
+ debug({ sendMessageBatchSend: { attemptNumber, params } })
232
+ const data = await client.send(cmd)
233
+ return data
234
+ }
235
+ const shouldRetry = (result, error) => {
236
+ debug({ shouldRetry: { error, result } })
237
+ if (result) {
238
+ // Handle failed result of one or more messages in the batch
239
+ if (result.Failed && result.Failed.length) {
240
+ for (const failed of result.Failed) {
241
+ // Find corresponding messages
242
+ const original = params.Entries.find((e) => e.Id === failed.Id)
243
+ const info = { failed, original, opt }
244
+ if (opt.sentryDsn) {
245
+ addBreadcrumb({ category: 'sendMessageBatch', message: 'Failed message: ' + JSON.stringify(info), level: 'error' })
246
+ } else {
247
+ console.error(info)
248
+ }
249
+ }
250
+ throw new Error('One or more message failures: ' + JSON.stringify(result.Failed))
251
+ }
252
+ }
253
+ if (error) {
254
+ // Handle a failed result from an overall error on request
255
+ if (opt.sentryDsn) {
256
+ addBreadcrumb({ category: 'sendMessageBatch', message: JSON.stringify({ error }), level: 'error' })
257
+ }
258
+ for (const exceptionClass of retryableExceptions) {
259
+ debug({ exceptionClass, retryableExceptions })
260
+ if (error instanceof exceptionClass) {
261
+ debug({ sendMessageRetryingBecause: { error, result } })
262
+ return true
263
+ }
264
+ }
265
+ }
266
+ }
267
+ return backoff.run(send, shouldRetry)
205
268
  }
206
269
 
207
270
  const messages = {}
@@ -263,6 +326,7 @@ export async function addMessage (qrl, command, opt) {
263
326
  const message = formatMessage(command, messageIndex++)
264
327
  messages[qrl] = messages[qrl] || []
265
328
  messages[qrl].push(message)
329
+ debug({ location: 'addMessage', messages })
266
330
  if (messages[qrl].length >= 10) {
267
331
  return flushMessages(qrl, opt)
268
332
  }
@@ -306,16 +370,11 @@ export async function enqueueBatch (pairs, options) {
306
370
  // After we've prefetched, all qrls are in cache
307
371
  // so go back through the list of pairs and fire off messages
308
372
  requestCount = 0
309
- const addMessagePromises = []
373
+ let initialFlushTotal = 0
310
374
  for (const { qname, command } of normalizedPairs) {
311
375
  const qrl = await getOrCreateQueue(qname, opt)
312
- addMessagePromises.push(addMessage(qrl, command, opt))
376
+ initialFlushTotal += await addMessage(qrl, command, opt)
313
377
  }
314
- const flushCounts = await Promise.all(addMessagePromises)
315
-
316
- // Count up how many were flushed during add
317
- debug('flushCounts', flushCounts)
318
- const initialFlushTotal = flushCounts.reduce((a, b) => a + b, 0)
319
378
 
320
379
  // And flush any remaining messages
321
380
  const extraFlushPromises = []
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Exponential backoff controller.
3
+ * usage:
4
+ * const exp = new ExponentialBackoff()
5
+ * const result = await exp.run(
6
+ * function action (attemptNumber) {
7
+ * console.log(attemptNumber) // 1, 2, 3, ...
8
+ * return axios.post(...)
9
+ * },
10
+ * function shouldRetry (returnValue, error) {
11
+ * if (returnValue && return value.code = 500) return true
12
+ * if (error && error.message === 'Internal Server Error') return true
13
+ * }
14
+ * )
15
+ */
16
+
17
+ export class ExponentialBackoff {
18
+ /**
19
+ * Creates various behaviors for backoff.
20
+ * @param {number} maxRetries - Number of times to attempt the action before
21
+ * throwing an error. Defaults to 3.
22
+ * @param {number} maxJitterPercent - Jitter as a percentage of the delay.
23
+ * For example, if the exponential delay is 2 seconds, then a jitter of
24
+ * 0.5 could lead to a delay as low as 1 second and as high as 3 seconds,
25
+ * since 0.5 * 2 = 1. Defaults to 0.5.
26
+ * @param {number} exponentBase - The base for the exponent. Defaults to 2,
27
+ * which means the delay doubles every attempt.
28
+ */
29
+ constructor (maxRetries = 3, maxJitterPercent = 0.5, exponentBase = 2) {
30
+ if (maxRetries < 1) throw new Error('maxRetries must be >= 1')
31
+ if (maxJitterPercent < 0.1 || maxJitterPercent > 1) throw new Error('maxJitterPercent must be in the interval [0.1, 1]')
32
+ if (exponentBase < 1 || exponentBase > 10) throw new Error('exponentBase must be in the range [1, 10]')
33
+ this.maxRetries = parseInt(maxRetries)
34
+ this.maxJitterPercent = parseFloat(maxJitterPercent)
35
+ this.exponentBase = parseFloat(exponentBase)
36
+ this.attemptNumber = 0
37
+ }
38
+
39
+ /**
40
+ * Calculates how many ms to delay based on the current attempt number.
41
+ */
42
+ calculateDelayMs (attemptNumber) {
43
+ const secondsRaw = this.exponentBase ** attemptNumber // 2, 4, 8, 16, ....
44
+ const jitter = this.maxJitterPercent * (Math.random() - 0.5) // [-0.5, 0.5]
45
+ const delayMs = Math.round(secondsRaw * (1 + jitter) * 1000)
46
+ // console.log({ secondsRaw, jitter, delayMs })
47
+ return delayMs
48
+ }
49
+
50
+ /**
51
+ * Resolves after a delay set by the current attempt.
52
+ */
53
+ async delay (attemptNumber) {
54
+ // console.log(attemptNumber)
55
+ const delay = this.calculateDelayMs(attemptNumber)
56
+ // console.log({ function: 'delay', attemptNumber, delay })
57
+ return new Promise((resolve, reject) => setTimeout(resolve, delay))
58
+ }
59
+
60
+ /**
61
+ * Call another function repeatedly, retrying with exponential backoff and
62
+ * jitter if not successful.
63
+ * @param {ExponentialBackoff~action} action - Callback that does the action
64
+ * to be attempted (web request, rpc, database call, etc). Will be called
65
+ * again after the exponential dealy if shouldRetry() returns true.
66
+ * @param {ExponentialBackoff~shouldRetry} shouldRetry - Callback that gets
67
+ * to look at the return value of action() and any potential exception. If
68
+ * this returns true then the action will be retried with the appropriate
69
+ * backoff delay. Defaults to a function that returns true if an exception
70
+ * is thrown.
71
+ */
72
+ async run (
73
+ action = async (attemptNumber) => undefined,
74
+ shouldRetry = async (returnValue, error) => !!error
75
+ ) {
76
+ let attemptNumber = 0
77
+ while (attemptNumber++ < this.maxRetries) {
78
+ try {
79
+ const result = await action(attemptNumber)
80
+ if (await shouldRetry(result, undefined)) {
81
+ if (attemptNumber >= this.maxRetries) throw new Error('Maximum number of attempts reached')
82
+ await this.delay(attemptNumber)
83
+ } else {
84
+ return result
85
+ }
86
+ } catch (e) {
87
+ if (await shouldRetry(undefined, e)) {
88
+ if (attemptNumber >= this.maxRetries) throw e
89
+ await this.delay(attemptNumber)
90
+ } else {
91
+ throw e
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Callback used by run().
99
+ * @callback ExponentialBackoff~action
100
+ * @param {number} attemptNumber - Which attempt this is, i.e. 1, 2, 3, ...
101
+ */
102
+
103
+ /**
104
+ * Callback used by run().
105
+ * @callback ExponentialBackoff~shouldRetry
106
+ * @param returnValue - The value returned by your action. If an exception
107
+ * was thrown by the action then this is undefined.
108
+ * @param error - The exception thrown by your action. If there was no
109
+ * exception, this is undefined.
110
+ */
111
+ }
package/src/idleQueues.js CHANGED
@@ -208,7 +208,7 @@ export async function processQueuePair (qname, qrl, opt) {
208
208
  const normalizeOptions = Object.assign({}, opt, { fifo: isFifo })
209
209
  // Generate fail queue name/url
210
210
  const fqname = normalizeFailQueueName(qname, normalizeOptions)
211
- const fqrl = normalizeFailQueueName(qrl, normalizeOptions)
211
+ const fqrl = normalizeFailQueueName(fqname, normalizeOptions)
212
212
 
213
213
  // Idle check
214
214
  const result = await checkIdle(qname, qrl, opt)
package/src/worker.js CHANGED
@@ -5,7 +5,8 @@
5
5
  import {
6
6
  ChangeMessageVisibilityCommand,
7
7
  ReceiveMessageCommand,
8
- DeleteMessageCommand
8
+ DeleteMessageCommand,
9
+ QueueDoesNotExist
9
10
  } from '@aws-sdk/client-sqs'
10
11
  import { exec } from 'child_process' // node:child_process
11
12
  import treeKill from 'tree-kill'
@@ -208,11 +209,25 @@ export async function listen (queues, options) {
208
209
  chalk.blue(' (' + qrl + ')')
209
210
  )
210
211
  }
211
- // Aggregate the results
212
- const { noJobs, jobsSucceeded, jobsFailed } = await pollForJobs(qname, qrl, opt)
213
- stats.noJobs += noJobs
214
- stats.jobsFailed += jobsFailed
215
- stats.jobsSucceeded += jobsSucceeded
212
+ try {
213
+ // Aggregate the results
214
+ const { noJobs, jobsSucceeded, jobsFailed } = await pollForJobs(qname, qrl, opt)
215
+ stats.noJobs += noJobs
216
+ stats.jobsFailed += jobsFailed
217
+ stats.jobsSucceeded += jobsSucceeded
218
+ } catch (e) {
219
+ if (e instanceof QueueDoesNotExist) {
220
+ if (opt.verbose) {
221
+ console.error(
222
+ chalk.yellow('Warning: Queue ') +
223
+ qname.slice(opt.prefix.length) +
224
+ chalk.yellow(' does not exist.')
225
+ )
226
+ }
227
+ } else {
228
+ throw e
229
+ }
230
+ }
216
231
  }
217
232
  return stats
218
233
  }