qdone 2.0.8-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,243 +2,45 @@
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
- const debug = Debug('qdone:worker')
15
+ const debug = Debug('qdone:consumer')
31
16
 
32
17
  // Global flag for shutdown request
33
18
  let shutdownRequested = false
34
19
  const shutdownCallbacks = []
35
20
 
36
21
  export function requestShutdown () {
22
+ debug('requestShutdown')
37
23
  shutdownRequested = true
38
24
  for (const callback of shutdownCallbacks) {
39
- try { callback() } catch (e) { }
40
- }
41
- }
42
-
43
- export async function processMessage (message, callback, qname, qrl, opt) {
44
- debug('processMessage', message, qname, qrl)
45
- const payload = JSON.parse(message.Body)
46
- if (opt.verbose) {
47
- console.error(chalk.blue(' Processing payload:'), payload)
48
- } else if (!opt.disableLog) {
49
- console.log(JSON.stringify({
50
- event: 'MESSAGE_PROCESSING_START',
51
- timestamp: new Date(),
52
- messageId: message.MessageId,
53
- payload
54
- }))
55
- }
56
-
57
- const jobStart = new Date()
58
- let visibilityTimeout = 30 // this should be the queue timeout
59
- let timeoutExtender
60
-
61
- async function extendTimeout () {
62
- debug('extendTimeout')
63
- const maxJobRun = 12 * 60 * 60
64
- const jobRunTime = ((new Date()) - jobStart) / 1000
65
- // Double every time, up to max
66
- visibilityTimeout = Math.min(visibilityTimeout * 2, maxJobRun - jobRunTime, opt.killAfter - jobRunTime)
67
- if (opt.verbose) {
68
- console.error(
69
- chalk.blue(' Ran for ') + jobRunTime +
70
- chalk.blue(' seconds, requesting another ') + visibilityTimeout +
71
- chalk.blue(' seconds')
72
- )
73
- }
74
-
75
- try {
76
- const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
77
- QueueUrl: qrl,
78
- ReceiptHandle: message.ReceiptHandle,
79
- VisibilityTimeout: visibilityTimeout
80
- }))
81
- debug('ChangeMessageVisibility returned', result)
82
- if (
83
- jobRunTime + visibilityTimeout >= maxJobRun ||
84
- jobRunTime + visibilityTimeout >= opt.killAfter
85
- ) {
86
- if (opt.verbose) console.error(chalk.yellow(' warning: this is our last time extension'))
87
- } else {
88
- // Extend when we get 50% of the way to timeout
89
- timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
90
- }
91
- } catch (err) {
92
- debug('ChangeMessageVisibility threw', err)
93
- // Rejection means we're ouuta time, whatever, let the job die
94
- if (opt.verbose) {
95
- console.error(chalk.red(' failed to extend job: ') + err)
96
- } else if (!opt.disableLog) {
97
- // Production error logging
98
- console.log(JSON.stringify({
99
- event: 'MESSAGE_PROCESSING_FAILED',
100
- reason: 'ran longer than --kill-after',
101
- timestamp: new Date(),
102
- messageId: message.MessageId,
103
- payload,
104
- errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
105
- err
106
- }))
107
- }
108
- }
109
- }
110
-
111
- // Extend when we get 50% of the way to timeout
112
- timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
113
- debug('timeout', visibilityTimeout * 1000 * 0.5)
114
-
115
- try {
116
- // Process message
117
- const queue = qname.slice(opt.prefix.length)
118
- const result = await callback(queue, payload)
119
- debug('processMessage callback finished', { payload, result })
120
- clearTimeout(timeoutExtender)
121
- if (opt.verbose) {
122
- console.error(chalk.green(' SUCCESS'))
123
- console.error(chalk.blue(' cleaning up (removing message) ...'))
124
- }
125
- await getSQSClient().send(new DeleteMessageCommand({
126
- QueueUrl: qrl,
127
- ReceiptHandle: message.ReceiptHandle
128
- }))
129
- if (opt.verbose) {
130
- console.error(chalk.blue(' done'))
131
- console.error()
132
- } else if (!opt.disableLog) {
133
- console.log(JSON.stringify({
134
- event: 'MESSAGE_PROCESSING_COMPLETE',
135
- timestamp: new Date(),
136
- messageId: message.MessageId,
137
- payload
138
- }))
139
- }
140
- return { noJobs: 0, jobsSucceeded: 1, jobsFailed: 0 }
141
- } catch (err) {
142
- debug('exec.catch')
143
- clearTimeout(timeoutExtender)
144
-
145
- // If the callback does not want to process this message, return to queue
146
- if (err instanceof DoNotProcess) {
147
- if (opt.verbose) {
148
- console.error(chalk.blue(' callback ') + chalk.yellow('REFUSED'))
149
- console.error(chalk.blue(' cleaning up (removing message) ...'))
150
- }
151
- const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
152
- QueueUrl: qrl,
153
- ReceiptHandle: message.ReceiptHandle,
154
- VisibilityTimeout: 0
155
- }))
156
- debug('ChangeMessageVisibility returned', result)
157
- return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
158
- }
159
-
160
- // Fail path for job execution
161
- if (opt.verbose) {
162
- console.error(chalk.red(' FAILED'))
163
- console.error(chalk.blue(' error : ') + err)
164
- } else if (!opt.disableLog) {
165
- // Production error logging
166
- console.log(JSON.stringify({
167
- event: 'MESSAGE_PROCESSING_FAILED',
168
- reason: 'exception thrown',
169
- timestamp: new Date(),
170
- messageId: message.MessageId,
171
- payload,
172
- errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
173
- err
174
- }))
175
- }
176
- return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 1 }
25
+ debug('callback', callback)
26
+ callback()
27
+ // try { callback() } catch (e) { }
177
28
  }
