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/cli.js CHANGED
@@ -1,15 +1,23 @@
1
-
2
- const debug = require('debug')('qdone:cli')
3
- const Q = require('q')
4
- const fs = require('fs')
5
- const readline = require('readline')
6
- const chalk = require('chalk')
7
- const commandLineCommands = require('command-line-commands')
8
- const commandLineArgs = require('command-line-args')
9
- const getUsage = require('command-line-usage')
10
- const uuid = require('uuid')
1
+ /**
2
+ * Command line interface implementation
3
+ */
4
+ import { createReadStream, openSync } from 'node:fs'
5
+ import { createInterface } from 'node:readline'
6
+ import { createRequire } from 'module'
7
+ import getUsage from 'command-line-usage'
8
+ import commandLineCommands from 'command-line-commands'
9
+ import commandLineArgs from 'command-line-args'
10
+ import Debug from 'debug'
11
+ import chalk from 'chalk'
12
+
13
+ import { QueueDoesNotExist } from '@aws-sdk/client-sqs'
14
+ import { defaults, setupAWS, setupVerbose, getOptionsWithDefaults } from './defaults.js'
15
+ import { shutdownCache } from './cache.js'
16
+ import { withSentry } from './sentry.js'
17
+
18
+ const debug = Debug('qdone:cli')
19
+ const require = createRequire(import.meta.url)
11
20
  const packageJson = require('../package.json')
12
-
13
21
  class UsageError extends Error {}
14
22
 
15
23
  const awsUsageHeader = { content: 'AWS SQS Authentication', raw: true, long: true }
@@ -24,42 +32,33 @@ const awsUsageBody = {
24
32
  }
25
33
 
26
34
  const globalOptionDefinitions = [
27
- { name: 'prefix', type: String, defaultValue: 'qdone_', description: 'Prefix to place at the front of each SQS queue name [default: qdone_]' },
28
- { name: 'fail-suffix', type: String, defaultValue: '_failed', description: 'Suffix to append to each queue to generate fail queue name [default: _failed]' },
29
- { name: 'region', type: String, defaultValue: 'us-east-1', description: 'AWS region for Queues [default: us-east-1]' },
30
- { name: 'quiet', alias: 'q', type: Boolean, defaultValue: false, description: 'Turn on production logging. Automatically set if stderr is not a tty.' },
31
- { name: 'verbose', alias: 'v', type: Boolean, defaultValue: false, description: 'Turn on verbose output. Automatically set if stderr is a tty.' },
35
+ { name: 'prefix', type: String, description: `Prefix to place at the front of each SQS queue name [default: ${defaults.prefix}]` },
36
+ { name: 'fail-suffix', type: String, description: `Suffix to append to each queue to generate fail queue name [default: ${defaults.failSuffix}]` },
37
+ { name: 'region', type: String, description: `AWS region for Queues [default: ${defaults.region}]` },
38
+ { name: 'quiet', alias: 'q', type: Boolean, description: 'Turn on production logging. Automatically set if stderr is not a tty.' },
39
+ { name: 'verbose', alias: 'v', type: Boolean, description: 'Turn on verbose output. Automatically set if stderr is a tty.' },
32
40
  { name: 'version', alias: 'V', type: Boolean, description: 'Show version number' },
33
41
  { name: 'cache-uri', type: String, description: 'URL to caching cluster. Only redis://... currently supported.' },
34
- { name: 'cache-prefix', type: String, defaultValue: 'qdone:', description: 'Prefix for all keys in cache.' },
35
- { name: 'cache-ttl-seconds', type: Number, defaultValue: 10, description: 'Number of seconds to cache GetQueueAttributes calls.' },
36
- { name: 'help', type: Boolean, description: 'Print full help message.' }
42
+ { name: 'cache-prefix', type: String, description: `Prefix for all keys in cache. [default: ${defaults.cachePrefix}]` },
43
+ { name: 'cache-ttl-seconds', type: Number, description: `Number of seconds to cache GetQueueAttributes calls. [default: ${defaults.cacheTtlSeconds}]` },
44
+ { name: 'help', type: Boolean, description: 'Print full help message.' },
45
+ { name: 'sentry-dsn', type: String, description: 'Optional Sentry DSN to track unhandled errors.' }
37
46
  ]
