qdone 2.0.12-alpha → 2.0.14-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
  });
@@ -137,8 +207,19 @@ var QueueManager = /** @class */ (function () {
137
207
  return this.selectedPairs;
138
208
  };
139
209
  QueueManager.prototype.shutdown = function () {
140
- clearTimeout(this.resolveTimeout);
141
- this.shutdownRequested = true;
210
+ return __awaiter(this, void 0, void 0, function () {
211
+ return __generator(this, function (_a) {
212
+ switch (_a.label) {
213
+ case 0:
214
+ this.shutdownRequested = true;
215
+ clearTimeout(this.resolveTimeout);
216
+ return [4 /*yield*/, this.resolvePromise];
217
+ case 1:
218
+ _a.sent();
219
+ return [2 /*return*/];
220
+ }
221
+ });
222
+ });
142
223
  };
143
224
  return QueueManager;
144
225
  }());
@@ -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.12-alpha",
3
+ "version": "2.0.14-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,20 +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
+ 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
61
+ })
54
62
  const jobExecutor = new JobExecutor(opt)
55
63
  const queueManager = new QueueManager(opt, queues)
56
- const maxActiveJobs = 100
57
- debug({ systemMonitor, jobExecutor, queueManager })
58
-
59
- shutdownCallbacks.push(() => {
60
- systemMonitor.shutdown()
61
- queueManager.shutdown()
62
- jobExecutor.shutdown()
63
- })
64
+ // debug({ systemMonitor, jobExecutor, queueManager })
64
65
 
65
66
  // This delay function keeps a timeout reference around so it can be
66
67
  // cancelled at shutdown
@@ -69,28 +70,55 @@ export async function processMessages (queues, callback, options) {
69
70
  delayTimeout = setTimeout(resolve, ms)
70
71
  })
71
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
+
72
83
  // Keep track of how many messages could be returned from each queue
73
84
  const activeQrls = new Set()
74
85
  let maxReturnCount = 0
75
86
  const listen = async (qname, qrl, maxMessages) => {
76
87
  activeQrls.add(qrl)
77
88
  maxReturnCount += maxMessages
78
- const messages = await getMessages(qrl, opt, maxMessages)
79
- for (const message of messages) {
80
- 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
+ }
81
111
  }
82
- maxReturnCount -= maxMessages
83
- activeQrls.delete(qrl)
84
112
  }
85
113
 
86
114
  while (!shutdownRequested) { // eslint-disable-line
87
115
  // Figure out how we are running
88
- const allowedJobs = maxActiveJobs - jobExecutor.activeJobCount() - maxReturnCount
116
+ const allowedJobs = opt.maxConcurrentJobs - jobExecutor.activeJobCount() - maxReturnCount
89
117
  const maxLatency = 100
90
118
  const latency = systemMonitor.getLatency().setTimeout
91
119
  const latencyFactor = 1 - Math.abs(Math.min(latency / maxLatency, 1)) // 0 if latency is at max, 1 if latency 0
92
120
  const targetJobs = Math.round(allowedJobs * latencyFactor)
93
- debug({ allowedJobs, maxLatency, latency, latencyFactor, targetJobs, activeQrls })
121
+ // debug({ allowedJobs, maxLatency, latency, latencyFactor, targetJobs, activeQrls })
94
122
 
95
123
  let jobsLeft = targetJobs
96
124
  for (const { qname, qrl } of queueManager.getPairs()) {
@@ -101,7 +129,7 @@ export async function processMessages (queues, callback, options) {
101
129
  if (opt.verbose) {
102
130
  console.error(chalk.blue('Listening on: '), qname)
103
131
  }
104
- debug({ listenedTo: { qname, maxMessages, jobsLeft } })
132
+ // debug({ listenedTo: { qname, maxMessages, jobsLeft } })
105
133
  }
106
134
  await delay(1000)
107
135
  }
package/src/defaults.js CHANGED
@@ -36,6 +36,7 @@ export const defaults = Object.freeze({
36
36
  killAfter: 30,
37
37
  archive: false,
38
38
  activeOnly: false,
39
+ maxConcurrentJobs: 100,
39
40
 
40
41
  // Idle Queues
41
42
  idleFor: 60,
@@ -43,6 +44,12 @@ export const defaults = Object.freeze({
43
44
  unpair: false
44
45
  })
45
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
+
46
53
  /**
47
54
  * This function should be called by each exposed API entry point on the
48
55
  * options passed in from the caller. It supports options named in camelCase
@@ -93,15 +100,29 @@ export function getOptionsWithDefaults (options) {
93
100
  archive: options.archive || defaults.archive,
94
101
  activeOnly: options.activeOnly || options['active-only'] || defaults.activeOnly,
95
102
  includeFailed: options.includeFailed || options['include-failed'] || defaults.includeFailed,
103
+ maxConcurrentJobs: options.maxConcurrentJobs || defaults.maxConcurrentJobs,
96
104
 
97
105
  // Idle Queues
98
106
  idleFor: options.idleFor || options['idle-for'] || defaults.idleFor,
99
107
  delete: options.delete || defaults.delete,
100
108
  unpair: options.delete || defaults.unpair
101
109
  }
110
+
111
+ // Setting this env here means we don't have to in AWS SDK constructors
102
112
  process.env.AWS_REGION = opt.region
103
113
 
104
- // 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
+
105
126
  return opt
106
127
  }
107
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