qdone 1.6.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 +17598 -264
- package/package.json +41 -29
- package/src/bin.js +3 -0
- package/src/cache.js +21 -25
- package/src/cli.js +269 -181
- 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/.DS_Store +0 -0
- package/.coveralls.yml +0 -2
- package/.travis.yml +0 -17
- package/CHANGELOG.md +0 -107
- package/dump.rdb +0 -0
- package/index.js +0 -6
- package/package-lock.json.old +0 -3939
- 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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functions that deal with CloudWatch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'
|
|
6
|
+
import Debug from 'debug'
|
|
7
|
+
const debug = Debug('qdone:cloudWatch')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Utility function to return an instantiated, shared CloudWatchClient.
|
|
11
|
+
*/
|
|
12
|
+
let client
|
|
13
|
+
export function getCloudWatchClient () {
|
|
14
|
+
if (client) return client
|
|
15
|
+
client = new CloudWatchClient()
|
|
16
|
+
return client
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Utility function to set the client explicitly, used in testing.
|
|
21
|
+
*/
|
|
22
|
+
export function setCloudWatchClient (explicitClient) {
|
|
23
|
+
client = explicitClient
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Takes data in the form returned by getAggregageData() and pushes it to
|
|
28
|
+
* CloudWatch metrics under the given queueName.
|
|
29
|
+
*
|
|
30
|
+
* @param queueName {String} - The name of the wildcard queue these metrics are for.
|
|
31
|
+
* @param total {Object} - returned object from getAggregateData()
|
|
32
|
+
*/
|
|
33
|
+
export async function putAggregateData (total, timestamp) {
|
|
34
|
+
const client = getCloudWatchClient()
|
|
35
|
+
const now = timestamp || new Date()
|
|
36
|
+
const input = {
|
|
37
|
+
Namespace: 'qmonitor',
|
|
38
|
+
MetricData: [
|
|
39
|
+
{
|
|
40
|
+
MetricName: 'totalQueues',
|
|
41
|
+
Dimensions: [{
|
|
42
|
+
Name: 'queueName',
|
|
43
|
+
Value: total.queueName
|
|
44
|
+
}],
|
|
45
|
+
Timestamp: now,
|
|
46
|
+
Value: total.totalQueues,
|
|
47
|
+
Unit: 'Count'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
MetricName: 'contributingQueueCount',
|
|
51
|
+
Dimensions: [{
|
|
52
|
+
Name: 'queueName',
|
|
53
|
+
Value: total.queueName
|
|
54
|
+
}],
|
|
55
|
+
Timestamp: now,
|
|
56
|
+
Value: total.contributingQueueNames.length,
|
|
57
|
+
Unit: 'Count'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
MetricName: 'ApproximateNumberOfMessages',
|
|
61
|
+
Dimensions: [{
|
|
62
|
+
Name: 'queueName',
|
|
63
|
+
Value: total.queueName
|
|
64
|
+
}],
|
|
65
|
+
Timestamp: now,
|
|
66
|
+
Value: total.ApproximateNumberOfMessages || 0,
|
|
67
|
+
Unit: 'Count'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
MetricName: 'ApproximateNumberOfMessagesDelayed',
|
|
71
|
+
Dimensions: [{
|
|
72
|
+
Name: 'queueName',
|
|
73
|
+
Value: total.queueName
|
|
74
|
+
}],
|
|
75
|
+
Timestamp: now,
|
|
76
|
+
Value: total.ApproximateNumberOfMessagesDelayed || 0,
|
|
77
|
+
Unit: 'Count'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
MetricName: 'ApproximateNumberOfMessagesNotVisible',
|
|
81
|
+
Dimensions: [{
|
|
82
|
+
Name: 'queueName',
|
|
83
|
+
Value: total.queueName
|
|
84
|
+
}],
|
|
85
|
+
Timestamp: now,
|
|
86
|
+
Value: total.ApproximateNumberOfMessagesNotVisible || 0,
|
|
87
|
+
Unit: 'Count'
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
const command = new PutMetricDataCommand(input)
|
|
92
|
+
// debug({ input, command })
|
|
93
|
+
const response = await client.send(command)
|
|
94
|
+
debug({ response })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
debug('loaded')
|
package/src/consumer.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer implementation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChangeMessageVisibilityCommand,
|
|
7
|
+
ReceiveMessageCommand,
|
|
8
|
+
DeleteMessageCommand
|
|
9
|
+
} from '@aws-sdk/client-sqs'
|
|
10
|
+
import chalk from 'chalk'
|
|
11
|
+
import Debug from 'debug'
|
|
12
|
+
|
|
13
|
+
import { normalizeQueueName, getQnameUrlPairs } from './qrlCache.js'
|
|
14
|
+
import { cheapIdleCheck } from './idleQueues.js'
|
|
15
|
+
import { getOptionsWithDefaults } from './defaults.js'
|
|
16
|
+
import { getSQSClient } from './sqs.js'
|
|
17
|
+
|
|
18
|
+
//
|
|
19
|
+
// Throwing an instance of this Error allows the processMessages callback to
|
|
20
|
+
// refuse a message which then gets immediately returned to the queue.
|
|
21
|
+
//
|
|
22
|
+
// This has the side effect of throtting the queue since it stops polling on
|
|
23
|
+
// the queue until the next queue resolution in processMessages.
|
|
24
|
+
//
|
|
25
|
+
// This is useful for implementing schedulers on top of qdone, for example, to
|
|
26
|
+
// look at the queue name and decide whether to take on a new message.
|
|
27
|
+
//
|
|
28
|
+
export class DoNotProcess extends Error {}
|
|
29
|
+
|
|
30
|
+
const debug = Debug('qdone:worker')
|
|
31
|
+
|
|
32
|
+
// Global flag for shutdown request
|
|
33
|
+
let shutdownRequested = false
|
|
34
|
+
const shutdownCallbacks = []
|
|
35
|
+
|
|
36
|
+
export function requestShutdown () {
|
|
37
|
+
shutdownRequested = true
|
|
38
|
+
for (const callback of shutdownCallbacks) {
|
|
39
|
+
try { callback() } catch (e) { }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function processMessage (message, callback, qname, qrl, opt) {
|
|
44
|
+
debug('processMessage', message, qname, qrl)
|
|
45
|
+
const payload = JSON.parse(message.Body)
|
|
46
|
+
if (opt.verbose) {
|
|
47
|
+
console.error(chalk.blue(' Processing payload:'), payload)
|
|
48
|
+
} else if (!opt.disableLog) {
|
|
49
|
+
console.log(JSON.stringify({
|
|
50
|
+
event: 'MESSAGE_PROCESSING_START',
|
|
51
|
+
timestamp: new Date(),
|
|
52
|
+
messageId: message.MessageId,
|
|
53
|
+
payload
|
|
54
|
+
}))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const jobStart = new Date()
|
|
58
|
+
let visibilityTimeout = 30 // this should be the queue timeout
|
|
59
|
+
let timeoutExtender
|
|
60
|
+
|
|
61
|
+
async function extendTimeout () {
|
|
62
|
+
debug('extendTimeout')
|
|
63
|
+
const maxJobRun = 12 * 60 * 60
|
|
64
|
+
const jobRunTime = ((new Date()) - jobStart) / 1000
|
|
65
|
+
// Double every time, up to max
|
|
66
|
+
visibilityTimeout = Math.min(visibilityTimeout * 2, maxJobRun - jobRunTime, opt.killAfter - jobRunTime)
|
|
67
|
+
if (opt.verbose) {
|
|
68
|
+
console.error(
|
|
69
|
+
chalk.blue(' Ran for ') + jobRunTime +
|
|
70
|
+
chalk.blue(' seconds, requesting another ') + visibilityTimeout +
|
|
71
|
+
chalk.blue(' seconds')
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
|
|
77
|
+
QueueUrl: qrl,
|
|
78
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
79
|
+
VisibilityTimeout: visibilityTimeout
|
|
80
|
+
}))
|
|
81
|
+
debug('ChangeMessageVisibility returned', result)
|
|
82
|
+
if (
|
|
83
|
+
jobRunTime + visibilityTimeout >= maxJobRun ||
|
|
84
|
+
jobRunTime + visibilityTimeout >= opt.killAfter
|
|
85
|
+
) {
|
|
86
|
+
if (opt.verbose) console.error(chalk.yellow(' warning: this is our last time extension'))
|
|
87
|
+
} else {
|
|
88
|
+
// Extend when we get 50% of the way to timeout
|
|
89
|
+
timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
debug('ChangeMessageVisibility threw', err)
|
|
93
|
+
// Rejection means we're ouuta time, whatever, let the job die
|
|
94
|
+
if (opt.verbose) {
|
|
95
|
+
console.error(chalk.red(' failed to extend job: ') + err)
|
|
96
|
+
} else if (!opt.disableLog) {
|
|
97
|
+
// Production error logging
|
|
98
|
+
console.log(JSON.stringify({
|
|
99
|
+
event: 'MESSAGE_PROCESSING_FAILED',
|
|
100
|
+
reason: 'ran longer than --kill-after',
|
|
101
|
+
timestamp: new Date(),
|
|
102
|
+
messageId: message.MessageId,
|
|
103
|
+
payload,
|
|
104
|
+
errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
|
|
105
|
+
err
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extend when we get 50% of the way to timeout
|
|
112
|
+
timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
|
|
113
|
+
debug('timeout', visibilityTimeout * 1000 * 0.5)
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Process message
|
|
117
|
+
const queue = qname.slice(opt.prefix.length)
|
|
118
|
+
const result = await callback(queue, payload)
|
|
119
|
+
debug('processMessage callback finished', { payload, result })
|
|
120
|
+
clearTimeout(timeoutExtender)
|
|
121
|
+
if (opt.verbose) {
|
|
122
|
+
console.error(chalk.green(' SUCCESS'))
|
|
123
|
+
console.error(chalk.blue(' cleaning up (removing message) ...'))
|
|
124
|
+
}
|
|
125
|
+
await getSQSClient().send(new DeleteMessageCommand({
|
|
126
|
+
QueueUrl: qrl,
|
|
127
|
+
ReceiptHandle: message.ReceiptHandle
|
|
128
|
+
}))
|
|
129
|
+
if (opt.verbose) {
|
|
130
|
+
console.error(chalk.blue(' done'))
|
|
131
|
+
console.error()
|
|
132
|
+
} else if (!opt.disableLog) {
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
event: 'MESSAGE_PROCESSING_COMPLETE',
|
|
135
|
+
timestamp: new Date(),
|
|
136
|
+
messageId: message.MessageId,
|
|
137
|
+
payload
|
|
138
|
+
}))
|
|
139
|
+
}
|
|
140
|
+
return { noJobs: 0, jobsSucceeded: 1, jobsFailed: 0 }
|
|
141
|
+
} catch (err) {
|
|
142
|
+
debug('exec.catch')
|
|
143
|
+
clearTimeout(timeoutExtender)
|
|
144
|
+
|
|
145
|
+
// If the callback does not want to process this message, return to queue
|
|
146
|
+
if (err instanceof DoNotProcess) {
|
|
147
|
+
if (opt.verbose) {
|
|
148
|
+
console.error(chalk.blue(' callback ') + chalk.yellow('REFUSED'))
|
|
149
|
+
console.error(chalk.blue(' cleaning up (removing message) ...'))
|
|
150
|
+
}
|
|
151
|
+
const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
|
|
152
|
+
QueueUrl: qrl,
|
|
153
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
154
|
+
VisibilityTimeout: 0
|
|
155
|
+
}))
|
|
156
|
+
debug('ChangeMessageVisibility returned', result)
|
|
157
|
+
return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fail path for job execution
|
|
161
|
+
if (opt.verbose) {
|
|
162
|
+
console.error(chalk.red(' FAILED'))
|
|
163
|
+
console.error(chalk.blue(' error : ') + err)
|
|
164
|
+
} else if (!opt.disableLog) {
|
|
165
|
+
// Production error logging
|
|
166
|
+
console.log(JSON.stringify({
|
|
167
|
+
event: 'MESSAGE_PROCESSING_FAILED',
|
|
168
|
+
reason: 'exception thrown',
|
|
169
|
+
timestamp: new Date(),
|
|
170
|
+
messageId: message.MessageId,
|
|
171
|
+
payload,
|
|
172
|
+
errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
|
|
173
|
+
err
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 1 }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//
|
|
181
|
+
// Pull work off of a single queue
|
|
182
|
+
//
|
|
183
|
+
export async function pollSingleQueue (qname, qrl, callback, opt) {
|
|
184
|
+
debug('pollSingleQueue', { qname, qrl, callback, opt })
|
|
185
|
+
const params = {
|
|
186
|
+
AttributeNames: ['All'],
|
|
187
|
+
MaxNumberOfMessages: 1,
|
|
188
|
+
MessageAttributeNames: ['All'],
|
|
189
|
+
QueueUrl: qrl,
|
|
190
|
+
VisibilityTimeout: 30,
|
|
191
|
+
WaitTimeSeconds: opt.waitTime
|
|
192
|
+
}
|
|
193
|
+
const response = await getSQSClient().send(new ReceiveMessageCommand(params))
|
|
194
|
+
debug('ReceiveMessage response', response)
|
|
195
|
+
if (shutdownRequested) return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
|
|
196
|
+
if (response.Messages) {
|
|
197
|
+
const message = response.Messages[0]
|
|
198
|
+
if (opt.verbose) console.error(chalk.blue(' Found message ' + message.MessageId))
|
|
199
|
+
return processMessage(message, callback, qname, qrl, opt)
|
|
200
|
+
} else {
|
|
201
|
+
return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//
|
|
206
|
+
// Resolve a set of queues
|
|
207
|
+
//
|
|
208
|
+
export async function resolveQueues (queues, opt) {
|
|
209
|
+
// Start processing
|
|
210
|
+
if (opt.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
|
|
211
|
+
const qnames = queues.map(queue => normalizeQueueName(queue, opt))
|
|
212
|
+
const pairs = await getQnameUrlPairs(qnames, opt)
|
|
213
|
+
|
|
214
|
+
// Figure out which pairs are active
|
|
215
|
+
const activePairs = []
|
|
216
|
+
if (opt.activeOnly) {
|
|
217
|
+
debug({ pairsBeforeCheck: pairs })
|
|
218
|
+
await Promise.all(pairs.map(async pair => {
|
|
219
|
+
const { idle } = await cheapIdleCheck(pair.qname, pair.qrl, opt)
|
|
220
|
+
if (!idle) activePairs.push(pair)
|
|
221
|
+
}))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Finished resolving
|
|
225
|
+
debug('getQnameUrlPairs.then')
|
|
226
|
+
if (opt.verbose) {
|
|
227
|
+
console.error(chalk.blue(' done'))
|
|
228
|
+
console.error()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Figure out which queues we want to listen on, choosing between active and
|
|
232
|
+
// all, filtering out failed queues if the user wants that
|
|
233
|
+
const selectedPairs = (opt.activeOnly ? activePairs : pairs)
|
|
234
|
+
.filter(({ qname }) => {
|
|
235
|
+
const suf = opt.failSuffix + (opt.fifo ? '.fifo' : '')
|
|
236
|
+
const isFailQueue = qname.slice(-suf.length) === suf
|
|
237
|
+
const shouldInclude = opt.includeFailed ? true : !isFailQueue
|
|
238
|
+
return shouldInclude
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return selectedPairs
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//
|
|
245
|
+
// Consumer
|
|
246
|
+
//
|
|
247
|
+
export async function processMessages (queues, callback, options) {
|
|
248
|
+
const opt = getOptionsWithDefaults(options)
|
|
249
|
+
debug('processMessages', { queues, callback, options, opt })
|
|
250
|
+
|
|
251
|
+
const stats = { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
|
|
252
|
+
const activeLoops = {}
|
|
253
|
+
|
|
254
|
+
// This delay function keeps a timeout reference around so it can be
|
|
255
|
+
// cancelled at shutdown
|
|
256
|
+
let delayTimeout
|
|
257
|
+
const delay = (ms) => new Promise(resolve => {
|
|
258
|
+
delayTimeout = setTimeout(resolve, ms)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Callback to help facilitate better UX at shutdown
|
|
262
|
+
function shutdownCallback () {
|
|
263
|
+
if (opt.verbose) {
|
|
264
|
+
debug({ activeLoops })
|
|
265
|
+
const activeQueues = Object.keys(activeLoops).filter(q => activeLoops[q]).map(q => q.slice(opt.prefix.length))
|
|
266
|
+
if (activeQueues.length) {
|
|
267
|
+
console.error(chalk.blue('Waiting for work to finish on the following queues: ') + activeQueues.join(chalk.blue(', ')))
|
|
268
|
+
}
|
|
269
|
+
clearTimeout(delayTimeout)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
shutdownCallbacks.push(shutdownCallback)
|
|
273
|
+
|
|
274
|
+
// Listen to a queue until it is out of messages
|
|
275
|
+
async function listenLoop (qname, qrl) {
|
|
276
|
+
try {
|
|
277
|
+
if (shutdownRequested) return
|
|
278
|
+
if (opt.verbose) {
|
|
279
|
+
console.error(
|
|
280
|
+
chalk.blue('Looking for work on ') +
|
|
281
|
+
qname.slice(opt.prefix.length) +
|
|
282
|
+
chalk.blue(' (' + qrl + ')')
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
// Aggregate the results
|
|
286
|
+
const { noJobs, jobsSucceeded, jobsFailed } = await pollSingleQueue(qname, qrl, callback, opt)
|
|
287
|
+
stats.noJobs += noJobs
|
|
288
|
+
stats.jobsFailed += jobsFailed
|
|
289
|
+
stats.jobsSucceeded += jobsSucceeded
|
|
290
|
+
|
|
291
|
+
// No work? return to outer loop
|
|
292
|
+
if (noJobs) return
|
|
293
|
+
|
|
294
|
+
// Otherwise keep going
|
|
295
|
+
return listenLoop(qname, qrl)
|
|
296
|
+
} catch (err) {
|
|
297
|
+
// TODO: Sentry
|
|
298
|
+
console.error(chalk.red(' ERROR in listenLoop'))
|
|
299
|
+
console.error(chalk.blue(' error : ') + err)
|
|
300
|
+
} finally {
|
|
301
|
+
delete activeLoops[qname]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Resolve loop
|
|
306
|
+
while (!shutdownRequested) { // eslint-disable-line
|
|
307
|
+
const start = new Date()
|
|
308
|
+
const selectedPairs = await resolveQueues(queues, opt)
|
|
309
|
+
if (shutdownRequested) break
|
|
310
|
+
|
|
311
|
+
// But only if we have queues to listen on
|
|
312
|
+
if (selectedPairs.length) {
|
|
313
|
+
// Randomize order
|
|
314
|
+
selectedPairs.sort(() => 0.5 - Math.random())
|
|
315
|
+
|
|
316
|
+
if (opt.verbose) {
|
|
317
|
+
console.error(chalk.blue('Listening to queues (in this order):'))
|
|
318
|
+
console.error(selectedPairs.map(({ qname, qrl }) =>
|
|
319
|
+
' ' + qname.slice(opt.prefix.length) + chalk.blue(' - ' + qrl)
|
|
320
|
+
).join('\n'))
|
|
321
|
+
console.error()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Launch listen loop for each queue
|
|
325
|
+
for (const { qname, qrl } of selectedPairs) {
|
|
326
|
+
if (!activeLoops[qname]) activeLoops[qname] = listenLoop(qname, qrl)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Wait until the next time we need to resolve
|
|
330
|
+
if (!shutdownRequested) {
|
|
331
|
+
const msSoFar = Math.max(0, new Date() - start)
|
|
332
|
+
const msUntilNextResolve = Math.max(0, opt.waitTime * 1000 - msSoFar)
|
|
333
|
+
debug({ msSoFar, msUntilNextResolve })
|
|
334
|
+
if (msUntilNextResolve) {
|
|
335
|
+
if (opt.verbose) console.error(chalk.blue('Will resolve queues again in ' + Math.round(msUntilNextResolve / 1000) + ' seconds'))
|
|
336
|
+
await delay(msUntilNextResolve)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Wait on all work to finish
|
|
342
|
+
// shutdownCallback()
|
|
343
|
+
await Promise.all(Object.values(activeLoops))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
debug('loaded')
|
package/src/defaults.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default options for qdone. Accepts a command line options object and
|
|
3
|
+
* returns nicely-named options.
|
|
4
|
+
*/
|
|
5
|
+
import { v1 as uuidv1 } from 'uuid'
|
|
6
|
+
|
|
7
|
+
export const defaults = Object.freeze({
|
|
8
|
+
// Shared
|
|
9
|
+
prefix: 'qdone_',
|
|
10
|
+
failSuffix: '_failed',
|
|
11
|
+
region: 'us-east-1',
|
|
12
|
+
quiet: false,
|
|
13
|
+
verbose: false,
|
|
14
|
+
cache: false,
|
|
15
|
+
cacheUri: undefined,
|
|
16
|
+
cachePrefix: 'qdone:',
|
|
17
|
+
cacheTtlSeconds: 10,
|
|
18
|
+
fifo: false,
|
|
19
|
+
disableLog: false,
|
|
20
|
+
includeFailed: false,
|
|
21
|
+
|
|
22
|
+
// Enqueue
|
|
23
|
+
groupId: uuidv1(),
|
|
24
|
+
groupIdPerMessage: false,
|
|
25
|
+
deduplicationId: uuidv1(),
|
|
26
|
+
messageRetentionPeriod: 1209600,
|
|
27
|
+
delay: 0,
|
|
28
|
+
dlq: false,
|
|
29
|
+
dlqSuffix: '_dead',
|
|
30
|
+
dlqAfter: 3,
|
|
31
|
+
|
|
32
|
+
// Worker
|
|
33
|
+
waitTime: 20,
|
|
34
|
+
killAfter: 30,
|
|
35
|
+
archive: false,
|
|
36
|
+
activeOnly: false,
|
|
37
|
+
|
|
38
|
+
// Idle Queues
|
|
39
|
+
idleFor: 60,
|
|
40
|
+
delete: false,
|
|
41
|
+
unpair: false
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function should be called by each exposed API entry point on the
|
|
46
|
+
* options passed in from the caller. It supports options named in camelCase
|
|
47
|
+
* and also in command-line-style returned by our parsers.
|
|
48
|
+
*
|
|
49
|
+
* By convention, we name the variable "options" if it comes from the user
|
|
50
|
+
* and "opt" if it has already passed through this function.
|
|
51
|
+
*/
|
|
52
|
+
export function getOptionsWithDefaults (options) {
|
|
53
|
+
// For API invocations don't force caller to supply default options
|
|
54
|
+
if (!options) options = {}
|
|
55
|
+
|
|
56
|
+
// Activate DLQ if any option is set
|
|
57
|
+
const dlq = options.dlq || !!(options['dlq-suffix'] || options['dlq-after'] || options['dlq-name'])
|
|
58
|
+
|
|
59
|
+
const opt = {
|
|
60
|
+
// Shared
|
|
61
|
+
prefix: options.prefix === '' ? options.prefix : (options.prefix || defaults.prefix),
|
|
62
|
+
failSuffix: options.failSuffix || options['fail-suffix'] || defaults.failSuffix,
|
|
63
|
+
region: options.region || process.env.AWS_REGION || defaults.region,
|
|
64
|
+
quiet: options.quiet || defaults.quiet,
|
|
65
|
+
verbose: options.verbose || defaults.verbose,
|
|
66
|
+
fifo: options.fifo || defaults.fifo,
|
|
67
|
+
sentryDsn: options.sentryDsn || options['sentry-dsn'],
|
|
68
|
+
disableLog: options.disableLog || options['disable-log'] || defaults.disableLog,
|
|
69
|
+
|
|
70
|
+
// Cache
|
|
71
|
+
cacheUri: options.cacheUri || options['cache-uri'] || defaults.cacheUri,
|
|
72
|
+
cachePrefix: options.cachePrefix || options['cache-prefix'] || defaults.cachePrefix,
|
|
73
|
+
cacheTtlSeconds: options.cacheTtlSeconds || options['cache-ttl-seconds'] || defaults.cacheTtlSeconds,
|
|
74
|
+
|
|
75
|
+
// Enqueue
|
|
76
|
+
groupId: options.groupId || options['group-id'] || defaults.groupId,
|
|
77
|
+
groupIdPerMessage: false,
|
|
78
|
+
deduplicationId: options.deduplicationId || options['deduplication-id'] || defaults.deduplicationId,
|
|
79
|
+
messageRetentionPeriod: options.messageRetentionPeriod || options['message-retention-period'] || defaults.messageRetentionPeriod,
|
|
80
|
+
delay: options.delay || defaults.delay,
|
|
81
|
+
dlq: dlq || defaults.dlq,
|
|
82
|
+
dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || defaults.dlqSuffix,
|
|
83
|
+
dlqAfter: options.dlqAfter || options['dlq-after'] || defaults.dlqAfter,
|
|
84
|
+
tags: options.tags || undefined,
|
|
85
|
+
|
|
86
|
+
// Worker
|
|
87
|
+
waitTime: options.waitTime || options['wait-time'] || defaults.waitTime,
|
|
88
|
+
killAfter: options.killAfter || options['kill-after'] || defaults.killAfter,
|
|
89
|
+
archive: options.archive || defaults.archive,
|
|
90
|
+
activeOnly: options.activeOnly || options['active-only'] || defaults.activeOnly,
|
|
91
|
+
includeFailed: options.includeFailed || options['include-failed'] || defaults.includeFailed,
|
|
92
|
+
|
|
93
|
+
// Idle Queues
|
|
94
|
+
idleFor: options.idleFor || options['idle-for'] || defaults.idleFor,
|
|
95
|
+
delete: options.delete || defaults.delete,
|
|
96
|
+
unpair: options.delete || defaults.unpair
|
|
97
|
+
}
|
|
98
|
+
process.env.AWS_REGION = opt.region
|
|
99
|
+
|
|
100
|
+
// TODO: validate options
|
|
101
|
+
return opt
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setupAWS (options) {
|
|
105
|
+
const opt = getOptionsWithDefaults(options)
|
|
106
|
+
process.env.AWS_REGION = opt.region
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function setupVerbose (options) {
|
|
110
|
+
const verbose = options.verbose || (process.stderr.isTTY && !options.quiet)
|
|
111
|
+
const quiet = options.quiet || (!process.stderr.isTTY && !options.verbose)
|
|
112
|
+
options.verbose = verbose
|
|
113
|
+
options.quiet = quiet
|
|
114
|
+
}
|