38
47
 
39
- function setupAWS (options) {
40
- debug('loading aws-sdk')
41
- const AWS = require('aws-sdk')
42
- AWS.config.setPromisesDependency(Q.Promise)
43
- AWS.config.update({ region: options.region })
44
- AWS.config.logger = require('debug')('qdone:aws')
45
- debug('loaded')
46
- }
47
-
48
- function setupVerbose (options) {
49
- const verbose = options.verbose || (process.stderr.isTTY && !options.quiet)
50
- const quiet = options.quiet || (!process.stderr.isTTY && !options.verbose)
51
- options.verbose = verbose
52
- options.quiet = quiet
53
- }
54
-
55
48
  const enqueueOptionDefinitions = [
56
49
  { name: 'fifo', alias: 'f', type: Boolean, description: 'Create new queues as FIFOs' },
57
- { name: 'group-id', alias: 'g', type: String, defaultValue: uuid.v1(), description: 'FIFO Group ID to use for all messages enqueued in current command. Defaults to a string unique to this invocation.' },
50
+ { name: 'group-id', alias: 'g', type: String, description: 'FIFO Group ID to use for all messages enqueued in current command. Defaults to a string unique to this invocation.' },
58
51
  { name: 'group-id-per-message', type: Boolean, description: 'Use a unique Group ID for every message, even messages in the same batch.' },
59
- { name: 'deduplication-id', type: String, defaultValue: uuid.v1(), description: 'A Message Deduplication ID to give SQS when sending a message. Use this option if you are managing retries outside of qdone, and make sure the ID is the same for each retry in the deduplication window. Defaults to a string unique to this invocation.' }
52
+ { name: 'deduplication-id', type: String, description: 'A Message Deduplication ID to give SQS when sending a message. Use this option if you are managing retries outside of qdone, and make sure the ID is the same for each retry in the deduplication window. Defaults to a string unique to this invocation.' },
53
+ { name: 'message-retention-period', type: Number, description: `Number of seconds to retain jobs (up to 14 days). [default: ${defaults.messageRetentionPeriod}]` },
54
+ { name: 'delay', alias: 'd', type: Number, description: 'Delays delivery of each message by the given number of seconds (up to 900 seconds, or 15 minutes). Defaults to immediate delivery (no delay).' },
55
+ { name: 'dlq', type: Boolean, description: 'Send messages from the failed queue to a DLQ.' },
56
+ { name: 'dql-suffix', type: String, description: `Suffix to append to each queue to generate DLQ name [default: ${defaults.dlqSuffix}]` },
57
+ { name: 'dql-after', type: String, description: `Drives message to the DLQ after this many failures in the failed queue. [default: ${defaults.dlqAfter}]` },
58
+ { name: 'tag', type: String, multiple: true, description: 'Adds an AWS tag to queue creation. Use the format Key=Value. Can specify multiple times.' }
60
59
  ]
61
60
 
