qdone 1.7.0 → 2.0.0-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,65 +1,153 @@
1
+ // const Q = require('q')
2
+ // const debug = require('debug')('qdone:enqueue')
3
+ // const chalk = require('chalk')
4
+ // const uuid = require('uuid')
5
+ // const qrlCache = require('./qrlCache')
6
+ // const AWS = require('aws-sdk')
1
7
 
2
- const Q = require('q')
3
- const debug = require('debug')('qdone:enqueue')
4
- const chalk = require('chalk')
5
- const uuid = require('uuid')
6
- const qrlCache = require('./qrlCache')
7
- const AWS = require('aws-sdk')
8
+ import { v1 as uuidV1 } from 'uuid'
9
+ import chalk from 'chalk'
10
+ import Debug from 'debug'
11
+ import {
12
+ CreateQueueCommand,
13
+ GetQueueAttributesCommand,
14
+ SendMessageCommand,
15
+ SendMessageBatchCommand,
16
+ QueueDoesNotExist
17
+ } from '@aws-sdk/client-sqs'
8
18
 
9
- function createFailQueue (fqueue, fqname, deadLetterTargetArn, options) {
10
- debug('createFailQueue(', fqueue, fqname, ')')
11
- const sqs = new AWS.SQS()
12
- const params = {
13
- /* Attributes: {MessageRetentionPeriod: '259200', RedrivePolicy: '{"deadLetterTargetArn": "arn:aws:sqs:us-east-1:80398EXAMPLE:MyDeadLetterQueue", "maxReceiveCount": "1000"}'}, */
14
- Attributes: {},
15
- QueueName: fqname
19
+ import {
20
+ qrlCacheGet,
21
+ qrlCacheSet,
22
+ normalizeQueueName,
23
+ normalizeFailQueueName,
24
+ normalizeDLQName
25
+ } from './qrlCache.js'
26
+ import { getSQSClient } from './sqs.js'
27
+ import { getOptionsWithDefaults } from './defaults.js'
28
+
29
+ const debug = Debug('qdone:enqueue')
30
+
31
+ export async function getOrCreateDLQ (queue, opt) {
32
+ debug('getOrCreateDLQ(', queue, ')')
33
+ const dqname = normalizeDLQName(queue, opt)
34
+ try {
35
+ const dqrl = await qrlCacheGet(dqname)
36
+ return dqrl
37
+ } catch (err) {
38
+ // Anything other than queue doesn't exist gets re-thrown
39
+ if (!(err instanceof QueueDoesNotExist)) throw err
40
+
41
+ // Create our DLQ
42
+ const client = getSQSClient()
43
+ const params = {
44
+ Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
45
+ QueueName: dqname
46
+ }
47
+ if (opt.tags) params.tags = opt.tags
48
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
49
+ const cmd = new CreateQueueCommand(params)
50
+ if (opt.verbose) console.error(chalk.blue('Creating dead letter queue ') + dqname)
51
+ const data = await client.send(cmd)
52
+ debug('createQueue returned', data)
53
+ const dqrl = data.QueueUrl
54
+ qrlCacheSet(dqname, dqrl)
55
+ return dqrl
16
56
  }
17
- if (options.fifo) params.Attributes.FifoQueue = 'true'
18
- return sqs
19
- .createQueue(params)
20
- .promise()
21
- .then(function (data) {
22
- debug('createQueue returned', data)
23
- return data.QueueUrl
24
- })
25
57
  }
26
58
 
27
- function createQueue (queue, qname, deadLetterTargetArn, options) {
28
- debug('createQueue(', queue, qname, ')')
29
- const sqs = new AWS.SQS()
30
- const params = {
31
- Attributes: {
32
- // MessageRetentionPeriod: '259200',
33
- RedrivePolicy: `{"deadLetterTargetArn": "${deadLetterTargetArn}", "maxReceiveCount": "1"}'}`
34
- },
35
- QueueName: qname
59
+ export async function getOrCreateFailQueue (queue, opt) {
60
+ debug('getOrCreateFailQueue(', queue, ')')
61
+ const fqname = normalizeFailQueueName(queue, opt)
62
+ try {
63
+ const fqrl = await qrlCacheGet(fqname)
64
+ return fqrl
65
+ } catch (err) {
66
+ // Anything other than queue doesn't exist gets re-thrown
67
+ if (!(err instanceof QueueDoesNotExist)) throw err
68
+
69
+ // Crate our fail queue
70
+ const client = getSQSClient()
71
+ const params = {
72
+ Attributes: { MessageRetentionPeriod: opt.messageRetentionPeriod + '' },
73
+ QueueName: fqname
74
+ }
75
+ // If we have a dlq, we grab it and set a redrive policy
76
+ if (opt.dlq) {
77
+ const dqrl = await getOrCreateDLQ(queue, opt)
78
+ const dqa = await getQueueAttributes(dqrl)
79
+ debug('dqa', dqa)
80
+ params.Attributes.RedrivePolicy = JSON.stringify({
81
+ deadLetterTargetArn: dqa.Attributes.QueueArn,
82
+ maxReceiveCount: opt.dlqAfter + ''
83
+ })
84
+ }
85
+ if (opt.tags) params.tags = opt.tags
86
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
87
+ const cmd = new CreateQueueCommand(params)
88
+ if (opt.verbose) console.error(chalk.blue('Creating fail queue ') + fqname)
89
+ const data = await client.send(cmd)
90
+ debug('createQueue returned', data)
91
+ const fqrl = data.QueueUrl
92
+ qrlCacheSet(fqname, fqrl)
93
+ return fqrl
36
94
  }
37
- if (options.fifo) params.Attributes.FifoQueue = 'true'
38
- return sqs
39
- .createQueue(params)
40
- .promise()
41
- .then(function (data) {
42
- debug('createQueue returned', data)
43
- return data.QueueUrl
44
- })
45
95
  }
46
96
 
47
- function getQueueAttributes (qrl) {
97
+ /**
98
+ * Returns a qrl for a queue that either exists or does not
99
+ */
100
+ export async function getOrCreateQueue (queue, opt) {
101
+ debug('getOrCreateQueue(', queue, ')')
102
+ const qname = normalizeQueueName(queue, opt)
103
+ try {
104
+ const qrl = await qrlCacheGet(qname)
105
+ return qrl
106
+ } catch (err) {
107
+ // Anything other than queue doesn't exist gets re-thrown
108
+ if (!(err instanceof QueueDoesNotExist)) throw err
109
+
110
+ // Get our fail queue so we can create our own
111
+ const fqrl = await getOrCreateFailQueue(qname, opt)
112
+ const fqa = await getQueueAttributes(fqrl)
113
+
114
+ // Create our queue
115
+ const client = getSQSClient()
116
+ const params = {
117
+ Attributes: {
118
+ MessageRetentionPeriod: opt.messageRetentionPeriod + '',
119
+ RedrivePolicy: JSON.stringify({
120
+ deadLetterTargetArn: fqa.Attributes.QueueArn,
121
+ maxReceiveCount: '1'
122
+ })
123
+ },
124
+ QueueName: qname
125
+ }
126
+ if (opt.tags) params.tags = opt.tags
127
+ if (opt.fifo) params.Attributes.FifoQueue = 'true'
128
+ const cmd = new CreateQueueCommand(params)
129
+ debug({ params })
130
+ if (opt.verbose) console.error(chalk.blue('Creating queue ') + qname)
131
+ const data = await client.send(cmd)
132
+ debug('createQueue returned', data)
133
+ const qrl = data.QueueUrl
134
+ qrlCacheSet(qname, qrl)
135
+ return qrl
136
+ }
137
+ }
138
+
139
+ export async function getQueueAttributes (qrl) {
48
140
  debug('getQueueAttributes(', qrl, ')')
49
- const sqs = new AWS.SQS()
50
- return sqs
51
- .getQueueAttributes({
52
- AttributeNames: ['All'],
53
- QueueUrl: qrl
54
- })
55
- .promise()
56
- .then(function (data) {
57
- debug('getQueueAttributes returned', data)
58
- return data
59
- })
141
+ const client = getSQSClient()
142
+ const params = { AttributeNames: ['All'], QueueUrl: qrl }
143
+ const cmd = new GetQueueAttributesCommand(params)
144
+ // debug({ cmd })
145
+ const data = await client.send(cmd)
146
+ debug('GetQueueAttributes returned', data)
147
+ return data
60
148
  }
61
149
 
62
- function formatMessage (command, id) {
150
+ export function formatMessage (command, id) {
63
151
  const message = {
64
152
  /*
65
153
  MessageAttributes: {
@@ -73,88 +161,93 @@ function formatMessage (command, id) {
73
161
  return message
74
162
  }
75
163
 
76
- function sendMessage (qrl, command, options) {
164
+ export async function sendMessage (qrl, command, opt) {
77
165
  debug('sendMessage(', qrl, command, ')')
78
- const message = Object.assign({ QueueUrl: qrl }, formatMessage(command))
166
+ const params = Object.assign({ QueueUrl: qrl }, formatMessage(command))
79
167
  // Add in group id if we're using fifo
80
- if (options.fifo) {
81
- message.MessageGroupId = options['group-id']
82
- message.MessageDeduplicationId = options['deduplication-id']
168
+ if (opt.fifo) {
169
+ params.MessageGroupId = opt.groupId
170
+ params.MessageDeduplicationId = opt.deduplicationId
83
171
  }
84
- const sqs = new AWS.SQS()
85
- return sqs
86
- .sendMessage(message)
87
- .promise()
88
- .then(function (data) {
89
- debug('sendMessage returned', data)
90
- return data
91
- })
172
+ if (opt.delay) params.DelaySeconds = opt.delay
173
+ const client = getSQSClient()
174
+ const cmd = new SendMessageCommand(params)
175
+ debug({ cmd })
176
+ const data = await client.send(cmd)
177
+ debug('sendMessage returned', data)
178
+ return data
92
179
  }
93
180
 
94
- function sendMessageBatch (qrl, messages, options) {
181
+ export async function sendMessageBatch (qrl, messages, opt) {
95
182
  debug('sendMessageBatch(', qrl, messages.map(e => Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' })), ')')
96
183
  const params = { Entries: messages, QueueUrl: qrl }
184
+ const uuidFunction = opt.uuidFunction || uuidV1
97
185
  // Add in group id if we're using fifo
98
- if (options.fifo) {
186
+ if (opt.fifo) {
99
187
  params.Entries = params.Entries.map(
100
188
  message => Object.assign({
101
- MessageGroupId: options['group-id-per-message'] ? uuid.v1() : options['group-id'],
102
- MessageDeduplicationId: uuid.v1()
189
+ MessageGroupId: opt.groupIdPerMessage ? uuidFunction() : opt.groupId,
190
+ MessageDeduplicationId: uuidFunction()
103
191
  }, message)
104
192
  )
105
193
  }
106
- const sqs = new AWS.SQS()
107
- return sqs
108
- .sendMessageBatch(params)
109
- .promise()
110
- .then(function (data) {
111
- debug('sendMessageBatch returned', data)
112
- return data
113
- })
194
+ if (opt.delay) {
195
+ params.Entries = params.Entries.map(message =>
196
+ Object.assign({ DelaySeconds: opt.delay }, message))
197
+ }
198
+ const client = getSQSClient()
199
+ const cmd = new SendMessageBatchCommand(params)
200
+ debug({ cmd })
201
+ const data = await client.send(cmd)
202
+ debug('sendMessageBatch returned', data)
203
+ return data
114
204
  }
115
205
 
116
206
  const messages = {}
117
- var requestCount = 0
207
+ let requestCount = 0
118
208
 
119
209
  //
120
210
  // Flushes the internal message buffer for qrl.
121
211
  // If the message is too large, batch is retried with half the messages.
122
212
  // Returns number of messages flushed.
123
213
  //
124
- function flushMessages (qrl, options) {
214
+ export async function flushMessages (qrl, opt) {
125
215
  debug('flushMessages', qrl)
126
216
  // Flush until empty
127
- var numFlushed = 0
128
- function whileNotEmpty () {
217
+ let numFlushed = 0
218
+ async function whileNotEmpty () {
129
219
  if (!(messages[qrl] && messages[qrl].length)) return numFlushed
130
220
  // Construct batch until full
131
221
  const batch = []
132
- var nextSize = JSON.stringify(messages[qrl][0]).length
133
- var totalSize = 0
222
+ let nextSize = JSON.stringify(messages[qrl][0]).length
223
+ let totalSize = 0
134
224
  while ((totalSize + nextSize) < 262144 && messages[qrl].length && batch.length < 10) {
135
225
  batch.push(messages[qrl].shift())
136
226
  totalSize += nextSize
137
227
  if (messages[qrl].length) nextSize = JSON.stringify(messages[qrl][0]).length
138
228
  else nextSize = 0
139
229
  }
140
- return sendMessageBatch(qrl, batch, options)
141
- .then(function (data) {
142
- // Fail if there are any individual message failures
143
- if (data.Failed && data.Failed.length) {
144
- const err = new Error('One or more message failures: ' + JSON.stringify(data.Failed))
145
- err.Failed = data.Failed
146
- throw err
147
- }
148
- // If we actually managed to flush any of them
149
- if (batch.length) {
150
- requestCount += 1
151
- data.Successful.forEach(message => {
152
- if (options.verbose) console.error(chalk.blue('Enqueued job ') + message.MessageId + chalk.blue(' request ' + requestCount))
153
- })
154
- numFlushed += batch.length
155
- }
230
+
231
+ // Send batch
232
+ const data = await sendMessageBatch(qrl, batch, opt)
233
+ debug({ data })
234
+
235
+ // Fail if there are any individual message failures
236
+ if (data.Failed && data.Failed.length) {
237
+ const err = new Error('One or more message failures: ' + JSON.stringify(data.Failed))
238
+ err.Failed = data.Failed
239
+ throw err
240
+ }
241
+
242
+ // If we actually managed to flush any of them
243
+ if (batch.length) {
244
+ requestCount += 1
245
+ data.Successful.forEach(message => {
246
+ if (opt.verbose) console.error(chalk.blue('Enqueued job ') + message.MessageId + chalk.blue(' request ' + requestCount))
156
247
  })
157
- .then(whileNotEmpty)
248
+ numFlushed += batch.length
249
+ }
250
+ return whileNotEmpty()
158
251
  }
159
252
  return whileNotEmpty()
160
253
  }
@@ -164,125 +257,75 @@ function flushMessages (qrl, options) {
164
257
  // Automaticaly flushes if queue has >= 10 messages.
165
258
  // Returns number of messages flushed.
166
259
  //
167
- var messageIndex = 0
168
- function addMessage (qrl, command, options) {
260
+ let messageIndex = 0
261
+ export async function addMessage (qrl, command, opt) {
169
262
  const message = formatMessage(command, messageIndex++)
170
263
  messages[qrl] = messages[qrl] || []
171
264
  messages[qrl].push(message)
172
- if (messages[qrl].length > 10) {
173
- return flushMessages(qrl, options)
265
+ if (messages[qrl].length >= 10) {
266
+ return flushMessages(qrl, opt)
174
267
  }
175
268
  return 0
176
269
  }
177
270
 
178
- //
179
- // Fetches (or returns cached) the qrl
180
- //
181
- function getQrl (queue, qname, fqueue, fqname, options) {
182
- debug('getQrl', queue, qname, fqueue, fqname)
183
- // Normal queue
184
- const qrl = qrlCache
185
- .get(qname)
186
- .catch(function (err) {
187
- // Create our queue if it doesn't exist
188
- if (err.code === 'AWS.SimpleQueueService.NonExistentQueue') {
189
- // Grab fail queue
190
- const fqrl = qrlCache
191
- .get(fqname)
192
- .catch(function (err) {
193
- // Create fail queue if it doesn't exist
194
- if (err.code === 'AWS.SimpleQueueService.NonExistentQueue') {
195
- if (options.verbose) console.error(chalk.blue('Creating fail queue ') + fqueue)
196
- return createFailQueue(fqueue, fqname, null, options)
197
- }
198
- throw err // throw unhandled errors
199
- })
200
-
201
- // Need to grab fail queue's ARN to create our queue
202
- return fqrl
203
- .then(getQueueAttributes)
204
- .then(data => {
205
- if (options.verbose) console.error(chalk.blue('Creating queue ') + queue)
206
- return createQueue(queue, qname, data.Attributes.QueueArn, options)
207
- })
208
- }
209
- throw err // throw unhandled errors
210
- })
211
-
212
- return qrl
213
- }
214
-
215
271
  //
216
272
  // Enqueue a single command
217
273
  // Returns a promise for the SQS API response.
218
274
  //
219
- exports.enqueue = function enqueue (queue, command, options) {
220
- debug('enqueue(', queue, command, ')')
221
-
222
- queue = qrlCache.normalizeQueueName(queue, options)
223
- const qname = options.prefix + queue
224
- const fqueue = qrlCache.normalizeFailQueueName(queue, options)
225
- const fqname = options.prefix + fqueue
226
-
227
- // Now that we have the queue, send our message
228
- return getQrl(queue, qname, fqueue, fqname, options)
229
- .then(qrl => sendMessage(qrl, command, options))
275
+ export async function enqueue (queue, command, options) {
276
+ debug('enqueue(', { queue, command }, ')')
277
+ const opt = getOptionsWithDefaults(options)
278
+ const qrl = await getOrCreateQueue(queue, opt)
279
+ return sendMessage(qrl, command, opt)
230
280
  }
231
281
 
232
282
  //
233
283
  // Enqueue many commands formatted as an array of {queue: ..., command: ...} pairs.
234
284
  // Returns a promise for the total number of messages enqueued.
235
285
  //
236
- exports.enqueueBatch = function enqueueBatch (pairs, options) {
286
+ export async function enqueueBatch (pairs, options) {
237
287
  debug('enqueueBatch(', pairs, ')')
288
+ const opt = getOptionsWithDefaults(options)
238
289
 
239
- function unpackPair (pair) {
240
- const queue = qrlCache.normalizeQueueName(pair.queue, options)
241
- const command = pair.command
242
- const qname = options.prefix + queue
243
- const fqueue = qrlCache.normalizeFailQueueName(queue, options)
244
- const fqname = options.prefix + fqueue
245
- return { queue, qname, fqueue, fqname, command }
246
- }
290
+ // Find unique queues so we can pre-fetch qrls. We do this so that all
291
+ // queues are created prior to going through our flush logic
292
+ const normalizedPairs = pairs.map(({ queue, command }) => ({
293
+ qname: normalizeQueueName(queue, opt),
294
+ command
295
+ }))
296
+ const uniqueQnames = new Set(normalizedPairs.map(p => p.qname))
247
297
 
248
- // Find unique pairs
249
- const uniquePairMap = {}
250
- const uniquePairs = []
251
- pairs.forEach(pair => {
252
- if (!uniquePairMap[pair.queue]) {
253
- uniquePairMap[pair.queue] = true
254
- uniquePairs.push(pair)
255
- }
256
- })
257
- debug({ uniquePairMap, uniquePairs })
298
+ // Prefetch qrls / create queues in parallel
299
+ const createPromises = []
300
+ for (const qname of uniqueQnames) {
301
+ createPromises.push(getOrCreateQueue(qname, opt))
302
+ }
303
+ await Promise.all(createPromises)
258
304
 
259
- // Prefetch unique qrls in parallel (creating as needed)
305
+ // After we've prefetched, all qrls are in cache
306
+ // so go back through the list of pairs and fire off messages
260
307
  requestCount = 0
261
- return Q.all(
262
- uniquePairs
263
- .map(unpackPair)
264
- .map(u => getQrl(u.queue, u.qname, u.fqueue, u.fqname, options))
265
- ).then(function () {
266
- // After we've prefetched, all qrls are in cache
267
- // so go back through the list of pairs and fire off messages
268
- return Q.all(
269
- // Add every individual command, flushing as we go
270
- pairs
271
- .map(unpackPair)
272
- .map(u =>
273
- getQrl(u.queue, u.qname, u.fqueue, u.fqname, options)
274
- .then(qrl => addMessage(qrl, u.command, options))
275
- )
276
- ).then(function (flushCounts) {
277
- // Count up how many were flushed during add
278
- debug('flushCounts', flushCounts)
279
- const totalFlushed = flushCounts.reduce((a, b) => a + b, 0)
280
- // And flush any remaining messages
281
- return Q
282
- .all(Object.keys(messages).map(key => flushMessages(key, options))) // messages is the global flush buffer
283
- .then(flushCounts => flushCounts.reduce((a, b) => a + b, totalFlushed))
284
- })
285
- })
308
+ const addMessagePromises = []
309
+ for (const { qname, command } of normalizedPairs) {
310
+ const qrl = await getOrCreateQueue(qname, opt)
311
+ addMessagePromises.push(addMessage(qrl, command, opt))
312
+ }
313
+ const flushCounts = await Promise.all(addMessagePromises)
314
+
315
+ // Count up how many were flushed during add
316
+ debug('flushCounts', flushCounts)
317
+ const initialFlushTotal = flushCounts.reduce((a, b) => a + b, 0)
318
+
319
+ // And flush any remaining messages
320
+ const extraFlushPromises = []
321
+ for (const qrl in messages) {
322
+ extraFlushPromises.push(flushMessages(qrl, opt))
323
+ }
324
+ const extraFlushCounts = await Promise.all(extraFlushPromises)
325
+ const extraFlushTotal = extraFlushCounts.reduce((a, b) => a + b, 0)
326
+ const totalFlushed = initialFlushTotal + extraFlushTotal
327
+ debug({ initialFlushTotal, extraFlushTotal, totalFlushed })
328
+ return totalFlushed
286
329
  }
287
330
 
288
331
  debug('loaded')