29
+ debug('requestShutdown done')
178
30
  }
179
31
 
180
- //
181
- // Pull work off of a single queue
182
- //
183
- export async function pollSingleQueue (qname, qrl, callback, opt) {
184
- debug('pollSingleQueue', { qname, qrl, callback, opt })
32
+ export async function getMessages (qrl, opt, maxMessages) {
185
33
  const params = {
186
34
  AttributeNames: ['All'],
187
- MaxNumberOfMessages: 1,
35
+ MaxNumberOfMessages: maxMessages,
188
36
  MessageAttributeNames: ['All'],
189
37
  QueueUrl: qrl,
190
38
  VisibilityTimeout: 30,
191
39
  WaitTimeSeconds: opt.waitTime
192
40
  }
193
41
  const response = await getSQSClient().send(new ReceiveMessageCommand(params))
194
- debug('ReceiveMessage response', response)
195
- if (shutdownRequested) return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
196
- if (response.Messages) {
197
- const message = response.Messages[0]
198
- if (opt.verbose) console.error(chalk.blue(' Found message ' + message.MessageId))
199
- return processMessage(message, callback, qname, qrl, opt)
200
- } else {
201
- return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
202
- }
203
- }
204
-
205
- //
206
- // Resolve a set of queues
207
- //
208
- export async function resolveQueues (queues, opt) {
209
- // Start processing
210
- if (opt.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
211
- const qnames = queues.map(queue => normalizeQueueName(queue, opt))
212
- const pairs = await getQnameUrlPairs(qnames, opt)
213
-
214
- // Figure out which pairs are active
215
- const activePairs = []
216
- if (opt.activeOnly) {
217
- debug({ pairsBeforeCheck: pairs })
218
- await Promise.all(pairs.map(async pair => {
219
- const { idle } = await cheapIdleCheck(pair.qname, pair.qrl, opt)
220
- if (!idle) activePairs.push(pair)
221
- }))
222
- }
223
-
224
- // Finished resolving
225
- debug('getQnameUrlPairs.then')
226
- if (opt.verbose) {
227
- console.error(chalk.blue(' done'))
228
- console.error()
229
- }
230
-
231
- // Figure out which queues we want to listen on, choosing between active and
232
- // all, filtering out failed queues if the user wants that
233
- const selectedPairs = (opt.activeOnly ? activePairs : pairs)
234
- .filter(({ qname }) => {
235
- const suf = opt.failSuffix + (opt.fifo ? '.fifo' : '')
236
- const isFailQueue = qname.slice(-suf.length) === suf
237
- const shouldInclude = opt.includeFailed ? true : !isFailQueue
238
- return shouldInclude
239
- })
240
-
241
- return selectedPairs
42
+ // debug('ReceiveMessage response', response)
43
+ return response.Messages || []
242
44
  }
243
45
 
244
46
  //
@@ -248,8 +50,17 @@ export async function processMessages (queues, callback, options) {
248
50
  const opt = getOptionsWithDefaults(options)
249
51
  debug('processMessages', { queues, callback, options, opt })
250
52
 
251
- const stats = { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
252
- const activeLoops = {}
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
+ })
253
64
 
254
65
  // This delay function keeps a timeout reference around so it can be
255
66
  // cancelled at shutdown
@@ -258,89 +69,41 @@ export async function processMessages (queues, callback, options) {
258
69
  delayTimeout = setTimeout(resolve, ms)
259
70
  })
260
71
 
261
- // Callback to help facilitate better UX at shutdown
262
- function shutdownCallback () {
263
- if (opt.verbose) {
264
- debug({ activeLoops })
265
- const activeQueues = Object.keys(activeLoops).filter(q => activeLoops[q]).map(q => q.slice(opt.prefix.length))
266
- if (activeQueues.length) {
267
- console.error(chalk.blue('Waiting for work to finish on the following queues: ') + activeQueues.join(chalk.blue(', ')))
268
- }
269
- clearTimeout(delayTimeout)
270
- }
271
- }
272
- shutdownCallbacks.push(shutdownCallback)
273
-
274
- // Listen to a queue until it is out of messages
275
- async function listenLoop (qname, qrl) {
276
- try {
277
- if (shutdownRequested) return
278
- if (opt.verbose) {
279
- console.error(
280
- chalk.blue('Looking for work on ') +
281
- qname.slice(opt.prefix.length) +
282
- chalk.blue(' (' + qrl + ')')
283
- )
284
- }
285
- // Aggregate the results
286
- const { noJobs, jobsSucceeded, jobsFailed } = await pollSingleQueue(qname, qrl, callback, opt)
287
- stats.noJobs += noJobs
288
- stats.jobsFailed += jobsFailed
289
- stats.jobsSucceeded += jobsSucceeded
290
-
291
- // No work? return to outer loop
292
- if (noJobs) return
293
-
294
- // Otherwise keep going
295
- return listenLoop(qname, qrl)
296
- } catch (err) {
297
- // TODO: Sentry
298
- console.error(chalk.red(' ERROR in listenLoop'))
299
- console.error(chalk.blue(' error : ') + err)
300
- } finally {
301
- delete activeLoops[qname]
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)
302
81
  }
82
+ maxReturnCount -= maxMessages
83
+ activeQrls.delete(qrl)
303
84
  }
