qdone 2.0.9-alpha → 2.0.10-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/src/consumer.js CHANGED
@@ -2,31 +2,16 @@
2
2
  * Consumer implementation.
3
3
  */
4
4
 
5
- import {
6
- ChangeMessageVisibilityCommand,
7
- ReceiveMessageCommand,
8
- DeleteMessageCommand
9
- } from '@aws-sdk/client-sqs'
5
+ import { ReceiveMessageCommand } from '@aws-sdk/client-sqs'
10
6
  import chalk from 'chalk'
11
7
  import Debug from 'debug'
12
8
 
13
- import { normalizeQueueName, getQnameUrlPairs } from './qrlCache.js'
14
- import { cheapIdleCheck } from './idleQueues.js'
9
+ import { SystemMonitor } from './scheduler/systemMonitor.js'
10
+ import { QueueManager } from './scheduler/queueManager.js'
11
+ import { JobExecutor } from './scheduler/jobExecutor.js'
15
12
  import { getOptionsWithDefaults } from './defaults.js'
16
13
  import { getSQSClient } from './sqs.js'
17
14
 
18
- //
19
- // Throwing an instance of this Error allows the processMessages callback to
20
- // refuse a message which then gets immediately returned to the queue.
21
- //
22
- // This has the side effect of throtting the queue since it stops polling on
23
- // the queue until the next queue resolution in processMessages.
24
- //
25
- // This is useful for implementing schedulers on top of qdone, for example, to
26
- // look at the queue name and decide whether to take on a new message.
27
- //
28
- export class DoNotProcess extends Error {}
29
-
30
15
  const debug = Debug('qdone:consumer')
31
16
 
32
17
  // Global flag for shutdown request
@@ -44,205 +29,18 @@ export function requestShutdown () {
44
29
  debug('requestShutdown done')
45
30
  }
46
31
 
