qdone 2.0.35-alpha → 2.0.37-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/enqueue.js CHANGED
@@ -1,4 +1,4 @@
1
- import { addBreadcrumb } from '@sentry/node'
1
+ import { addBreadcrumb, setExtra } from '@sentry/node'
2
2
  import { v1 as uuidV1 } from 'uuid'
3
3
  import chalk from 'chalk'
4
4
  import Debug from 'debug'
@@ -20,14 +20,30 @@ import {
20
20
  normalizeDLQName
21
21
  } from './qrlCache.js'
22
22
  import { getSQSClient } from './sqs.js'
23
- import { getOptionsWithDefaults } from './defaults.js'
23
+ import {
24
+ addDedupParamsToMessage,
25
+ dedupShouldEnqueue,
26
+ dedupSuccessfullyProcessed
27
+ } from './dedup.js'
28
+ import { getOptionsWithDefaults, validateMessageOptions } from './defaults.js'
24
29
  import { ExponentialBackoff } from './exponentialBackoff.js'
25
30
 
26
31
  const debug = Debug('qdone:enqueue')
27
32
 
33
+ export function getDLQParams (queue, opt) {
34
+ const dqname = normalizeDLQName(queue, opt)
35
+ const params = {
36
+ Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
37
+ QueueName: dqname
38
+ }
39
+ if (opt.tags) params.tags = opt.tags
40
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
41
+ return { dqname, params }
42
+ }
43
+
28
44
  export async function getOrCreateDLQ (queue, opt) {
29
45
  debug('getOrCreateDLQ(', queue, ')')
30
- const dqname = normalizeDLQName(queue, opt)
46
+ const { dqname, params } = getDLQParams(queue, opt)
31
47
  try {
32
48
  const dqrl = await qrlCacheGet(dqname)
33
49
  return dqrl
@@ -37,12 +53,6 @@ export async function getOrCreateDLQ (queue, opt) {
37
53
 
38
54
  // Create our DLQ
39
55
  const client = getSQSClient()
40
- const params = {
41
- Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
42
- QueueName: dqname
43
- }
44
- if (opt.tags) params.tags = opt.tags
45
- if (opt.fifo) params.Attributes.FifoQueue = 'true'
46
56
  const cmd = new CreateQueueCommand(params)
47
57
  if (opt.verbose) console.error(chalk.blue('Creating dead letter queue ') + dqname)
48
58
  const data = await client.send(cmd)
@@ -53,35 +63,62 @@ export async function getOrCreateDLQ (queue, opt) {
53
63
  }
54
64
  }
55
65
 
56
- export async function getOrCreateFailQueue (queue, opt) {
66
+ /**
67
+ * Returns the parameters needed for creating a failed queue. If DLQ options
68
+ * are set, it makes an API call to get this DLQ's ARN.
69
+ */
70
+ export async function getFailParams (queue, opt) {
71
+ const fqname = normalizeFailQueueName(queue, opt)
72
+ const params = {
73
+ Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
74
+ QueueName: fqname
75
+ }
76
+ // If we have a dlq, we grab it and set a redrive policy
77
+ if (opt.dlq) {
78
+ const dqname = normalizeDLQName(queue, opt)
79
+ const dqrl = await qrlCacheGet(dqname)
80
+ const dqa = await getQueueAttributes(dqrl)
81
+ debug('dqa', dqa)
82
+ params.Attributes.RedrivePolicy = JSON.stringify({
83
+ deadLetterTargetArn: dqa.Attributes.QueueArn,
84
+ maxReceiveCount: opt.dlqAfter
85
+ })
86
+ }
87
+ if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay + ''
88
+ if (opt.tags) params.tags = opt.tags
89
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
90
+ return params
91
+ }
92
+
93
+ /**
94
+ * Returns the qrl for the failed queue for the given queue. Creates the queue
95
+ * if it does not exist.
96
+ */
97
+ export async function getOrCreateFailQueue (queue, opt, doesNotExist) {
57
98
  debug('getOrCreateFailQueue(', queue, ')')
58
99
  const fqname = normalizeFailQueueName(queue, opt)
59
100
  try {
101
+ // Bail early if the caller knew we didn't have a queue
102
+ if (doesNotExist) throw new QueueDoesNotExist(fqname)
60
103
  const fqrl = await qrlCacheGet(fqname)
61
104
  return fqrl
62
105
  } catch (err) {
63
106
  // Anything other than queue doesn't exist gets re-thrown
64
107
  if (!(err instanceof QueueDoesNotExist)) throw err
65
108
 
66
- // Crate our fail queue
67
- const client = getSQSClient()
68
- const params = {
69
- Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
70
- QueueName: fqname
71
- }
72
- // If we have a dlq, we grab it and set a redrive policy
73
- if (opt.dlq) {
74
- const dqrl = await getOrCreateDLQ(queue, opt)
75
- const dqa = await getQueueAttributes(dqrl)
76
- debug('dqa', dqa)
77
- params.Attributes.RedrivePolicy = JSON.stringify({
78
- deadLetterTargetArn: dqa.Attributes.QueueArn,
79
- maxReceiveCount: opt.dlqAfter + ''
80
- })
109
+ // Grab params, creating DLQ if needed
110
+ let params
111
+ try {
112
+ params = await getFailParams(queue, opt)
113
+ } catch (e) {
114
+ // If DLQ doesn't exist, create it
115
+ if (!(opt.dlq && e instanceof QueueDoesNotExist)) throw e
116
+ await getOrCreateDLQ(queue, opt)
117
+ params = await getFailParams(queue, opt)
81
118
  }
82
- if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay + ''
83
- if (opt.tags) params.tags = opt.tags
84
- if (opt.fifo) params.Attributes.FifoQueue = 'true'
119
+
120
+ // Create our fail queue
121
+ const client = getSQSClient()
85
122
  const cmd = new CreateQueueCommand(params)
86
123
  if (opt.verbose) console.error(chalk.blue('Creating fail queue ') + fqname)
87
124
  const data = await client.send(cmd)
@@ -92,6 +129,30 @@ export async function getOrCreateFailQueue (queue, opt) {
92
129
  }
93
130
  }
94
131
 
132
+ /**
133
+ * Returns the parameters needed for creating a queue. If fail options
134
+ * are set, it makes an API call to get the fail queue's ARN.
135
+ */
136
+ export async function getQueueParams (queue, opt) {
137
+ const qname = normalizeQueueName(queue, opt)
138
+ const fqname = normalizeFailQueueName(queue, opt)
139
+ const fqrl = await qrlCacheGet(fqname, opt)
140
+ const fqa = await getQueueAttributes(fqrl)
141
+ const params = {
142
+ Attributes: {
143
+ MessageRetentionPeriod: opt.messageRetentionPeriod + '',
144
+ RedrivePolicy: JSON.stringify({
145
+ deadLetterTargetArn: fqa.Attributes.QueueArn,
146
+ maxReceiveCount: 1
147
+ })
148
+ },
149
+ QueueName: qname
150
+ }
151
+ if (opt.tags) params.tags = opt.tags
152
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
153
+ return params
154
+ }
155
+
95
156
  /**
96
157
  * Returns a qrl for a queue that either exists or does not
97
158
  */
@@ -105,29 +166,25 @@ export async function getOrCreateQueue (queue, opt) {
105
166
  // Anything other than queue doesn't exist gets re-thrown
106
167
  if (!(err instanceof QueueDoesNotExist)) throw err
107
168
 
108
- // Get our fail queue so we can create our own
109
- const fqrl = await getOrCreateFailQueue(qname, opt)
110
- const fqa = await getQueueAttributes(fqrl)
169
+ // Grab params, creating fail queue if needed
170
+ let params
171
+ try {
172
+ params = await getQueueParams(qname, opt)
173
+ } catch (e) {
174
+ // If fail queue doesn't exist, create it
175
+ if (!(e instanceof QueueDoesNotExist)) throw e
176
+ await getOrCreateFailQueue(qname, opt, true)
177
+ params = await getQueueParams(qname, opt)
178
+ }
179
+
180
+ debug({ getOrCreateQueue: { qname, params } })
111
181
 
112
182
  // Create our queue
113
183
  const client = getSQSClient()
114
- const params = {
115
- Attributes: {
116
- MessageRetentionPeriod: opt.messageRetentionPeriod + '',
117
- RedrivePolicy: JSON.stringify({
118
- deadLetterTargetArn: fqa.Attributes.QueueArn,
119
- maxReceiveCount: '1'
120
- })
121
- },
122
- QueueName: qname
123
- }
124
- if (opt.tags) params.tags = opt.tags
125
- if (opt.fifo) params.Attributes.FifoQueue = 'true'
126
184
  const cmd = new CreateQueueCommand(params)
127
- debug({ params })
128
- if (opt.verbose) console.error(chalk.blue('Creating queue ') + qname)
185
+ if (opt.verbose) console.error(chalk.blue('Creating fail queue ') + qname)
129
186
  const data = await client.send(cmd)
130
- debug('createQueue returned', data)
187
+ debug('AWS createQueue returned', data)
131
188
  const qrl = data.QueueUrl
132
189
  qrlCacheSet(qname, qrl)
133
190
  return qrl
@@ -145,17 +202,15 @@ export async function getQueueAttributes (qrl) {
145
202
  return data
146
203
  }
147
204
 
148
- export function formatMessage (command, id) {
149
- const message = {
150
- /*
151
- MessageAttributes: {
152
- City: { DataType: 'String', StringValue: 'Any City' },
153
- Population: { DataType: 'Number', StringValue: '1250800' }
154
- },
155
- */
156
- MessageBody: command
157
- }
205
+ export function formatMessage (body, id, opt, messageOptions) {
206
+ const message = { MessageBody: body }
158
207
  if (typeof id !== 'undefined') message.Id = '' + id
208
+ if (opt.fifo) {
209
+ message.MessageGroupId = messageOptions?.groupId || opt?.groupId
210
+ }
211
+ addDedupParamsToMessage(message, opt, messageOptions)
212
+ if (opt.delay) message.DelaySeconds = opt.delay
213
+ if (messageOptions?.delay) message.DelaySeconds = messageOptions.delay
159
214
  return message
160
215
  }
161
216
 
@@ -166,14 +221,19 @@ const retryableExceptions = [
166
221
  QueueDoesNotExist // Queue could temporarily not exist due to eventual consistency, let it retry
167
222
  ]
168
223
 
169
- export async function sendMessage (qrl, command, opt) {
224
+ export async function sendMessage (qrl, command, opt, messageOptions) {
170
225
  debug('sendMessage(', qrl, command, ')')
171
- const params = Object.assign({ QueueUrl: qrl }, formatMessage(command))
172
- if (opt.fifo) {
173
- params.MessageGroupId = opt.groupId
174
- params.MessageDeduplicationId = opt.deduplicationId || uuidV1()
226
+ const uuidFunction = opt.uuidFunction || uuidV1
227
+ const params = {
228
+ QueueUrl: qrl,
229
+ ...formatMessage(command, null, opt, messageOptions)
230
+ }
231
+
232
+ // See if we even have to send it
233
+ if (opt.externalDedup) {
234
+ const shouldEnqueue = await dedupShouldEnqueue(params, opt)
235
+ if (!shouldEnqueue) return { MessageId: uuidFunction() }
175
236
  }
176
- if (opt.delay) params.DelaySeconds = opt.delay
177
237
 
178
238
  // Send it
179
239
  const client = getSQSClient()
@@ -187,12 +247,15 @@ export async function sendMessage (qrl, command, opt) {
187
247
  return data
188
248
  }
189
249
  const shouldRetry = async (result, error) => {
250
+ if (!error) return false
190
251
  for (const exceptionClass of retryableExceptions) {
191
252
  if (error instanceof exceptionClass) {
192
253
  debug({ sendMessageRetryingBecause: { error, result } })
193
254
  return true
194
255
  }
195
256
  }
257
+ // If we could not send it, we also need to remove our dedup flag
258
+ await dedupSuccessfullyProcessed(params, opt)
196
259
  return false
197
260
  }
198
261
  const result = await backoff.run(send, shouldRetry)
@@ -203,25 +266,19 @@ export async function sendMessage (qrl, command, opt) {
203
266
  export async function sendMessageBatch (qrl, messages, opt) {
204
267
  debug('sendMessageBatch(', qrl, messages.map(e => Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' })), ')')
205
268
  const params = { Entries: messages, QueueUrl: qrl }
206
- const uuidFunction = opt.uuidFunction || uuidV1
207
- // Add in group id if we're using fifo
208
- if (opt.fifo) {
209
- params.Entries = params.Entries.map(
210
- message => Object.assign({
211
- MessageGroupId: opt.groupIdPerMessage ? uuidFunction() : opt.groupId,
212
- MessageDeduplicationId: opt.deduplicationId || uuidFunction()
213
- }, message)
214
- )
215
- }
216
- if (opt.delay) {
217
- params.Entries = params.Entries.map(message =>
218
- Object.assign({ DelaySeconds: opt.delay }, message))
219
- }
220
269
  if (opt.sentryDsn) {
221
270
  addBreadcrumb({ category: 'sendMessageBatch', message: JSON.stringify({ params }), level: 'debug' })
222
271
  }
223
272
  debug({ params })
224
273
 
274
+ // See which messages we even have to send
275
+ if (opt.externalDedup) {
276
+ const promises = params.Entries.map(async m => ({ m, shouldEnqueue: await dedupShouldEnqueue(m, opt) }))
277
+ const results = await Promise.all(promises)
278
+ params.Entries = results.filter(({ shouldEnqueue }) => shouldEnqueue)
279
+ if (!params.Entries.length) return
280
+ }
281
+
225
282
  // Send them
226
283
  const client = getSQSClient()
227
284
  const cmd = new SendMessageBatchCommand(params)
@@ -320,8 +377,8 @@ export async function flushMessages (qrl, opt, sendBuffer) {
320
377
  // Automaticaly flushes if queue has >= 10 messages.
321
378
  // Returns number of messages flushed.
322
379
  //
323
- export async function addMessage (qrl, command, messageIndex, opt, sendBuffer) {
324
- const message = formatMessage(command, messageIndex)
380
+ export async function addMessage (qrl, command, messageIndex, opt, sendBuffer, messageOptions) {
381
+ const message = formatMessage(command, messageIndex, opt, messageOptions)
325
382
  sendBuffer[qrl] = sendBuffer[qrl] || []
326
383
  sendBuffer[qrl].push(message)
327
384
  debug({ location: 'addMessage', sendBuffer })
@@ -338,8 +395,16 @@ export async function addMessage (qrl, command, messageIndex, opt, sendBuffer) {
338
395
  export async function enqueue (queue, command, options) {
339
396
  debug('enqueue(', { queue, command }, ')')
340
397
  const opt = getOptionsWithDefaults(options)
341
- const qrl = await getOrCreateQueue(queue, opt)
342
- return sendMessage(qrl, command, opt)
398
+ if (opt.sentryDsn) {
399
+ setExtra({ qdoneOperation: 'enqueue', args: { queue, command, opt } })
400
+ }
401
+ try {
402
+ const qrl = await getOrCreateQueue(queue, opt)
403
+ return sendMessage(qrl, command, opt)
404
+ } catch (e) {
405
+ console.log(e)
406
+ throw e
407
+ }
343
408
  }
344
409
 
345
410
  //
@@ -349,43 +414,50 @@ export async function enqueue (queue, command, options) {
349
414
  export async function enqueueBatch (pairs, options) {
350
415
  debug('enqueueBatch(', pairs, ')')
351
416
  const opt = getOptionsWithDefaults(options)
352
-
353
- // Find unique queues so we can pre-fetch qrls. We do this so that all
354
- // queues are created prior to going through our flush logic
355
- const normalizedPairs = pairs.map(({ queue, command }) => ({
356
- qname: normalizeQueueName(queue, opt),
357
- command
358
- }))
359
- const uniqueQnames = new Set(normalizedPairs.map(p => p.qname))
360
-
361
- // Prefetch qrls / create queues in parallel
362
- const createPromises = []
363
- for (const qname of uniqueQnames) {
364
- createPromises.push(getOrCreateQueue(qname, opt))
365
- }
366
- await Promise.all(createPromises)
367
-
368
- // After we've prefetched, all qrls are in cache
369
- // so go back through the list of pairs and fire off messages
370
- requestCount = 0
371
- const sendBuffer = {}
372
- let messageIndex = 0
373
- let initialFlushTotal = 0
374
- for (const { qname, command } of normalizedPairs) {
375
- const qrl = await getOrCreateQueue(qname, opt)
376
- initialFlushTotal += await addMessage(qrl, command, messageIndex++, opt, sendBuffer)
417
+ if (opt.sentryDsn) {
418
+ setExtra({ qdoneOperation: 'enqueueBatch', args: { pairs, opt } })
377
419
  }
420
+ try {
421
+ // Find unique queues so we can pre-fetch qrls. We do this so that all
422
+ // queues are created prior to going through our flush logic
423
+ const normalizedPairs = pairs.map(({ queue, command, messageOptions }) => ({
424
+ qname: normalizeQueueName(queue, opt),
425
+ command,
426
+ messageOptions: validateMessageOptions(messageOptions)
427
+ }))
428
+ const uniqueQnames = new Set(normalizedPairs.map(p => p.qname))
378
429
 
379
- // And flush any remaining messages
380
- const extraFlushPromises = []
381
- for (const qrl in sendBuffer) {
382
- extraFlushPromises.push(flushMessages(qrl, opt, sendBuffer))
430
+ // Prefetch qrls / create queues in parallel
431
+ const createPromises = []
432
+ for (const qname of uniqueQnames) {
433
+ createPromises.push(getOrCreateQueue(qname, opt))
434
+ }
435
+ await Promise.all(createPromises)
436
+ // After we've prefetched, all qrls are in cache
437
+ // so go back through the list of pairs and fire off messages
438
+ requestCount = 0
439
+ const sendBuffer = {}
440
+ let messageIndex = 0
441
+ let initialFlushTotal = 0
442
+ for (const { qname, command, messageOptions } of normalizedPairs) {
443
+ const qrl = await getOrCreateQueue(qname, opt)
444
+ initialFlushTotal += await addMessage(qrl, command, messageIndex++, opt, sendBuffer, messageOptions)
445
+ }
446
+
447
+ // And flush any remaining messages
448
+ const extraFlushPromises = []
449
+ for (const qrl in sendBuffer) {
450
+ extraFlushPromises.push(flushMessages(qrl, opt, sendBuffer))
451
+ }
452
+ const extraFlushCounts = await Promise.all(extraFlushPromises)
453
+ const extraFlushTotal = extraFlushCounts.reduce((a, b) => a + b, 0)
454
+ const totalFlushed = initialFlushTotal + extraFlushTotal
455
+ debug({ initialFlushTotal, extraFlushTotal, totalFlushed })
456
+ return totalFlushed
457
+ } catch (e) {
458
+ console.log(e)
459
+ throw e
383
460
  }
384
- const extraFlushCounts = await Promise.all(extraFlushPromises)
385
- const extraFlushTotal = extraFlushCounts.reduce((a, b) => a + b, 0)
386
- const totalFlushed = initialFlushTotal + extraFlushTotal
387
- debug({ initialFlushTotal, extraFlushTotal, totalFlushed })
388
- return totalFlushed
389
461
  }
390
462
 
391
463
  debug('loaded')
package/src/qrlCache.js CHANGED
@@ -66,7 +66,7 @@ export async function qrlCacheGet (qname) {
66
66
  // debug({ cmd })
67
67
  const result = await client.send(cmd)
68
68
  // debug('result', result)
69
- // if (!result) throw new Error(`No such queue ${qname}`)
69
+ if (!result) throw new QueueDoesNotExist(qname)
70
70
  const { QueueUrl: qrl } = result
71
71
  // debug('getQueueUrl returned', data)
72
72
  qcache.set(qname, qrl)
@@ -8,6 +8,7 @@ import { ChangeMessageVisibilityBatchCommand, DeleteMessageBatchCommand } from '
8
8
  import chalk from 'chalk'
9
9
  import Debug from 'debug'
10
10
 
11
+ import { dedupSuccessfullyProcessed } from '../dedup.js'
11
12
  import { getSQSClient } from '../sqs.js'
12
13
 
13
14
  const debug = Debug('qdone:jobExecutor')
@@ -221,6 +222,10 @@ export class JobExecutor {
221
222
  }
222
223
  }
223
224
  debug('DeleteMessageBatch returned', result)
225
+
226
+ // Mark batch as processed for dedup
227
+ await Promise.all(entries.map(e => dedupSuccessfullyProcessed(this.jobsByMessageId[e.Id], this.opt)))
228
+
224
229
  // TODO Sentry
225
230
  }
226
231
  }
package/src/worker.js CHANGED
@@ -13,6 +13,7 @@ import treeKill from 'tree-kill'
13
13
  import chalk from 'chalk'
14
14
  import Debug from 'debug'
15
15
 
16
+ import { dedupSuccessfullyProcessed } from './dedup.js'
16
17
  import { normalizeQueueName, getQnameUrlPairs } from './qrlCache.js'
17
18
  import { getOptionsWithDefaults } from './defaults.js'
18
19
  import { cheapIdleCheck } from './idleQueues.js'
@@ -129,10 +130,15 @@ export async function executeJob (job, qname, qrl, opt) {
129
130
  QueueUrl: qrl,
130
131
  ReceiptHandle: job.ReceiptHandle
131
132
  }))
133
+
132
134
  if (opt.verbose) {
133
135
  console.error(chalk.blue(' done'))
134
136
  console.error()
135
137
  }
138
+
139
+ // Let dedup system know we processed it
140
+ await dedupSuccessfullyProcessed(job, opt)
141
+
136
142
  return { noJobs: 0, jobsSucceeded: 1, jobsFailed: 0 }
137
143
  } catch (err) {
138
144
  // Fail path for job execution