qdone 2.0.29-alpha → 2.0.31-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdone",
3
- "version": "2.0.29-alpha",
3
+ "version": "2.0.31-alpha",
4
4
  "description": "Language agnostic job queue for SQS",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -47,7 +47,7 @@
47
47
  "scripts": {
48
48
  "start": "./src/bin.js",
49
49
  "test": "NODE_OPTIONS='--experimental-json-modules --experimental-vm-modules --no-warnings' jest",
50
- "build": "tsc --allowJs index.js --outdir commonjs --esModuleInterop --module commonjs",
50
+ "build": "tsc --allowJs index.js --outdir commonjs --esModuleInterop --module commonjs --target es2021 --strict --skipLibCheck --forceConsistentCasingInFileNames",
51
51
  "clean": "rm -rf commonjs/src commonjs/*.js coverage",
52
52
  "lint": "standard",
53
53
  "coverage": "nyc report --reporter=text-lcov | coveralls",
package/src/consumer.js CHANGED
@@ -96,9 +96,7 @@ export async function processMessages (queues, callback, options) {
96
96
 
97
97
  if (!shutdownRequested) {
98
98
  if (messages.length) {
99
- for (const message of messages) {
100
- jobExecutor.executeJob(message, callback, qname, qrl)
101
- }
99
+ jobExecutor.executeJobs(messages, callback, qname, qrl)
102
100
  queueManager.updateIcehouse(qrl, false)
103
101
  } else {
104
102
  // If we didn't get any, update the icehouse so we can back off
@@ -107,7 +105,7 @@ export async function processMessages (queues, callback, options) {
107
105
  }
108
106
 
109
107
  // Max job accounting
110
- maxReturnCount -= maxMessages
108
+ if (messages.length) maxReturnCount -= messages.length
111
109
  activeQrls.delete(qrl)
112
110
  } catch (e) {
113
111
  // If the queue has been cleaned up, we should back off anyway
@@ -121,7 +119,7 @@ export async function processMessages (queues, callback, options) {
121
119
 
122
120
  while (!shutdownRequested) { // eslint-disable-line
123
121
  // Figure out how we are running
124
- const allowedJobs = Math.max(0, opt.maxConcurrentJobs - jobExecutor.activeJobCount() - maxReturnCount)
122
+ const allowedJobs = Math.max(0, opt.maxConcurrentJobs - jobExecutor.runningJobCount() - maxReturnCount)
125
123
 
126
124
  // Latency
127
125
  const maxLatency = 100
@@ -146,10 +144,11 @@ export async function processMessages (queues, callback, options) {
146
144
  let jobsLeft = targetJobs
147
145
 
148
146
  if (opt.verbose) {
149
- console.error({ maxConcurrentJobs: opt.maxConcurrentJobs, jobCount: jobExecutor.activeJobCount(), allowedJobs, maxLatency, latencyFactor, freememFactor, loadFactor, overallFactor, targetJobs, activeQrls })
147
+ // console.error({ maxConcurrentJobs: opt.maxConcurrentJobs, jobCount: jobExecutor.activeJobCount(), allowedJobs, maxLatency, latencyFactor, freememFactor, loadFactor, overallFactor, targetJobs, activeQrls })
150
148
  }
151
149
  for (const { qname, qrl } of queueManager.getPairs()) {
152
- // debug({ evaluating: { qname, qrl, jobsLeft, activeQrlsHasQrl: activeQrls.has(qrl) } })
150
+ // const qcount = jobExecutor.runningJobCountForQueue(qname)
151
+ // console.log({ evaluating: { qname, qrl, qcount, jobsLeft, activeQrlsHasQrl: activeQrls.has(qrl) } })
153
152
  if (jobsLeft <= 0 || activeQrls.has(qrl)) continue
154
153
  const maxMessages = Math.min(10, jobsLeft)
155
154
  listen(qname, qrl, maxMessages)
package/src/defaults.js CHANGED
@@ -26,8 +26,8 @@ export const defaults = Object.freeze({
26
26
  messageRetentionPeriod: 1209600,
27
27
  delay: 0,
28
28
  sendRetries: 6,
29
- failDelay: 0,
30
- dlq: false,
29
+ failDelay: 120,
30
+ dlq: true,
31
31
  dlqSuffix: '_dead',
32
32
  dlqAfter: 3,
33
33
 
@@ -90,7 +90,7 @@ export function getOptionsWithDefaults (options) {
90
90
  delay: options.delay || defaults.delay,
91
91
  sendRetries: options['send-retries'] || defaults.sendRetries,
92
92
  failDelay: options.failDelay || options['fail-delay'] || defaults.failDelay,
93
- dlq: dlq || defaults.dlq,
93
+ dlq: dlq === false ? false : (dlq || defaults.dlq),
94
94
  dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || defaults.dlqSuffix,
95
95
  dlqAfter: options.dlqAfter || options['dlq-after'] || defaults.dlqAfter,
96
96
  tags: options.tags || undefined,
package/src/enqueue.js CHANGED
@@ -79,7 +79,7 @@ export async function getOrCreateFailQueue (queue, opt) {
79
79
  maxReceiveCount: opt.dlqAfter + ''
80
80
  })
81
81
  }
82
- if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay
82
+ if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay + ''
83
83
  if (opt.tags) params.tags = opt.tags
84
84
  if (opt.fifo) params.Attributes.FifoQueue = 'true'
85
85
  const cmd = new CreateQueueCommand(params)
@@ -19,8 +19,11 @@ export class JobExecutor {
19
19
  this.opt = opt
20
20
  this.jobs = []
21
21
  this.jobsByMessageId = {}
22
+ this.jobsByQueue = new Map()
22
23
  this.stats = {
23
24
  activeJobs: 0,
25
+ waitingJobs: 0,
26
+ runningJobs: 0,
24
27
  sqsCalls: 0,
25
28
  timeoutsExtended: 0,
26
29
  jobsSucceeded: 0,
@@ -46,6 +49,20 @@ export class JobExecutor {
46
49
  return this.stats.activeJobs
47
50
  }
48
51
 
52
+ runningJobCount () {
53
+ return this.stats.runningJobs
54
+ }
55
+
56
+ /**
57
+ * Returns the number of jobs running in a queue.
58
+ */
59
+ runningJobCountForQueue (qname) {
60
+ const jobs = this.jobsByQueue.get(qname) || new Set()
61
+ let runningCount = 0
62
+ for (const job of jobs.values()) runningCount += job.status === 'running'
63
+ return runningCount
64
+ }
65
+
49
66
  /**
50
67
  * Changes message visibility on all running jobs using as few calls as possible.
51
68
  */
@@ -71,15 +88,12 @@ export class JobExecutor {
71
88
  const jobsToDeleteByQrl = {}
72
89
  const jobsToCleanup = new Set()
73
90
 
74
- if (this.opt.verbose) {
75
- console.error(chalk.blue('Stats: '), this.stats)
76
- console.error(chalk.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })))
77
- }
78
-
79
91
  // Build list of jobs we need to deal with
92
+ const jobStatuses = {}
80
93
  for (let i = 0; i < this.jobs.length; i++) {
81
94
  const job = this.jobs[i]
82
95
  const jobRunTime = Math.round((start - job.start) / 1000)
96
+ jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1
83
97
  // debug('considering job', job)
84
98
  if (job.status === 'complete') {
85
99
  const jobsToDelete = jobsToDeleteByQrl[job.qrl] || []
@@ -88,7 +102,8 @@ export class JobExecutor {
88
102
  jobsToDeleteByQrl[job.qrl] = jobsToDelete
89
103
  } else if (job.status === 'failed') {
90
104
  jobsToCleanup.add(job)
91
- } else if (job.status === 'processing') {
105
+ } else if (job.status !== 'deleting') {
106
+ // Any other job state gets visibility accounting
92
107
  debug('processing', { job, jobRunTime })
93
108
  if (jobRunTime >= job.extendAtSecond) {
94
109
  // Add it to our organized list of jobs
@@ -106,7 +121,11 @@ export class JobExecutor {
106
121
  }
107
122
  }
108
123
  }
109
- // debug('maintainVisibility', { jobsToDeleteByQrl, jobsToExtendByQrl })
124
+
125
+ if (this.opt.verbose) {
126
+ console.error(chalk.blue('Stats: '), { stats: this.stats, jobStatuses })
127
+ console.error(chalk.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })))
128
+ }
110
129
 
111
130
  // Extend in batches for each queue
112
131
  for (const qrl in jobsToExtendByQrl) {
@@ -199,7 +218,9 @@ export class JobExecutor {
199
218
  this.jobs = this.jobs.filter(job => {
200
219
  if (job.status === 'deleting' || job.status === 'failed') {
201
220
  debug('removed', job.message.MessageId)
221
+ // Accounting
202
222
  delete this.jobsByMessageId[job.message.MessageId]
223
+ this.jobsByQueue.get(job.qname).delete(job)
203
224
  return false
204
225
  } else {
205
226
  return true
@@ -207,20 +228,19 @@ export class JobExecutor {
207
228
  })
208
229
  }
209
230
 
210
- async executeJob (message, callback, qname, qrl, failedCallback) {
211
- if (this.shutdownRequested) throw new Error('jobExecutor is shutting down so cannot execute new job')
231
+ addJob (message, callback, qname, qrl) {
212
232
  // Create job entry and track it
213
- const payload = this.opt.json ? JSON.parse(message.Body) : message.Body
214
- const visibilityTimeout = 60
233
+ const defaultVisibilityTimeout = 60
215
234
  const job = {
216
- status: 'processing',
235
+ status: 'waiting',
217
236
  start: new Date(),
218
- visibilityTimeout,
219
- extendAtSecond: visibilityTimeout / 2,
237
+ visibilityTimeout: defaultVisibilityTimeout,
238
+ extendAtSecond: defaultVisibilityTimeout / 2,
220
239
  payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
221
240
  message,
222
241
  callback,
223
242
  qname,
243
+ prettyQname: qname.slice(this.opt.prefix.length),
224
244
  qrl
225
245
  }
226
246
 
@@ -237,29 +257,51 @@ export class JobExecutor {
237
257
  throw e
238
258
  }
239
259
 
240
- // debug('executeJob', job)
260
+ // Accounting
241
261
  this.jobs.push(job)
242
262
  this.jobsByMessageId[job.message.MessageId] = job
263
+
264
+ // Track all jobs for each queue
265
+ if (!this.jobsByQueue.has(job.qname)) this.jobsByQueue.set(job.qname, new Set())
266
+ this.jobsByQueue.get(job.qname).add(job)
267
+
243
268
  this.stats.activeJobs++
269
+ this.stats.waitingJobs++
244
270
  if (this.opt.verbose) {
245
- console.error(chalk.blue('Executing:'), qname, chalk.blue('-->'), job.payload)
271
+ console.error(chalk.blue('Got message:'), job.prettyQname, chalk.blue('-->'), job.payload, job.message.MessageId)
246
272
  } else if (!this.opt.disableLog) {
247
273
  console.log(JSON.stringify({
248
- event: 'MESSAGE_PROCESSING_START',
274
+ event: 'MESSAGE_RECEIVED',
249
275
  timestamp: new Date(),
250
- qrl,
276
+ queue: job.qname,
251
277
  messageId: message.MessageId,
252
278
  payload: job.payload
253
279
  }))
254
280
  }
281
+ return job
282
+ }
255
283
 
256
- // Execute job
284
+ async runJob (job) {
257
285
  try {
258
- const queue = qname.slice(this.opt.prefix.length)
259
- const result = await callback(queue, payload)
260
- debug('executeJob callback finished', { payload, result })
261
286
  if (this.opt.verbose) {
262
- console.error(chalk.green('SUCCESS'), message.Body)
287
+ console.error(chalk.blue('Running:'), job.prettyQname, chalk.blue('-->'), job.payload, job.message.MessageId)
288
+ } else if (!this.opt.disableLog) {
289
+ console.log(JSON.stringify({
290
+ event: 'MESSAGE_PROCESSING_START',
291
+ timestamp: new Date(),
292
+ queue: job.qname,
293
+ messageId: job.message.MessageId,
294
+ payload: job.payload
295
+ }))
296
+ }
297
+ job.status = 'running'
298
+ this.stats.runningJobs++
299
+ this.stats.waitingJobs--
300
+ const queue = job.qname.slice(this.opt.prefix.length)
301
+ const result = await job.callback(queue, job.payload)
302
+ debug('executeJob callback finished', { payload: job.payload, result })
303
+ if (this.opt.verbose) {
304
+ console.error(chalk.green('SUCCESS'), job.payload)
263
305
  }
264
306
  job.status = 'complete'
265
307
 
@@ -269,35 +311,57 @@ export class JobExecutor {
269
311
  } else if (!this.opt.disableLog) {
270
312
  console.log(JSON.stringify({
271
313
  event: 'MESSAGE_PROCESSING_COMPLETE',
314
+ queue: job.qname,
272
315
  timestamp: new Date(),
273
- messageId: message.MessageId,
274
- payload
316
+ messageId: job.message.MessageId,
317
+ payload: job.payload
275
318
  }))
276
319
  }
277
320
  this.stats.jobsSucceeded++
278
321
  } catch (err) {
279
- // Notify caller that we failed
280
- if (failedCallback) failedCallback(message, qname, qrl)
322
+ job.status = 'failed'
323
+ this.stats.jobsFailed++
281
324
  // Fail path for job execution
282
325
  if (this.opt.verbose) {
283
- console.error(chalk.red('FAILED'), message.Body)
326
+ console.error(chalk.red('FAILED'), job.payload)
284
327
  console.error(chalk.blue(' error : ') + err)
285
328
  } else if (!this.opt.disableLog) {
286
329
  // Production error logging
287
330
  console.log(JSON.stringify({
288
331
  event: 'MESSAGE_PROCESSING_FAILED',
289
332
  reason: 'exception thrown',
290
- qrl,
333
+ queue: job.qname,
291
334
  timestamp: new Date(),
292
- messageId: message.MessageId,
293
- payload,
335
+ messageId: job.message.MessageId,
336
+ payload: job.payload,
294
337
  errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
295
338
  err
296
339
  }))
297
340
  }
298
- job.status = 'failed'
299
- this.stats.jobsFailed++
300
341
  }
301
342
  this.stats.activeJobs--
343
+ this.stats.runningJobs--
344
+ }
345
+
346
+ async executeJobs (messages, callback, qname, qrl) {
347
+ if (this.shutdownRequested) throw new Error('jobExecutor is shutting down so cannot execute new jobs')
348
+
349
+ // Begin tracking jobs
350
+ const jobs = messages.map(message => this.addJob(message, callback, qname, qrl))
351
+ const isFifo = qrl.endsWith('.fifo')
352
+
353
+ // console.log(jobs)
354
+
355
+ // Begin executing
356
+ for (const [job, i] of jobs.map((job, i) => [job, i])) {
357
+ // Figure out if the next job needs to happen in serial, otherwise we can parallel execute
358
+ const nextJob = jobs[i + 1]
359
+ const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId
360
+
361
+ // console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
362
+ // Execute serial or parallel
363
+ if (nextJobIsSerial) await this.runJob(job)
364
+ else this.runJob(job)
365
+ }
302
366
  }
303
367
  }
@@ -35,7 +35,7 @@ export class QueueManager {
35
35
  const now = new Date()
36
36
  const secondsElapsed = lastCheck - now
37
37
  const minWait = 10
38
- const maxWait = 600
38
+ const maxWait = 120
39
39
  const baseSeconds = numEmptyReceives ** 2 * 20
40
40
  const jitterSeconds = Math.round((Math.random() - 0.5) * baseSeconds)
41
41
  const newSecondsToWait = Math.max(minWait, Math.min(maxWait, baseSeconds + jitterSeconds))