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/README.md +9 -1
- package/commonjs/index.js +10 -0
- package/commonjs/package.json +3 -0
- package/commonjs/src/cache.js +142 -0
- package/commonjs/src/cloudWatch.js +148 -0
- package/commonjs/src/consumer.js +483 -0
- package/commonjs/src/defaults.js +107 -0
- package/commonjs/src/enqueue.js +498 -0
- package/commonjs/src/idleQueues.js +466 -0
- package/commonjs/src/qrlCache.js +250 -0
- package/commonjs/src/sqs.js +160 -0
- package/npm-shrinkwrap.json +17240 -3367
- package/package.json +41 -29
- package/src/bin.js +3 -0
- package/src/cache.js +18 -22
- package/src/cli.js +268 -182
- package/src/cloudWatch.js +97 -0
- package/src/consumer.js +346 -0
- package/src/defaults.js +114 -0
- package/src/enqueue.js +239 -196
- package/src/idleQueues.js +242 -223
- package/src/monitor.js +53 -0
- package/src/qrlCache.js +110 -83
- package/src/sentry.js +30 -0
- package/src/sqs.js +73 -0
- package/src/worker.js +197 -202
- package/.coveralls.yml +0 -1
- package/.travis.yml +0 -19
- package/CHANGELOG.md +0 -121
- package/index.js +0 -6
- package/qdone +0 -2
- package/test/fixtures/test-child-kill-linux.sh +0 -9
- package/test/fixtures/test-fifo01-x24.batch +0 -24
- package/test/fixtures/test-too-big-1.batch +0 -10
- package/test/fixtures/test-unique01-x24.batch +0 -24
- package/test/fixtures/test-unique02-x24.batch +0 -24
- package/test/fixtures/test-unique24-x24.batch +0 -24
- package/test/fixtures/test-unique24-x240.batch +0 -240
- package/test/test.cache.js +0 -61
- package/test/test.cli.js +0 -1609
package/src/cli.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
28
|
-
{ name: 'fail-suffix', type: String,
|
|
29
|
-
{ name: 'region', type: String,
|
|
30
|
-
{ name: 'quiet', alias: 'q', type: Boolean,
|
|
31
|
-
{ name: 'verbose', alias: 'v', type: Boolean,
|
|
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,
|
|
35
|
-
{ name: 'cache-ttl-seconds', type: Number,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
216
|
+
let filenames, options
|
|
127
217
|
try {
|
|
128
|
-
|
|
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
|
-
|
|
224
|
+
filenames = options._unknown
|
|
135
225
|
} catch (err) {
|
|
136
226
|
console.log(getUsage(usageSections.filter(s => !s.long)))
|
|
137
|
-
|
|
227
|
+
throw err
|
|
138
228
|
}
|
|
139
229
|
|
|
140
230
|
// Load module after AWS global load
|
|
141
231
|
setupAWS(options)
|
|
142
|
-
const
|
|
143
|
-
const
|
|
232
|
+
const { enqueueBatch: enqueueBatchOriginal } = await import('./enqueue.js')
|
|
233
|
+
const enqueueBatch = testHook || enqueueBatchOriginal
|
|
144
234
|
|
|
145
235
|
// Load data and enqueue it
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
+
throw err
|
|
216
291
|
}
|
|
217
292
|
|
|
218
293
|
// Load module after AWS global load
|
|
219
294
|
setupAWS(options)
|
|
220
|
-
const
|
|
295
|
+
const { listen: originalListen, requestShutdown } = await import('./worker.js')
|
|
296
|
+
const listen = testHook || originalListen
|
|
221
297
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
371
|
+
export async function idleQueues (argv, testHook) {
|
|
293
372
|
const optionDefinitions = [
|
|
294
|
-
{ name: 'idle-for', alias: 'o', type: Number, defaultValue:
|
|
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
|
-
|
|
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 =
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
495
|
+
return enqueue(argv, testHook)
|
|
409
496
|
} else if (command === 'enqueue-batch') {
|
|
410
|
-
return
|
|
497
|
+
return enqueueBatch(argv, testHook)
|
|
411
498
|
} else if (command === 'worker') {
|
|
412
|
-
return
|
|
499
|
+
return worker(argv, testHook)
|
|
413
500
|
} else if (command === 'idle-queues') {
|
|
414
|
-
return
|
|
501
|
+
return idleQueues(argv, testHook)
|
|
502
|
+
} else if (command === 'monitor') {
|
|
503
|
+
return monitor(argv, testHook)
|
|
415
504
|
}
|
|
416
505
|
}
|
|
417
506
|
|
|
418
|
-
|
|
507
|
+
export async function run (argv, testHook) {
|
|
419
508
|
debug('run', argv)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
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')
|