304
85
 
305
- // Resolve loop
306
86
  while (!shutdownRequested) { // eslint-disable-line
307
- const start = new Date()
308
- const selectedPairs = await resolveQueues(queues, opt)
309
- if (shutdownRequested) break
310
-
311
- // But only if we have queues to listen on
312
- if (selectedPairs.length) {
313
- // Randomize order
314
- selectedPairs.sort(() => 0.5 - Math.random())
315
-
316
- if (opt.verbose) {
317
- console.error(chalk.blue('Listening to queues (in this order):'))
318
- console.error(selectedPairs.map(({ qname, qrl }) =>
319
- ' ' + qname.slice(opt.prefix.length) + chalk.blue(' - ' + qrl)
320
- ).join('\n'))
321
- console.error()
322
- }
323
-
324
- // Launch listen loop for each queue
325
- for (const { qname, qrl } of selectedPairs) {
326
- if (!activeLoops[qname]) activeLoops[qname] = listenLoop(qname, qrl)
327
- }
328
- }
329
- // Wait until the next time we need to resolve
330
- if (!shutdownRequested) {
331
- const msSoFar = Math.max(0, new Date() - start)
332
- const msUntilNextResolve = Math.max(0, opt.waitTime * 1000 - msSoFar)
333
- debug({ msSoFar, msUntilNextResolve })
334
- if (msUntilNextResolve) {
335
- if (opt.verbose) console.error(chalk.blue('Will resolve queues again in ' + Math.round(msUntilNextResolve / 1000) + ' seconds'))
336
- await delay(msUntilNextResolve)
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)
337
103
  }
104
+ debug({ listenedTo: { qname, maxMessages, jobsLeft } })
338
105
  }
106
+ await delay(1000)
339
107
  }
340
-
341
- // Wait on all work to finish
342
- // shutdownCallback()
343
- await Promise.all(Object.values(activeLoops))
108
+ debug('after all')
344
109
  }
345
-
346
- 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
+ }