qdone 2.0.13-alpha → 2.0.15-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.
@@ -47,7 +47,6 @@ var chalk_1 = __importDefault(require("chalk"));
47
47
  var debug_1 = __importDefault(require("debug"));
48
48
  var qrlCache_js_1 = require("../qrlCache.js");
49
49
  var idleQueues_js_1 = require("../idleQueues.js");
50
- var sqs_js_1 = require("../sqs.js");
51
50
  var debug = (0, debug_1.default)('qdone:queueManager');
52
51
  var QueueManager = /** @class */ (function () {
53
52
  function QueueManager(opt, queues, resolveSeconds) {
@@ -56,72 +55,143 @@ var QueueManager = /** @class */ (function () {
56
55
  this.queues = queues;
57
56
  this.resolveSeconds = resolveSeconds;
58
57
  this.selectedPairs = [];
58
+ this.icehouse = {};
59
59
  this.resolveTimeout = undefined;
60
60
  this.shutdownRequested = false;
61
- this.resolveQueues();
61
+ this.resolvePromise = this.resolveQueues();
62
62
  }
63
+ // Sends a queue to the icehouse, where it waits for a while before being
64
+ // checked again
65
+ QueueManager.prototype.updateIcehouse = function (qrl, emptyReceive) {
66
+ var foundEntry = !!this.icehouse[qrl];
67
+ var _a = this.icehouse[qrl] || {
68
+ lastCheck: new Date(),
69
+ secondsToWait: Math.round(20 + 10 * Math.random()),
70
+ numEmptyReceives: 0 + emptyReceive
71
+ }, lastCheck = _a.lastCheck, secondsToWait = _a.secondsToWait, numEmptyReceives = _a.numEmptyReceives;
72
+ if (emptyReceive) {
73
+ var now = new Date();
74
+ var secondsElapsed = lastCheck - now;
75
+ var minWait = 10;
76
+ var maxWait = 600;
77
+ var baseSeconds = Math.pow(numEmptyReceives, 2) * 20;
78
+ var jitterSeconds = Math.round((Math.random() - 0.5) * baseSeconds);
79
+ var newSecondsToWait = Math.max(minWait, Math.min(maxWait, baseSeconds + jitterSeconds));
80
+ var newEntry = { lastCheck: now, secondsToWait: newSecondsToWait, numEmptyReceives: numEmptyReceives + 1 };
81
+ this.icehouse[qrl] = newEntry;
82
+ if (this.opt.verbose) {
83
+ console.error(chalk_1.default.blue('Sending queue to icehouse'), qrl, chalk_1.default.blue('for'), newSecondsToWait, chalk_1.default.blue('seconds'));
84
+ }
85
+ debug({ foundEntry: foundEntry, newEntry: newEntry, lastCheck: lastCheck, secondsToWait: secondsToWait, now: now, secondsElapsed: secondsElapsed, maxWait: maxWait, minWait: minWait, baseSeconds: baseSeconds, jitterSeconds: jitterSeconds });
86
+ }
87
+ else {
88
+ delete this.icehouse[qrl];
89
+ }
90
+ };
91
+ // Returns true if the queue should be kept in the icehouse
92
+ QueueManager.prototype.keepInIcehouse = function (qrl, now) {
93
+ if (this.icehouse[qrl]) {
94
+ var _a = this.icehouse[qrl], lastCheck = _a.lastCheck, secondsToWait = _a.secondsToWait;
95
+ var secondsElapsed = Math.round((now - lastCheck) / 1000);
96
+ debug({ icehouseCheck: { qrl: qrl, lastCheck: lastCheck, secondsToWait: secondsToWait, secondsElapsed: secondsElapsed } });
97
+ var letOut = secondsElapsed > secondsToWait;
98
+ if (letOut && this.opt.verbose) {
99
+ console.error(chalk_1.default.blue('Coming out of icehouse:'), qrl);
100
+ }
101
+ return !letOut;
102
+ }
103
+ else {
104
+ return false;
105
+ }
106
+ };
63
107
  QueueManager.prototype.resolveQueues = function () {
64
108
  return __awaiter(this, void 0, void 0, function () {
65
- var qnames, pairs, activePairs;
109
+ var qnames, pairs, now, filteredPairs, activePairs;
66
110
  var _this = this;
67
111
  return __generator(this, function (_a) {
68
112
  switch (_a.label) {
69
113
  case 0:
114
+ clearTimeout(this.resolveTimeout);
70
115
  if (this.shutdownRequested)
71
116
  return [2 /*return*/];
72
- // Start processing
73
- if (this.opt.verbose)
74
- console.error(chalk_1.default.blue('Resolving queues: ') + this.queues.join(' '));
75
117
  qnames = this.queues.map(function (queue) { return (0, qrlCache_js_1.normalizeQueueName)(queue, _this.opt); });
76
118
  return [4 /*yield*/, (0, qrlCache_js_1.getQnameUrlPairs)(qnames, this.opt)];
77
119
  case 1:
78
120
  pairs = _a.sent();
121
+ if (this.opt.verbose)
122
+ console.error(chalk_1.default.blue('Resolving queues:'));
79
123
  if (this.shutdownRequested)
80
124
  return [2 /*return*/];
125
+ now = new Date();
126
+ filteredPairs = pairs
127
+ // first failed
128
+ .filter(function (_a) {
129
+ var qname = _a.qname, qrl = _a.qrl;
130
+ var suf = _this.opt.failSuffix + (_this.opt.fifo ? '.fifo' : '');
131
+ var isFailQueue = qname.slice(-suf.length) === suf;
132
+ return _this.opt.includeFailed ? true : !isFailQueue;
133
+ })
134
+ // first fifo
135
+ .filter(function (_a) {
136
+ var qname = _a.qname, qrl = _a.qrl;
137
+ var isFifo = qname.endsWith('.fifo');
138
+ return _this.opt.fifo ? isFifo : !isFifo;
139
+ })
140
+ // then icehouse
141
+ .filter(function (_a) {
142
+ var qname = _a.qname, qrl = _a.qrl;
143
+ return !_this.keepInIcehouse(qrl, now);
144
+ });
81
145
  activePairs = [];
82
146
  if (!this.opt.activeOnly) return [3 /*break*/, 3];
83
- debug({ pairsBeforeCheck: pairs });
84
- return [4 /*yield*/, Promise.all(pairs.map(function (pair) { return __awaiter(_this, void 0, void 0, function () {
85
- var idle;
86
- return __generator(this, function (_a) {
87
- switch (_a.label) {
88
- case 0: return [4 /*yield*/, (0, idleQueues_js_1.cheapIdleCheck)(pair.qname, pair.qrl, this.opt)];
89
- case 1:
90
- idle = (_a.sent()).idle;
91
- if (!idle)
92
- activePairs.push(pair);
93
- return [2 /*return*/];
94
- }
147
+ if (this.opt.verbose) {
148
+ console.error(chalk_1.default.blue(' checking active only'));
149
+ }
150
+ return [4 /*yield*/, Promise.all(filteredPairs.map(function (_a) {
151
+ var qname = _a.qname, qrl = _a.qrl;
152
+ return __awaiter(_this, void 0, void 0, function () {
153
+ var idle;
154
+ return __generator(this, function (_b) {
155
+ switch (_b.label) {
156
+ case 0: return [4 /*yield*/, (0, idleQueues_js_1.cheapIdleCheck)(qname, qrl, this.opt)];
157
+ case 1:
158
+ idle = (_b.sent()).result.idle;
159
+ debug({ idle: idle, qname: qname });
160
+ if (!idle)
161
+ activePairs.push({ qname: qname, qrl: qrl });
162
+ return [2 /*return*/];
163
+ }
164
+ });
95
165
  });
96
- }); }))];
166
+ }))];
97
167
  case 2:
98
168
  _a.sent();
99
169
  _a.label = 3;
100
170
  case 3:
101
171
  if (this.shutdownRequested)
102
172
  return [2 /*return*/];
103
- // Finished resolving
104
- if (this.opt.verbose) {
105
- console.error(chalk_1.default.blue(' done'));
106
- console.error();
107
- }
108
173
  // Figure out which queues we want to listen on, choosing between active and
109
174
  // all, filtering out failed queues if the user wants that
110
- this.selectedPairs = (this.opt.activeOnly ? activePairs : pairs)
111
- .filter(function (_a) {
112
- var qname = _a.qname;
113
- var suf = _this.opt.failSuffix + (_this.opt.fifo ? '.fifo' : '');
114
- var isFailQueue = qname.slice(-suf.length) === suf;
115
- var shouldInclude = _this.opt.includeFailed ? true : !isFailQueue;
116
- return shouldInclude;
117
- });
175
+ this.selectedPairs = (this.opt.activeOnly ? activePairs : filteredPairs);
118
176
  // Randomize order
119
177
  this.selectedPairs.sort(function () { return 0.5 - Math.random(); });
178
+ if (this.opt.verbose)
179
+ console.error(chalk_1.default.blue(' selected:\n ') + this.selectedPairs.map(function (_a) {
180
+ var qname = _a.qname;
181
+ return qname;
182
+ }).join('\n '));
120
183
  debug('selectedPairs', this.selectedPairs);
184
+ // Finished resolving
185
+ if (this.opt.verbose) {
186
+ console.error(chalk_1.default.blue(' done'));
187
+ console.error();
188
+ }
121
189
  if (this.opt.verbose) {
122
190
  console.error(chalk_1.default.blue('Will resolve queues again in ' + this.resolveSeconds + ' seconds'));
123
191
  }
124
- this.resolveTimeout = setTimeout(function () { return _this.resolveQueues(); }, this.resolveSeconds * 1000);
192
+ this.resolveTimeout = setTimeout(function () {
193
+ _this.resolvePromise = _this.resolveQueues();
194
+ }, this.resolveSeconds * 1000);
125
195
  return [2 /*return*/];
126
196
  }
127
197
  });
@@ -134,11 +204,27 @@ var QueueManager = /** @class */ (function () {
134
204
  return pair;
135
205
  };
136
206
  QueueManager.prototype.getPairs = function () {
137
- return this.selectedPairs;
207
+ var _this = this;
208
+ var now = new Date();
209
+ return this.selectedPairs.filter(function (_a) {
210
+ var qname = _a.qname, qrl = _a.qrl;
211
+ return !_this.keepInIcehouse(qrl, now);
212
+ });
138
213
  };
139
214
  QueueManager.prototype.shutdown = function () {
140
- clearTimeout(this.resolveTimeout);
141
- this.shutdownRequested = true;
215
+ return __awaiter(this, void 0, void 0, function () {
216
+ return __generator(this, function (_a) {
217
+ switch (_a.label) {
218
+ case 0:
219
+ this.shutdownRequested = true;
220
+ clearTimeout(this.resolveTimeout);
221
+ return [4 /*yield*/, this.resolvePromise];
222
+ case 1:
223
+ _a.sent();
224
+ return [2 /*return*/];
225
+ }
226
+ });
227
+ });
142
228
  };
143
229
  return QueueManager;
144
230
  }());
@@ -6,59 +6,43 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.SystemMonitor = void 0;
8
8
  var SystemMonitor = /** @class */ (function () {
9
- function SystemMonitor(opt, smoothingFactor, reportSeconds) {
10
- if (smoothingFactor === void 0) { smoothingFactor = 0.5; }
11
- if (reportSeconds === void 0) { reportSeconds = 5; }
12
- this.opt = opt;
13
- this.smoothingFactor = smoothingFactor;
9
+ function SystemMonitor(reportCallback, reportSeconds) {
10
+ if (reportSeconds === void 0) { reportSeconds = 1; }
11
+ this.reportCallback = reportCallback || console.log;
14
12
  this.reportSeconds = reportSeconds;
15
- this.measurements = {
16
- setTimeout: [],
17
- setImmediate: []
18
- };
19
- this.timeouts = {
20
- setTimeout: undefined,
21
- setImmediate: undefined,
22
- reportLatency: undefined
23
- };
24
- this.measureLatencySetTimeout();
13
+ this.measurements = [];
14
+ this.measure();
25
15
  this.reportLatency();
26
16
  }
27
- SystemMonitor.prototype.measureLatencySetTimeout = function () {
17
+ SystemMonitor.prototype.measure = function () {
28
18
  var _this = this;
19
+ clearTimeout(this.measureTimeout);
29
20
  var start = new Date();
30
- this.timeouts.setTimeout = setTimeout(function () {
21
+ this.measureTimeout = setTimeout(function () {
31
22
  var latency = new Date() - start;
32
- _this.measurements.setTimeout.push(latency);
33
- if (_this.measurements.setTimeout.length > 1000)
34
- _this.measurements.setTimeout.shift();
35
- _this.measureLatencySetTimeout();
23
+ _this.measurements.push(latency);
24
+ if (_this.measurements.length > 1000)
25
+ _this.measurements.shift();
26
+ _this.measure();
36
27
  });
37
28
  };
38
29
  SystemMonitor.prototype.getLatency = function () {
39
- var results = {};
40
- for (var k in this.measurements) {
41
- var values = this.measurements[k];
42
- results[k] = values.length ? values.reduce(function (a, b) { return a + b; }, 0) / values.length : 0;
43
- }
44
- return results;
30
+ return this.measurements.length ? this.measurements.reduce(function (a, b) { return a + b; }, 0) / this.measurements.length : 0;
45
31
  };
46
32
  SystemMonitor.prototype.reportLatency = function () {
47
33
  var _this = this;
48
- this.timeouts.reportLatency = setTimeout(function () {
49
- var _a;
50
- for (var k in _this.measurements) {
51
- var values = _this.measurements[k];
52
- var mean = values.length ? values.reduce(function (a, b) { return a + b; }, 0) / values.length : 0;
53
- console.log((_a = {}, _a[k] = mean, _a));
54
- }
34
+ clearTimeout(this.reportTimeout);
35
+ this.reportTimeout = setTimeout(function () {
36
+ var latency = _this.getLatency();
37
+ // console.log({ latency })
38
+ if (_this.reportCallback)
39
+ _this.reportCallback(latency);
55
40
  _this.reportLatency();
56
41
  }, this.reportSeconds * 1000);
57
42
  };
58
43
  SystemMonitor.prototype.shutdown = function () {
59
- console.log(this.measurements);
60
- for (var k in this.timeouts)
61
- clearTimeout(this.timeouts[k]);
44
+ clearTimeout(this.measureTimeout);
45
+ clearTimeout(this.reportTimeout);
62
46
  };
63
47
  return SystemMonitor;
64
48
  }());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.0.13-alpha",
3
+ "version": "2.0.15-alpha",
4
4
  "description": "Language agnostic job queue for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/consumer.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Consumer implementation.
3
3
  */
4
4
 
5
- import { ReceiveMessageCommand } from '@aws-sdk/client-sqs'
5
+ import { ReceiveMessageCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
6
6
  import chalk from 'chalk'
7
7
  import Debug from 'debug'
8
8
 
@@ -18,12 +18,12 @@ const debug = Debug('qdone:consumer')
18
18
  let shutdownRequested = false
19
19
  const shutdownCallbacks = []
20
20
 
21
- export function requestShutdown () {
21
+ export async function requestShutdown () {
22
22
  debug('requestShutdown')
23
23
  shutdownRequested = true
24
24
  for (const callback of shutdownCallbacks) {
25
25
  debug('callback', callback)
26
- callback()
26
+ await callback()
27
27
  // try { callback() } catch (e) { }
28
28
  }
29
29
  debug('requestShutdown done')
@@ -35,7 +35,7 @@ export async function getMessages (qrl, opt, maxMessages) {
35
35
  MaxNumberOfMessages: maxMessages,
36
36
  MessageAttributeNames: ['All'],
37
37
  QueueUrl: qrl,
38
- VisibilityTimeout: 30,
38
+ VisibilityTimeout: 60,
39
39
  WaitTimeSeconds: opt.waitTime
40
40
  }
41
41
  const response = await getSQSClient().send(new ReceiveMessageCommand(params))
@@ -47,19 +47,21 @@ export async function getMessages (qrl, opt, maxMessages) {
47
47
  // Consumer
48
48
  //
49
49
  export async function processMessages (queues, callback, options) {
50
+ debug({ options })
50
51
  const opt = getOptionsWithDefaults(options)
51
- debug('processMessages', { queues, callback, options, opt })
52
+ debug('processMessages', { queues, callback, options, opt, argv: process.argv })
52
53
 
53
- const systemMonitor = new SystemMonitor(opt)
54
- const jobExecutor = new JobExecutor(opt)
55
- const queueManager = new QueueManager(opt, queues)
56
- debug({ systemMonitor, jobExecutor, queueManager })
57
-
58
- shutdownCallbacks.push(() => {
59
- systemMonitor.shutdown()
60
- queueManager.shutdown()
61
- jobExecutor.shutdown()
54
+ let lastLatency = 10
55
+ const systemMonitor = new SystemMonitor(latency => {
56
+ const percentDifference = 100 * Math.abs(lastLatency - latency) / lastLatency
57
+ if (percentDifference > 10 && opt.verbose) {
58
+ console.error(chalk.blue('Latency:', Math.round(latency), 'ms'))
59
+ }
60
+ lastLatency = latency
62
61
  })
62
+ const jobExecutor = new JobExecutor(opt)
63
+ const queueManager = new QueueManager(opt, queues, 60)
64
+ // debug({ systemMonitor, jobExecutor, queueManager })
63
65
 
64
66
  // This delay function keeps a timeout reference around so it can be
65
67
  // cancelled at shutdown
@@ -68,31 +70,58 @@ export async function processMessages (queues, callback, options) {
68
70
  delayTimeout = setTimeout(resolve, ms)
69
71
  })
70
72
 
73
+ shutdownCallbacks.push(async () => {
74
+ clearTimeout(delayTimeout)
75
+ await queueManager.shutdown()
76
+ debug({ queueManager: 'done' })
77
+ await jobExecutor.shutdown()
78
+ debug({ jobExecutor: 'done' })
79
+ await systemMonitor.shutdown()
80
+ debug({ systemMonitor: 'done' })
81
+ })
82
+
71
83
  // Keep track of how many messages could be returned from each queue
72
84
  const activeQrls = new Set()
73
85
  let maxReturnCount = 0
74
86
  const listen = async (qname, qrl, maxMessages) => {
75
87
  activeQrls.add(qrl)
76
88
  maxReturnCount += maxMessages
77
- const messages = await getMessages(qrl, opt, maxMessages)
78
- for (const message of messages) {
79
- jobExecutor.executeJob(message, callback, qname, qrl)
89
+ try {
90
+ const messages = await getMessages(qrl, opt, maxMessages)
91
+ if (messages.length) {
92
+ for (const message of messages) {
93
+ jobExecutor.executeJob(message, callback, qname, qrl)
94
+ }
95
+ queueManager.updateIcehouse(qrl, false)
96
+ } else {
97
+ // If we didn't get any, update the icehouse so we can back off
98
+ queueManager.updateIcehouse(qrl, true)
99
+ }
100
+
101
+ // Max job accounting
102
+ maxReturnCount -= maxMessages
103
+ activeQrls.delete(qrl)
104
+ } catch (e) {
105
+ // If the queue has been cleaned up, we should back off anyway
106
+ if (e instanceof QueueDoesNotExist) {
107
+ queueManager.updateIcehouse(qrl, true)
108
+ } else {
109
+ throw e
110
+ }
80
111
  }
81
- maxReturnCount -= maxMessages
82
- activeQrls.delete(qrl)
83
112
  }
84
113
 
85
114
  while (!shutdownRequested) { // eslint-disable-line
86
115
  // Figure out how we are running
87
- const allowedJobs = opt.maxConcurrentJobs - jobExecutor.activeJobCount() - maxReturnCount
116
+ const allowedJobs = Math.max(0, opt.maxConcurrentJobs - jobExecutor.activeJobCount() - maxReturnCount)
88
117
  const maxLatency = 100
89
- const latency = systemMonitor.getLatency().setTimeout
118
+ const latency = systemMonitor.getLatency() || 10
90
119
  const latencyFactor = 1 - Math.abs(Math.min(latency / maxLatency, 1)) // 0 if latency is at max, 1 if latency 0
91
120
  const targetJobs = Math.round(allowedJobs * latencyFactor)
92
- debug({ allowedJobs, maxLatency, latency, latencyFactor, targetJobs, activeQrls })
93
-
94
121
  let jobsLeft = targetJobs
122
+ debug({ jobCount: jobExecutor.activeJobCount(), maxReturnCount, allowedJobs, maxLatency, latency, latencyFactor, targetJobs, activeQrls })
95
123
  for (const { qname, qrl } of queueManager.getPairs()) {
124
+ debug({ evaluating: { qname, qrl, jobsLeft, activeQrlsHasQrl: activeQrls.has(qrl) } })
96
125
  if (jobsLeft <= 0 || activeQrls.has(qrl)) continue
97
126
  const maxMessages = Math.min(10, jobsLeft)
98
127
  listen(qname, qrl, maxMessages)
package/src/defaults.js CHANGED
@@ -44,6 +44,12 @@ export const defaults = Object.freeze({
44
44
  unpair: false
45
45
  })
46
46
 
47
+ function validateInteger (opt, name) {
48
+ const parsed = parseInt(opt[name], 10)
49
+ if (isNaN(parsed)) throw new Error(`${name} needs to be an integer.`)
50
+ return parsed
51
+ }
52
+
47
53
  /**
48
54
  * This function should be called by each exposed API entry point on the
49
55
  * options passed in from the caller. It supports options named in camelCase
@@ -101,9 +107,22 @@ export function getOptionsWithDefaults (options) {
101
107
  delete: options.delete || defaults.delete,
102
108
  unpair: options.delete || defaults.unpair
103
109
  }
110
+
111
+ // Setting this env here means we don't have to in AWS SDK constructors
104
112
  process.env.AWS_REGION = opt.region
105
113
 
106
- // TODO: validate options
114
+ // Validation
115
+ opt.cacheTtlSeconds = validateInteger(opt, 'cacheTtlSeconds')
116
+ opt.messageRetentionPeriod = validateInteger(opt, 'messageRetentionPeriod')
117
+ opt.delay = validateInteger(opt, 'delay')
118
+ opt.sendRetries = validateInteger(opt, 'sendRetries')
119
+ opt.failDelay = validateInteger(opt, 'failDelay')
120
+ opt.dlqAfter = validateInteger(opt, 'dlqAfter')
121
+ opt.waitTime = validateInteger(opt, 'waitTime')
122
+ opt.killAfter = validateInteger(opt, 'killAfter')
123
+ opt.maxConcurrentJobs = validateInteger(opt, 'maxConcurrentJobs')
124
+ opt.idleFor = validateInteger(opt, 'idleFor')
125
+
107
126
  return opt
108
127
  }
109
128
 
package/src/idleQueues.js CHANGED
@@ -40,7 +40,7 @@ export async function _cheapIdleCheck (qname, qrl, opt) {
40
40
  const client = getSQSClient()
41
41
  const cmd = new GetQueueAttributesCommand({ AttributeNames: attributeNames, QueueUrl: qrl })
42
42
  const data = await client.send(cmd)
43
- // debug('data', data)
43
+ debug('data', data)
44
44
  const result = data.Attributes
45
45
  result.queue = qname.slice(opt.prefix.length)
46
46
  // We are idle if all the messages attributes are zero