62
- exports.enqueue = function enqueue (argv) {
61
+ export async function enqueue (argv, testHook) {
63
62
  const optionDefinitions = [].concat(enqueueOptionDefinitions, globalOptionDefinitions)
64
63
  const usageSections = [
65
64
  { content: 'usage: qdone enqueue [options] <queue> <command>', raw: true },
@@ -77,14 +76,80 @@ exports.enqueue = function enqueue (argv) {
77
76
  debug('enqueue argv', argv)
78
77
 
79
78
  // Parse command and options
79
+ let options, queue, command
80
80
  try {
81
- var options = commandLineArgs(optionDefinitions, { argv, partial: true })
81
+ options = commandLineArgs(optionDefinitions, { argv, partial: true })
82
82
  setupVerbose(options)
83
83
  debug('enqueue options', options)
84
84
  if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
85
85
  if (!options._unknown || options._unknown.length !== 2) throw new UsageError('enqueue requires both <queue> and <command> arguments')
86
- var [queue, command] = options._unknown
86
+ queue = options._unknown[0]
87
+ command = options._unknown[1]
87
88
  debug('queue', queue, 'command', command)
89
+ } catch (err) {
90
+ console.log(getUsage(usageSections.filter(s => !s.long)))
91
+ throw err
92
+ }
93
+
94
+ // Process tags
95
+ if (options.tag && options.tag.length) {
96
+ options.tags = {}
97
+ for (const input of options.tag) {
98
+ debug({ input })
99
+ if (input.indexOf('=') === -1) throw new UsageError('Tags must be separated with the "=" character.')
100
+ const [key, ...rest] = input.split('=')
101
+ const value = rest.join('=')
102
+ debug({ input, key, rest, value, tags: options.tags })
103
+ options.tags[key] = value
104
+ }
105
+ }
106
+
107
+ // Load module after AWS global load
108
+ setupAWS(options)
109
+ const { enqueue: enqueueOriginal } = await import('./enqueue.js')
110
+ const enqueue = testHook || enqueueOriginal
111
+
112
+ // Normal (non batch) enqueue
113
+ const opt = getOptionsWithDefaults(options)
114
+ const result = (
115
+ await withSentry(async () => enqueue(queue, command, opt), opt)
116
+ )
117
+ debug('enqueue returned', result)
118
+ if (options.verbose) console.error(chalk.blue('Enqueued job ') + result.MessageId)
119
+ return result
120
+ }
121
+
122
+ const monitorOptionDefinitions = [
123
+ { name: 'save', alias: 's', type: Boolean, description: 'Saves data to CloudWatch' }
124
+ ]
125
+
126
+ export async function monitor (argv) {
127
+ const optionDefinitions = [].concat(monitorOptionDefinitions, globalOptionDefinitions)
128
+ const usageSections = [
129
+ { content: 'usage: qdone monitor <queuePattern> ', raw: true },
130
+ { content: 'Options', raw: true },
131
+ { optionList: optionDefinitions },
132
+ { content: 'SQS API Call Complexity', raw: true, long: true },
133
+ {
134
+ content: [
135
+ { count: '1 + N', summary: 'one call to resolve the queue names (potentially more calls if there are pages)\none call per queue to get attributes' }
136
+ ],
137
+ long: true
138
+ },
139
+ awsUsageHeader, awsUsageBody
140
+ ]
141
+ debug('monitor argv', argv)
142
+
143
+ // Parse command and options
144
+ let options, queue
145
+ try {
146
+ options = commandLineArgs(optionDefinitions, { argv, partial: true })
147
+ setupVerbose(options)
148
+ debug('enqueue options', options)
149
+ if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
150
+ if (!options._unknown || options._unknown.length !== 1) throw new UsageError('monitor requires the <queuePattern> argument')
151
+ queue = options._unknown[0]
152
+ debug('queue', queue)
88
153
  } catch (e) {
89
154
  console.log(getUsage(usageSections.filter(s => !s.long)))
90
155
  return Promise.reject(e)
@@ -92,19 +157,44 @@ exports.enqueue = function enqueue (argv) {
92
157
 
93
158
  // Load module after AWS global load
94
159
  setupAWS(options)
95
- const enqueue = require('./enqueue')
160
+ const { getAggregateData } = await import('./monitor.js')
161
+ const { putAggregateData } = await import('./cloudWatch.js')
162
+ const data = await getAggregateData(queue)
163
+ console.log(data)
164
+ if (options.save) {
165
+ process.stderr.write('Saving to CloudWatch...')
166
+ await putAggregateData(data)
167
+ process.stderr.write('done\n')
168
+ }
169
+ return data
170
+ }
96
171
 
97
- // Normal (non batch) enqueue
98
- return enqueue
99
- .enqueue(queue, command, options)
100
- .then(function (result) {
101
- debug('enqueue returned', result)
102
- if (options.verbose) console.error(chalk.blue('Enqueued job ') + result.MessageId)
103
- return result
172
+ export async function loadBatchFile (filename) {
173
+ const file = filename === '-' ? process.stdin : createReadStream(filename, { fd: openSync(filename, 'r') })
174
+ const pairs = []
175
+ await new Promise((resolve, reject) => {
176
+ debug('file', file.name || 'stdin')
177
+ // Construct (queue, command) pairs from input
178
+ const input = createInterface({ input: file })
179
+ input.on('line', line => {
180
+ const parts = line.split(/\s+/)
181
+ const queue = parts[0]
182
+ const command = line.slice(queue.length).trim()
183
+ pairs.push({ queue, command })
104
184
  })
185
+ input.on('error', reject)
186
+ input.on('close', resolve)
187
+ })
188
+ return pairs
189
+ }
190
+
191
+ export async function loadBatchFiles (filenames) {
192
+ const results = await Promise.all(filenames.map(loadBatchFile))
193
+ const pairs = results.flat()
194
+ return pairs
105
195
  }
106
196
 
107
- exports.enqueueBatch = function enqueueBatch (argv) {
197
+ export async function enqueueBatch (argv, testHook) {
108
198
  const optionDefinitions = [].concat(enqueueOptionDefinitions, globalOptionDefinitions)
109
199
  const usageSections = [
110
200
  { content: 'usage: qdone enqueue-batch [options] <file...>', raw: true },
@@ -123,61 +213,46 @@ exports.enqueueBatch = function enqueueBatch (argv) {
123
213
  debug('enqueue-batch argv', argv)
124
214
 
125
215
  // Parse command and options
126
- let files
216
+ let filenames, options
127
217
  try {
128
- var options = commandLineArgs(optionDefinitions, { argv, partial: true })
218
+ options = commandLineArgs(optionDefinitions, { argv, partial: true })
129
219
  setupVerbose(options)
130
220
  debug('enqueue-batch options', options)
131
221
  if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
132
222
  if (!options._unknown || options._unknown.length === 0) throw new UsageError('enqueue-batch requres one or more <file> arguments')
133
223
  debug('filenames', options._unknown)
134
- files = options._unknown.map(f => f === '-' ? process.stdin : fs.createReadStream(f, { fd: fs.openSync(f, 'r') }))
224
+ filenames = options._unknown
135
225
  } catch (err) {
136
226
  console.log(getUsage(usageSections.filter(s => !s.long)))
137
- return Promise.reject(err)
227
+ throw err
138
228
  }
139
229
 
140
230
  // Load module after AWS global load
141
231
  setupAWS(options)
142
- const enqueue = require('./enqueue')
143
- const pairs = []
232
+ const { enqueueBatch: enqueueBatchOriginal } = await import('./enqueue.js')
233
+ const enqueueBatch = testHook || enqueueBatchOriginal
144
234
 
145
235
  // Load data and enqueue it
146
- return Promise.all(
147
- files.map(function (file) {
148
- // Construct (queue, command) pairs from input
149
- debug('file', file.name || 'stdin')
150
- const input = readline.createInterface({ input: file })
151
- const deferred = Q.defer()
152
- input.on('line', line => {
153
- const parts = line.split(/\s+/)
154
- const queue = parts[0]
155
- const command = line.slice(queue.length).trim()
156
- pairs.push({ queue, command })
157
- })
158
- input.on('error', deferred.reject)
159
- input.on('close', deferred.resolve)
160
- return deferred.promise
161
- })
236
+ const pairs = await loadBatchFiles(filenames)
237
+ debug('pairs', pairs)
238
+
239
+ // Normal (non batch) enqueue
240
+ const opt = getOptionsWithDefaults(options)
241
+ const result = (
242
+ await withSentry(async () => enqueueBatch(pairs, opt), opt)
162
243
  )
163
- .then(function () {
164
- debug('pairs', pairs)
165
- return enqueue
166
- .enqueueBatch(pairs, options)
167
- .then(function (result) {
168
- debug('enqueueBatch returned', result)
169
- if (options.verbose) console.error(chalk.blue('Enqueued ') + result + chalk.blue(' jobs'))
170
- })
171
- })
244
+ debug('enqueueBatch returned', result)
245
+ if (options.verbose) console.error(chalk.blue('Enqueued ') + result + chalk.blue(' jobs'))
172
246
  }
173
247
 
174
- exports.worker = function worker (argv) {
248
+ export async function worker (argv, testHook) {
175
249
  const optionDefinitions = [
176
250
  { name: 'kill-after', alias: 'k', type: Number, defaultValue: 30, description: 'Kill job after this many seconds [default: 30]' },
177
251
  { name: 'wait-time', alias: 'w', type: Number, defaultValue: 20, description: 'Listen at most this long on each queue [default: 20]' },
178
252
  { name: 'include-failed', type: Boolean, description: 'When using \'*\' do not ignore fail queues.' },
179
253
  { name: 'active-only', type: Boolean, description: 'Listen only to queues with pending messages.' },
180
254
  { name: 'drain', type: Boolean, description: 'Run until no more work is found and quit. NOTE: if used with --wait-time 0, this option will not drain queues.' },
255
+ { name: 'archive', type: Boolean, description: 'Does not run jobs, just prints commands to stdout. Use this flag for draining a queue and recording the commands that were in it.' },
181
256
  { name: 'fifo', alias: 'f', type: Boolean, description: 'Automatically adds .fifo to queue names. Only listens to fifo queues when using \'*\'.' }
182
257
  ].concat(globalOptionDefinitions)
183
258
 
@@ -200,9 +275,9 @@ exports.worker = function worker (argv) {
200
275
  debug('enqueue-batch argv', argv)
201
276
 
202
277
  // Parse command and options
203
- let queues
278
+ let queues, options
204
279
  try {
205
- var options = commandLineArgs(optionDefinitions, { argv, partial: true })
280
+ options = commandLineArgs(optionDefinitions, { argv, partial: true })
206
281
  setupVerbose(options)
207
282
  debug('worker options', options)
208
283
  if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
@@ -212,17 +287,18 @@ exports.worker = function worker (argv) {
212
287
  debug('queues', queues)
213
288
  } catch (err) {
214
289
  console.log(getUsage(usageSections.filter(s => !s.long)))
215
- return Promise.reject(err)
290
+ throw err
216
291
  }
217
292
 
218
293
  // Load module after AWS global load
219
294
  setupAWS(options)
220
- const worker = require('./worker')
295
+ const { listen: originalListen, requestShutdown } = await import('./worker.js')
296
+ const listen = testHook || originalListen
221
297
 
222
- var jobCount = 0
223
- var jobsSucceeded = 0
224
- var jobsFailed = 0
225
- var shutdownRequested = false
298
+ let jobCount = 0
299
+ let jobsSucceeded = 0
300
+ let jobsFailed = 0
301
+ let shutdownRequested = false
226
302
 
227
303
  function handleShutdown () {
228
304
  // Second signal forces shutdown
@@ -231,7 +307,7 @@ exports.worker = function worker (argv) {
231
307
  process.kill(-process.pid, 'SIGKILL')
232
308
  }
233
309
  shutdownRequested = true
234
- worker.requestShutdown()
310
+ requestShutdown()
235
311
  if (options.verbose) {
236
312
  console.error(chalk.yellow('Shutdown requested. Will stop when current job is done or a second signal is recieved.'))
237
313
  if (process.stdout.isTTY) {
@@ -242,56 +318,59 @@ exports.worker = function worker (argv) {
242
318
  process.on('SIGINT', handleShutdown)
243
319
  process.on('SIGTERM', handleShutdown)
244
320
 
245
- function workLoop () {
321
+ async function workLoop () {
246
322
  if (shutdownRequested) {
247
323
  if (options.verbose) console.error(chalk.blue('Shutting down as requested.'))
248
324
  return Promise.resolve()
249
325
  }
250
- return worker
251
- .listen(queues, options)
252
- .then(function (result) {
253
- debug('listen returned', result)
254
-
255
- // Handle delay in the case we don't have any queues
256
- if (result === 'noQueues') {
257
- const roundDelay = Math.max(1000, options['wait-time'] * 1000)
258
- if (options.verbose) console.error(chalk.yellow('No queues to listen on!'))
259
- if (options.drain) {
260
- console.error(chalk.blue('Shutting down because we are in drain mode and no work is available.'))
261
- return Promise.resolve()
262
- }
263
- console.error(chalk.yellow('Retrying in ' + (roundDelay / 1000) + 's'))
264
- return Q.delay(roundDelay).then(workLoop)
265
- }
266
-
267
- const ranJob = (result.jobsSucceeded + result.jobsFailed) > 0
268
- jobCount += result.jobsSucceeded + result.jobsFailed
269
- jobsFailed += result.jobsFailed
270
- jobsSucceeded += result.jobsSucceeded
271
- // Draining continues to listen as long as there is work
272
- if (options.drain) {
273
- if (ranJob) return workLoop()
274
- if (options.verbose) {
275
- console.error(chalk.blue('Ran ') + jobCount + chalk.blue(' jobs: ') + jobsSucceeded + chalk.blue(' succeeded ') + jobsFailed + chalk.blue(' failed'))
276
- }
277
- // return Promise.resolve(jobCount)
278
- } else {
279
- // If we're not draining, loop forever
280
- // We can go immediately if we just ran a job
281
- if (ranJob) return workLoop()
282
- // Otherwise, we could do backoff logic here to slow down requests when
283
- // work is not happening (at the expense of latency)
284
- // But we won't do that now.
285
- return workLoop()
286
- }
287
- })
326
+ // const result = await listen(queues, options)
327
+ const opt = getOptionsWithDefaults(options)
328
+ const result = (
329
+ await withSentry(async () => listen(queues, opt), opt)
330
+ )
331
+ debug('listen returned', result)
332
+
333
+ // Handle delay in the case we don't have any queues
334
+ if (result === 'noQueues') {
335
+ const roundDelay = Math.max(1000, options['wait-time'] * 1000)
336
+ if (options.verbose) console.error(chalk.yellow('No queues to listen on!'))
337
+ if (options.drain) {
338
+ console.error(chalk.blue('Shutting down because we are in drain mode and no work is available.'))
339
+ return Promise.resolve()
340
+ }
341
+ console.error(chalk.yellow('Retrying in ' + (roundDelay / 1000) + 's'))
342
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
343
+ return delay(roundDelay).then(workLoop)
344
+ }
345
+
346
+ const ranJob = (result.jobsSucceeded + result.jobsFailed) > 0
347
+ jobCount += result.jobsSucceeded + result.jobsFailed
348
+ jobsFailed += result.jobsFailed
349
+ jobsSucceeded += result.jobsSucceeded
350
+ // Draining continues to listen as long as there is work
351
+ if (options.drain) {
352
+ if (ranJob) return workLoop()
353
+ if (options.verbose) {
354
+ console.error(chalk.blue('Ran ') + jobCount + chalk.blue(' jobs: ') + jobsSucceeded + chalk.blue(' succeeded ') + jobsFailed + chalk.blue(' failed'))
355
+ }
356
+ // return Promise.resolve(jobCount)
357
+ } else {
358
+ // If we're not draining, loop forever
359
+ // We can go immediately if we just ran a job
360
+ if (ranJob) return workLoop()
361
+ // Otherwise, we could do backoff logic here to slow down requests when
362
+ // work is not happening (at the expense of latency)
363
+ // But we won't do that now.
364
+ return workLoop()
365
+ }
288
366
  }
367
+
289
368
  return workLoop()
290
369
  }
291
370
 
292
- exports.idleQueues = function idleQueues (argv) {
371
+ export async function idleQueues (argv, testHook) {
293
372
  const optionDefinitions = [
294
- { name: 'idle-for', alias: 'o', type: Number, defaultValue: 60, description: 'Minutes of inactivity after which a queue is considered idle. [default: 60]' },
373
+ { name: 'idle-for', alias: 'o', type: Number, defaultValue: defaults.idleFor, description: `Minutes of inactivity after which a queue is considered idle. [default: ${defaults.idleFor}]` },
295
374
  { name: 'delete', type: Boolean, description: 'Delete the queue if it is idle. The fail queue also must be idle unless you use --unpair.' },
296
375
  { name: 'unpair', type: Boolean, description: 'Treat queues and their fail queues as independent. By default they are treated as a unit.' },
297
376
  { name: 'include-failed', type: Boolean, description: 'When using \'*\' do not ignore fail queues. This option only applies if you use --unpair. Otherwise, queues and fail queues are treated as a unit.' }
@@ -326,9 +405,9 @@ exports.idleQueues = function idleQueues (argv) {
326
405
  debug('idleQueues argv', argv)
327
406
 
328
407
  // Parse command and options
329
- let queues
408
+ let queues, options
330
409
  try {
331
- var options = commandLineArgs(optionDefinitions, { argv, partial: true })
410
+ options = commandLineArgs(optionDefinitions, { argv, partial: true })
332
411
  setupVerbose(options)
333
412
  debug('idleQueues options', options)
334
413
  if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
@@ -344,48 +423,56 @@ exports.idleQueues = function idleQueues (argv) {
344
423
 
345
424
  // Load module after AWS global load
346
425
  setupAWS(options)
347
- const idleQueues = require('./idleQueues')
348
-
349
- return idleQueues
350
- .idleQueues(queues, options)
351
- .then(function (result) {
352
- debug('idleQueues returned', result)
353
- if (result === 'noQueues') return Promise.resolve()
354
- const callsSQS = result.map(a => a.apiCalls.SQS).reduce((a, b) => a + b, 0)
355
- const callsCloudWatch = result.map(a => a.apiCalls.CloudWatch).reduce((a, b) => a + b, 0)
356
- if (options.verbose) console.error(chalk.blue('Used ') + callsSQS + chalk.blue(' SQS and ') + callsCloudWatch + chalk.blue(' CloudWatch API calls.'))
357
- // Print idle queues to stdout
358
- result.filter(a => a.idle).map(a => a.queue).forEach(q => console.log(q))
359
- return result
360
- })
361
- .catch(err => {
362
- if (err.code === 'AWS.SimpleQueueService.NonExistentQueue') {
363
- console.error(chalk.yellow('This error can occur when you run this command immediately after deleting a queue. Wait 60 seconds and try again.'))
364
- return Promise.reject(err)
365
- }
366
- })
426
+ const { idleQueues: idleQueuesOriginal } = await import('./idleQueues.js')
427
+ const idleQueues = testHook || idleQueuesOriginal
428
+ const opt = getOptionsWithDefaults(options)
429
+ try {
430
+ const result = (
431
+ await withSentry(async () => idleQueues(queues, opt), opt)
432
+ )
433
+ debug('idleQueues returned', result)
434
+ if (result === 'noQueues') return Promise.resolve()
435
+ const callsSQS = result.map(a => a.apiCalls.SQS).reduce((a, b) => a + b, 0)
436
+ const callsCloudWatch = result.map(a => a.apiCalls.CloudWatch).reduce((a, b) => a + b, 0)
437
+ if (options.verbose) console.error(chalk.blue('Used ') + callsSQS + chalk.blue(' SQS and ') + callsCloudWatch + chalk.blue(' CloudWatch API calls.'))
438
+
439
+ // Print idle queues to stdout
440
+ result.filter(a => a.idle).map(a => a.queue).forEach(q => console.log(q))
441
+ return result
442
+ } catch (err) {
443
+ if (err instanceof QueueDoesNotExist) {
444
+ console.error(chalk.yellow('This error can occur when you run this command immediately after deleting a queue. Wait 60 seconds and try again.'))
445
+ }
446
+ throw err
447
+ }
367
448
  }
368
449
 
369
- exports.root = function root (originalArgv) {
370
- const validCommands = [null, 'enqueue', 'enqueue-batch', 'worker', 'idle-queues']
450
+ export async function root (originalArgv, testHook) {
451
+ const validCommands = [null, 'enqueue', 'enqueue-batch', 'worker', 'idle-queues', 'monitor']
371
452
  const usageSections = [
372
453
  { content: 'qdone - Command line job queue for SQS', raw: true, long: true },
373
454
  { content: 'usage: qdone [options] <command>', raw: true },
374
455
  { content: 'Commands', raw: true },
375
- { content: [
376
- { name: 'enqueue', summary: 'Enqueue a single command' },
377
- { name: 'enqueue-batch', summary: 'Enqueue multiple commands from stdin or a file' },
378
- { name: 'worker', summary: 'Execute work on one or more queues' },
379
- { name: 'idle-queues', summary: 'Write a list of idle queues to stdout' }
380
- ] },
456
+ {
457
+ content: [
458
+ { name: 'enqueue', summary: 'Enqueue a single command' },
459
+ { name: 'enqueue-batch', summary: 'Enqueue multiple commands from stdin or a file' },
460
+ { name: 'worker', summary: 'Execute work on one or more queues' },
461
+ { name: 'idle-queues', summary: 'Write a list of idle queues to stdout' },
462
+ { name: 'monitor', summary: 'Monitor multiple queues at once' }
463
+ ]
464
+ },
381
465
  { content: 'Global Options', raw: true },
382
466
  { optionList: globalOptionDefinitions },
383
467
  awsUsageHeader, awsUsageBody
384
468
  ]
385
469
 
386
470
  // Parse command and options
471
+ let command, argv
387
472
  try {
388
- var { command, argv } = commandLineCommands(validCommands, originalArgv)
473
+ const parsed = commandLineCommands(validCommands, originalArgv)
474
+ command = parsed.command
475
+ argv = parsed.argv
389
476
  debug('command', command)
390
477
 
391
478
  // Root command
@@ -393,7 +480,7 @@ exports.root = function root (originalArgv) {
393
480
  const options = commandLineArgs(globalOptionDefinitions, { argv: originalArgv })
394
481
  setupVerbose(options)
395
482
  debug('options', options)
396
- if (options.version) return Promise.resolve(console.log(packageJson.version))
483
+ if (options.version) return console.log(packageJson.version)
397
484
  else if (options.help) return Promise.resolve(console.log(getUsage(usageSections)))
398
485
  else console.log(getUsage(usageSections.filter(s => !s.long)))
399
486
  return Promise.resolve()
@@ -405,32 +492,31 @@ exports.root = function root (originalArgv) {
405
492
 
406
493
  // Run child commands
407
494
  if (command === 'enqueue') {
408
- return exports.enqueue(argv)
495
+ return enqueue(argv, testHook)
409
496
  } else if (command === 'enqueue-batch') {
410
- return exports.enqueueBatch(argv)
497
+ return enqueueBatch(argv, testHook)
411
498
  } else if (command === 'worker') {
412
- return exports.worker(argv)
499
+ return worker(argv, testHook)
413
500
  } else if (command === 'idle-queues') {
414
- return exports.idleQueues(argv)
501
+ return idleQueues(argv, testHook)
502
+ } else if (command === 'monitor') {
503
+ return monitor(argv, testHook)
415
504
  }
416
505
  }
417
506
 
418
- exports.run = function run (argv) {
507
+ export async function run (argv, testHook) {
419
508
  debug('run', argv)
420
- return exports
421
- .root(argv)
422
- .then(() => {
423
- // If cache actually is active, it will keep our program from exiting
424
- // until we disconnect the cache client
425
- const cache = require('./cache')
426
- cache.resetClient()
427
- })
428
- .catch(function (err) {
429
- if (err.code === 'AccessDenied') console.log(getUsage([awsUsageHeader, awsUsageBody]))
430
- console.error(chalk.red.bold(err))
431
- console.error(err.stack.slice(err.stack.indexOf('\n') + 1))
432
- throw err
433
- })
509
+ try {
510
+ await root(argv, testHook)
511
+ // If cache actually is active, it will keep our program from exiting
512
+ // until we disconnect the cache client
513
+ shutdownCache()
514
+ } catch (err) {
515
+ if (err.Code === 'AccessDenied') console.log(getUsage([awsUsageHeader, awsUsageBody]))
516
+ console.error(chalk.red.bold(err))
517
+ console.error(err.stack.slice(err.stack.indexOf('\n') + 1))
518
+ throw err
519
+ }
434
520
  }
435
521
 
436
522
  debug('loaded')