47
- export async function processMessage (message, callback, qname, qrl, opt) {
48
- debug('processMessage', message, qname, qrl)
49
- const payload = opt.json ? JSON.parse(message.Body) : message.Body
50
- if (opt.verbose) {
51
- console.error(chalk.blue(' Processing payload:'), payload)
52
- } else if (!opt.disableLog) {
53
- console.log(JSON.stringify({
54
- event: 'MESSAGE_PROCESSING_START',
55
- timestamp: new Date(),
56
- messageId: message.MessageId,
57
- payload
58
- }))
59
- }
60
-
61
- const jobStart = new Date()
62
- let visibilityTimeout = 30 // this should be the queue timeout
63
- let timeoutExtender
64
-
65
- async function extendTimeout () {
66
- debug('extendTimeout')
67
- const maxJobRun = 12 * 60 * 60
68
- const jobRunTime = ((new Date()) - jobStart) / 1000
69
- // Double every time, up to max
70
- visibilityTimeout = Math.min(visibilityTimeout * 2, maxJobRun - jobRunTime, opt.killAfter - jobRunTime)
71
- if (opt.verbose) {
72
- console.error(
73
- chalk.blue(' Ran for ') + jobRunTime +
74
- chalk.blue(' seconds, requesting another ') + visibilityTimeout +
75
- chalk.blue(' seconds')
76
- )
77
- }
78
-
79
- try {
80
- const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
81
- QueueUrl: qrl,
82
- ReceiptHandle: message.ReceiptHandle,
83
- VisibilityTimeout: visibilityTimeout
84
- }))
85
- debug('ChangeMessageVisibility returned', result)
86
- if (
87
- jobRunTime + visibilityTimeout >= maxJobRun ||
88
- jobRunTime + visibilityTimeout >= opt.killAfter
89
- ) {
90
- if (opt.verbose) console.error(chalk.yellow(' warning: this is our last time extension'))
91
- } else {
92
- // Extend when we get 50% of the way to timeout
93
- timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
94
- }
95
- } catch (err) {
96
- debug('ChangeMessageVisibility threw', err)
97
- // Rejection means we're ouuta time, whatever, let the job die
98
- if (opt.verbose) {
99
- console.error(chalk.red(' failed to extend job: ') + err)
100
- } else if (!opt.disableLog) {
101
- // Production error logging
102
- console.log(JSON.stringify({
103
- event: 'MESSAGE_PROCESSING_FAILED',
104
- reason: 'ran longer than --kill-after',
105
- timestamp: new Date(),
106
- messageId: message.MessageId,
107
- payload,
108
- errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
109
- err
110
- }))
111
- }
112
- }
113
- }
114
-
115
- // Extend when we get 50% of the way to timeout
116
- timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
117
- debug('timeout', visibilityTimeout * 1000 * 0.5)
118
-
119
- try {
120
- // Process message
121
- const queue = qname.slice(opt.prefix.length)
122
- const result = await callback(queue, payload)
123
- debug('processMessage callback finished', { payload, result })
124
- clearTimeout(timeoutExtender)
125
- if (opt.verbose) {
126
- console.error(chalk.green(' SUCCESS'))
127
- console.error(chalk.blue(' cleaning up (removing message) ...'))
128
- }
129
- await getSQSClient().send(new DeleteMessageCommand({
130
- QueueUrl: qrl,
131
- ReceiptHandle: message.ReceiptHandle
132
- }))
133
- if (opt.verbose) {
134
- console.error(chalk.blue(' done'))
135
- console.error()
136
- } else if (!opt.disableLog) {
137
- console.log(JSON.stringify({
138
- event: 'MESSAGE_PROCESSING_COMPLETE',
139
- timestamp: new Date(),
140
- messageId: message.MessageId,
141
- payload
142
- }))
143
- }
144
- return { noJobs: 0, jobsSucceeded: 1, jobsFailed: 0 }
145
- } catch (err) {
146
- debug('exec.catch')
147
- clearTimeout(timeoutExtender)
148
-
149
- // If the callback does not want to process this message, return to queue
150
- if (err instanceof DoNotProcess) {
151
- if (opt.verbose) {
152
- console.error(chalk.blue(' callback ') + chalk.yellow('REFUSED'))
153
- console.error(chalk.blue(' cleaning up (removing message) ...'))
154
- }
155
- const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
156
- QueueUrl: qrl,
157
- ReceiptHandle: message.ReceiptHandle,
158
- VisibilityTimeout: 0
159
- }))
160
- debug('ChangeMessageVisibility returned', result)
161
- return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
162
- }
163
-
164
- // Fail path for job execution
165
- if (opt.verbose) {
166
- console.error(chalk.red(' FAILED'))
167
- console.error(chalk.blue(' error : ') + err)
168
- } else if (!opt.disableLog) {
169
- // Production error logging
170
- console.log(JSON.stringify({
171
- event: 'MESSAGE_PROCESSING_FAILED',
172
- reason: 'exception thrown',
173
- timestamp: new Date(),
174
- messageId: message.MessageId,
175
- payload,
176
- errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
177
- err
178
- }))
179
- }
180
- return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 1 }
181
- }
182
- }
183
-
184
- //
185
- // Pull work off of a single queue
186
- //
187
- export async function pollSingleQueue (qname, qrl, callback, opt) {
188
- debug('pollSingleQueue', { qname, qrl, callback, opt })
32
+ export async function getMessages (qrl, opt, maxMessages) {
189
33
  const params = {
190
34
  AttributeNames: ['All'],
191
- MaxNumberOfMessages: 1,
35
+ MaxNumberOfMessages: maxMessages,
192
36
  MessageAttributeNames: ['All'],
193
37
  QueueUrl: qrl,
194
38
  VisibilityTimeout: 30,
195
39
  WaitTimeSeconds: opt.waitTime
196
40
  }
197
41
  const response = await getSQSClient().send(new ReceiveMessageCommand(params))
198
- debug('ReceiveMessage response', response)
199
- if (shutdownRequested) return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
200
- if (response.Messages) {
201
- const message = response.Messages[0]
202
- if (opt.verbose) console.error(chalk.blue(' Found message ' + message.MessageId))
203
- return processMessage(message, callback, qname, qrl, opt)
204
- } else {
205
- return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
206
- }
207
- }
208
-
209
- //
210
- // Resolve a set of queues
211
- //
212
- export async function resolveQueues (queues, opt) {
213
- // Start processing
214
- if (opt.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
215
- const qnames = queues.map(queue => normalizeQueueName(queue, opt))
216
- const pairs = await getQnameUrlPairs(qnames, opt)
217
-
218
- // Figure out which pairs are active
219
- const activePairs = []
220
- if (opt.activeOnly) {
221
- debug({ pairsBeforeCheck: pairs })
222
- await Promise.all(pairs.map(async pair => {
223
- const { idle } = await cheapIdleCheck(pair.qname, pair.qrl, opt)
224
- if (!idle) activePairs.push(pair)
225
- }))
226
- }
227
-
228
- // Finished resolving
229
- debug('getQnameUrlPairs.then')
230
- if (opt.verbose) {
231
- console.error(chalk.blue(' done'))
232
- console.error()
233
- }
234
-
235
- // Figure out which queues we want to listen on, choosing between active and
236
- // all, filtering out failed queues if the user wants that
237
- const selectedPairs = (opt.activeOnly ? activePairs : pairs)
238
- .filter(({ qname }) => {
239
- const suf = opt.failSuffix + (opt.fifo ? '.fifo' : '')
240
- const isFailQueue = qname.slice(-suf.length) === suf
241
- const shouldInclude = opt.includeFailed ? true : !isFailQueue
242
- return shouldInclude
243
- })
244
-
245
- return selectedPairs
42
+ // debug('ReceiveMessage response', response)
43
+ return response.Messages || []
246
44
  }
247
45
 
248
46
  //
@@ -252,11 +50,17 @@ export async function processMessages (queues, callback, options) {
252
50
  const opt = getOptionsWithDefaults(options)
253
51
  debug('processMessages', { queues, callback, options, opt })
254
52
 
255
- let loopCounter = 0
256
- const stats = { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
257
- const maxActiveLoops = 10
258
- const activeLoops = new Map()
259
- const completedLoops = new Map()
53
+ const systemMonitor = new SystemMonitor(opt)
54
+ const jobExecutor = new JobExecutor(opt)
55
+ 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
+ })
260
64
 
261
65
  // This delay function keeps a timeout reference around so it can be
262
66
  // cancelled at shutdown
@@ -265,114 +69,41 @@ export async function processMessages (queues, callback, options) {
265
69
  delayTimeout = setTimeout(resolve, ms)
266
70
  })
267
71
 
268
- // Callback to help facilitate better UX at shutdown
269
- function shutdownCallback () {
270
- if (opt.verbose) {
271
- debug({ activeLoops, completedLoops })
272
- if (activeLoops.length) {
273
- console.error(chalk.blue('Waiting for work to finish on the following queues: '))
274
- for (const [id, { qname, qrl, promise }] of activeLoops) {
275
- console.error(' ' + qname + `job ${id}`)
276
- }
277
- }
278
- // clearTimeout(delayTimeout)
279
- }
280
- }
281
- shutdownCallbacks.push(shutdownCallback)
282
-
283
- // Listen to a queue until it is out of messages
284
- async function listenLoop (qname, qrl, loopId) {
285
- try {
286
- if (shutdownRequested) return
287
- if (opt.verbose) {
288
- console.error(
289
- chalk.blue('Looking for work on ') +
290
- qname.slice(opt.prefix.length) +
291
- chalk.blue(' (' + qrl + ')')
292
- )
293
- }
294
- // Aggregate the results
295
- const { noJobs, jobsSucceeded, jobsFailed } = await pollSingleQueue(qname, qrl, callback, opt)
296
- debug('pollSingleQueue return')
297
- stats.noJobs += noJobs
298
- stats.jobsFailed += jobsFailed
299
- stats.jobsSucceeded += jobsSucceeded
300
- debug({ stats, noJobs })
301
-
302
- // No work? Shutdown requested? Return to outer loop
303
- debug({ noJobs, shutdownRequested })
304
- if (noJobs || shutdownRequested) return
305
-
306
- // Otherwise keep going
307
- return listenLoop(qname, qrl)
308
- } catch (err) {
309
- // TODO: Sentry
310
- console.error(chalk.red(' ERROR in listenLoop'))
311
- console.error(chalk.blue(' error : ') + err)
312
- } finally {
313
- completedLoops.set(loopId, activeLoops.get(loopId))
72
+ // Keep track of how many messages could be returned from each queue
73
+ const activeQrls = new Set()
74
+ let maxReturnCount = 0
75
+ const listen = async (qname, qrl, maxMessages) => {
76
+ activeQrls.add(qrl)
77
+ maxReturnCount += maxMessages
78
+ const messages = await getMessages(qrl, opt, maxMessages)
79
+ for (const message of messages) {
80
+ jobExecutor.executeJob(message, callback, qname, qrl)
314
81
  }
82
+ maxReturnCount -= maxMessages
83
+ activeQrls.delete(qrl)
315
84
  }
316
85
 
317
- // Resolve loop
318
86
  while (!shutdownRequested) { // eslint-disable-line
319
- const start = new Date()
320
- const selectedPairs = await resolveQueues(queues, opt)
321
- debug({ selectedPairs })
322
- if (shutdownRequested) break
323
-
324
- // But only if we have queues to listen on
325
- if (selectedPairs.length) {
326
- // Randomize order
327
- selectedPairs.sort(() => 0.5 - Math.random())
328
-
329
- if (opt.verbose) {
330
- console.error(chalk.blue('Listening to queues (in this order):'))
331
- console.error(selectedPairs.map(({ qname, qrl }) =>
332
- ' ' + qname.slice(opt.prefix.length) + chalk.blue(' - ' + qrl)
333
- ).join('\n'))
334
- console.error()
335
- }
336
-
337
- // Launch listen loop for each queue
338
- for (const { qname, qrl } of selectedPairs) {
339
- // Bail if we already have too many
340
- if (activeLoops.size >= maxActiveLoops) {
341
- if (opt.verbose) console.error(chalk.yellow('Hit active worker limit of ') + maxActiveLoops)
342
- break
343
- }
344
- const loopId = loopCounter++
345
- activeLoops.set(loopId, { qname, qrl, promise: listenLoop(qname, qrl, loopId) })
87
+ // Figure out how we are running
88
+ const allowedJobs = maxActiveJobs - jobExecutor.activeJobCount() - maxReturnCount
89
+ const maxLatency = 100
90
+ const latency = systemMonitor.getLatency().setTimeout
91
+ const latencyFactor = 1 - Math.abs(Math.min(latency / maxLatency, 1)) // 0 if latency is at max, 1 if latency 0
92
+ const targetJobs = Math.round(allowedJobs * latencyFactor)
93
+ debug({ allowedJobs, maxLatency, latency, latencyFactor, targetJobs, activeQrls })
94
+
95
+ let jobsLeft = targetJobs
96
+ for (const { qname, qrl } of queueManager.getPairs()) {
97
+ if (jobsLeft <= 0 || activeQrls.has(qrl)) continue
98
+ const maxMessages = Math.min(10, jobsLeft)
99
+ listen(qname, qrl, maxMessages)
100
+ jobsLeft -= maxMessages
101
+ if (this.opt.verbose) {
102
+ console.error(chalk.blue('Listening on: '), qname)
346
103
  }
104
+ debug({ listenedTo: { qname, maxMessages, jobsLeft } })
347
105
  }
348
-
349
- // Wait until the next time we need to resolve
350
- if (!shutdownRequested) {
351
- const msSoFar = Math.max(0, new Date() - start)
352
- const msUntilNextResolve = Math.max(0, /*opt.waitTime **/ 1000 - msSoFar)
353
- debug({ msSoFar, msUntilNextResolve })
354
- if (msUntilNextResolve) {
355
- if (opt.verbose) console.error(chalk.blue('Will resolve queues again in ' + Math.round(msUntilNextResolve / 1000) + ' seconds'))
356
- await delay(msUntilNextResolve)
357
- }
358
- }
359
-
360
- // Cleanup completed loops
361
- for (const [id, { qname, qrl, promise }] of completedLoops) {
362
- await promise // make sure the promise resolves
363
- debug('Cleaning up', { id, qname, qrl, promise })
364
- activeLoops.delete(id)
365
- }
366
- }
367
- debug('out here', { activeLoops })
368
-
369
- // Wait on all work to finish
370
- // shutdownCallback()
371
- for (const [id, { qname, qrl, promise }] of activeLoops) {
372
- debug('Waiting on active loop', id)
373
- await promise // make sure the promise resolves
106
+ await delay(1000)
374
107
  }
375
108
  debug('after all')
376
109
  }
377
-
378
- debug('loaded')
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Component to manage all the currently executing jobs, including extending
3
+ * their visibility timeouts and deleting them when they are successful.
4
+ */
5
+
6
+ import {
7
+ ChangeMessageVisibilityBatchCommand,
8
+ DeleteMessageBatchCommand
9
+ } from '@aws-sdk/client-sqs'
10
+ import chalk from 'chalk'
11
+ import Debug from 'debug'
12
+
13
+ import { getSQSClient } from '../sqs.js'
14
+
15
+ const debug = Debug('qdone:jobExecutor')
16
+
17
+ const maxJobSeconds = 12 * 60 * 60
18
+
19
+ export class JobExecutor {
20
+ constructor (opt) {
21
+ this.opt = opt
22
+ this.jobs = []
23
+ this.stats = {
24
+ activeJobs: 0,
25
+ sqsCalls: 0,
26
+ timeoutsExtended: 0,
27
+ jobsSucceeded: 0,
28
+ jobsFailed: 0,
29
+ jobsDeleted: 0
30
+ }
31
+ this.maintainVisibility()
32
+ debug({ this: this })
33
+ }
34
+
35
+ shutdown () {
36
+ this.shutdownRequested = true
37
+ if (this.stats.activeJobs === 0 && this.jobs.length === 0) {
38
+ clearTimeout(this.maintainVisibilityTimeout)
39
+ }
40
+ }
41
+
42
+ activeJobCount () {
43
+ return this.stats.activeJobs
44
+ }
45
+
46
+ /**
47
+ * Changes message visibility on all running jobs using as few calls as possible.
48
+ */
49
+ async maintainVisibility () {
50
+ debug('maintainVisibility', this.jobs)
51
+ const now = new Date()
52
+ const jobsToExtendByQrl = new Map()
53
+ const jobsToDeleteByQrl = new Map()
54
+ const jobsToCleanup = new Set()
55
+
56
+ if (this.opt.verbose) {
57
+ console.error(chalk.blue('Stats: '), this.stats)
58
+ console.error(chalk.blue('Running: '), this.jobs.map(({ qname, message }) => ({ qname, payload: message.Body })))
59
+ }
60
+
61
+ // Build list of jobs we need to deal with
62
+ for (const job of this.jobs) {
63
+ const jobRunTime = (now - job.start) / 1000
64
+ if (job.status === 'complete') {
65
+ const jobsToDelete = jobsToDeleteByQrl.get(job.qrl) || []
66
+ jobsToDelete.push(job)
67
+ jobsToDeleteByQrl.set(job.qrl, jobsToDelete)
68
+ } else if (job.status === 'failed') {
69
+ jobsToCleanup.add(job)
70
+ } else if (jobRunTime >= job.exendAtSecond) {
71
+ // Add it to our organized list of jobs
72
+ const jobsToExtend = jobsToExtendByQrl.get(job.qrl) || []
73
+ jobsToExtend.push(job)
74
+ jobsToExtendByQrl.set(job.qrl, jobsToExtend)
75
+
76
+ // Update the visibility timeout, double every time, up to max
77
+ const doubled = job.visibilityTimeout * 2
78
+ const secondsUntilMax = maxJobSeconds - jobRunTime
79
+ const secondsUntilKill = this.opt.killAfter - jobRunTime
80
+ job.visibilityTimeout = Math.min(double, secondsUntilMax, secondsUntilKill)
81
+ job.extendAtSecond = jobRunTime + job.visibilityTimeout // this is what we use next time
82
+ }
83
+ }
84
+ debug('maintainVisibility', { jobsToDeleteByQrl, jobsToExtendByQrl })
85
+
86
+ // Extend in batches for each queue
87
+ for (const [qrl, jobsToExtend] of jobsToExtendByQrl) {
88
+ while (jobsToExtend.length) {
89
+ // Build list of messages to go in this batch
90
+ const entries = []
91
+ let messageId = 0
92
+ while (messageId++ < 10 && jobsToExtend.length) {
93
+ const job = jobsToExtend.shift()
94
+ const entry = {
95
+ Id: '' + messageId,
96
+ ReceiptHandle: job.message.ReceiptHandle,
97
+ VisibilityTimeout: job.visibilityTimeout
98
+ }
99
+ entries.push(entry)
100
+ }
101
+
102
+ // Change batch
103
+ const input = { QueueUrl: qrl, Entries: entries }
104
+ debug({ ChangeMessageVisibilityBatch: input })
105
+ const result = await getSQSClient().send(new ChangeMessageVisibilityBatchCommand(input))
106
+ debug('ChangeMessageVisibilityBatch returned', result)
107
+ this.stats.sqsCalls++
108
+ this.stats.timeoutsExtended += 10
109
+ // TODO Sentry
110
+ }
111
+ if (this.opt.verbose) {
112
+ console.error(chalk.blue('Extended these jobs: '), jobsToExtend)
113
+ } else if (!this.opt.disableLog) {
114
+ console.log(JSON.stringify({
115
+ event: 'EXTEND_VISIBILITY_TIMEOUTS',
116
+ timestamp: now,
117
+ messageIds: jobsToExtend.map(({ message }) => message.MessageId)
118
+ }))
119
+ }
120
+ }
121
+
122
+ // Delete in batches for each queue
123
+ for (const [qrl, jobsToDelete] of jobsToDeleteByQrl) {
124
+ while (jobsToDelete.length) {
125
+ // Build list of messages to go in this batch
126
+ const entries = []
127
+ let messageId = 0
128
+ while (messageId++ < 10 && jobsToDelete.length) {
129
+ const job = jobsToDelete.shift()
130
+ const entry = {
131
+ Id: '' + messageId,
132
+ ReceiptHandle: job.message.ReceiptHandle,
133
+ VisibilityTimeout: job.visibilityTimeout
134
+ }
135
+ entries.push(entry)
136
+ }
137
+
138
+ // Delete batch
139
+ const input = { QueueUrl: qrl, Entries: entries }
140
+ debug({ DeleteMessageBatch: input })
141
+ const result = await getSQSClient().send(new DeleteMessageBatchCommand(input))
142
+ this.stats.sqsCalls++
143
+ this.stats.jobsDeleted += 10
144
+ debug('DeleteMessageBatch returned', result)
145
+ // TODO Sentry
146
+ }
147
+ if (this.opt.verbose) {
148
+ console.error(chalk.blue('Deleted these finished jobs: '), jobsToDelete)
149
+ } else if (!this.opt.disableLog) {
150
+ console.log(JSON.stringify({
151
+ event: 'DELETE_MESSAGES',
152
+ timestamp: now,
153
+ messageIds: jobsToDelete.map(({ message }) => message.MessageId)
154
+ }))
155
+ }
156
+ }
157
+
158
+ // Get rid of deleted and failed jobs
159
+ this.jobs = this.jobs.filter(j => j.status === 'processing')
160
+
161
+ // Check again later, unless we are shutting down and nothing's left
162
+ if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) return
163
+ this.maintainVisibilityTimeout = setTimeout(() => this.maintainVisibility(), 10 * 1000)
164
+ }
165
+
166
+ async executeJob (message, callback, qname, qrl) {
167
+ // Create job entry and track it
168
+ const payload = this.opt.json ? JSON.parse(message.Body) : message.Body
169
+ const visibilityTimeout = 30
170
+ const job = {
171
+ status: 'processing',
172
+ start: new Date(),
173
+ visibilityTimeout: 30,
174
+ extendAtSecond: visibilityTimeout / 2,
175
+ payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
176
+ message,
177
+ callback,
178
+ qname,
179
+ qrl
180
+ }
181
+ debug('executeJob', job)
182
+ this.jobs.push(job)
183
+ this.stats.activeJobs++
184
+ if (this.opt.verbose) {
185
+ console.error(chalk.blue('Executing:'), qname, chalk.blue('-->'), job.payload)
186
+ } else if (!this.opt.disableLog) {
187
+ console.log(JSON.stringify({
188
+ event: 'MESSAGE_PROCESSING_START',
189
+ timestamp: new Date(),
190
+ qname,
191
+ messageId: message.MessageId,
192
+ payload: job.payload
193
+ }))
194
+ }
195
+
196
+ // Execute job
197
+ try {
198
+ const queue = qname.slice(this.opt.prefix.length)
199
+ const result = await callback(queue, payload)
200
+ debug('executeJob callback finished', { payload, result })
201
+ if (this.opt.verbose) {
202
+ console.error(chalk.green('SUCCESS'), message.Body)
203
+ }
204
+ job.status = 'complete'
205
+
206
+ if (this.opt.verbose) {
207
+ console.error(chalk.blue(' done'))
208
+ console.error()
209
+ } else if (!this.opt.disableLog) {
210
+ console.log(JSON.stringify({
211
+ event: 'MESSAGE_PROCESSING_COMPLETE',
212
+ timestamp: new Date(),
213
+ messageId: message.MessageId,
214
+ payload
215
+ }))
216
+ }
217
+ this.stats.jobsSucceeded++
218
+ } catch (err) {
219
+ debug('exec.catch')
220
+ // Fail path for job execution
221
+ if (this.opt.verbose) {
222
+ console.error(chalk.red('FAILED'), message.Body)
223
+ console.error(chalk.blue(' error : ') + err)
224
+ } else if (!this.opt.disableLog) {
225
+ // Production error logging
226
+ console.log(JSON.stringify({
227
+ event: 'MESSAGE_PROCESSING_FAILED',
228
+ reason: 'exception thrown',
229
+ qname,
230
+ timestamp: new Date(),
231
+ messageId: message.MessageId,
232
+ payload,
233
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
234
+ err
235
+ }))
236
+ }
237
+ job.status = 'failed'
238
+ this.stats.jobsFailed++
239
+ }
240
+ this.stats.activeJobs--
241
+ }
242
+ }