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/qrlCache.js
CHANGED
|
@@ -1,134 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cache for maintaining queue urls. Avoids having to call the GetQueueUrl
|
|
3
|
+
* api too often.
|
|
4
|
+
*/
|
|
1
5
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
6
|
+
import { URL } from 'url'
|
|
7
|
+
import { basename } from 'path'
|
|
8
|
+
import { ListQueuesCommand, GetQueueUrlCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
|
|
9
|
+
import { getSQSClient } from './sqs.js'
|
|
10
|
+
import Debug from 'debug'
|
|
11
|
+
const debug = Debug('qdone:qrlCache')
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
// The actual cache is shared across callers.
|
|
14
|
+
const qcache = new Map()
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
//
|
|
17
|
+
// Normalizes a queue name to end with .fifo if opt.fifo is set
|
|
18
|
+
//
|
|
19
|
+
export const fifoSuffix = '.fifo'
|
|
20
|
+
export function normalizeQueueName (qname, opt) {
|
|
21
|
+
const hasFifo = qname.endsWith(fifoSuffix)
|
|
22
|
+
const needsFifo = opt.fifo && qname.slice(-1) !== '*'
|
|
23
|
+
const needsPrefix = !qname.startsWith(opt.prefix)
|
|
24
|
+
const base = hasFifo ? qname.slice(0, -fifoSuffix.length) : qname
|
|
25
|
+
return (needsPrefix ? opt.prefix : '') + base + (needsFifo ? fifoSuffix : '')
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
//
|
|
16
|
-
// Normalizes
|
|
29
|
+
// Normalizes fail queue name, appending both --fail-suffix and .fifo depending on opt
|
|
17
30
|
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
return base + (options.fifo && qname.slice(-1) !== '*' ? fifoSuffix : '')
|
|
31
|
+
export function normalizeFailQueueName (fqname, opt) {
|
|
32
|
+
const newopt = Object.assign({}, opt, { fifo: false })
|
|
33
|
+
const base = normalizeQueueName(fqname, newopt) // strip fifo
|
|
34
|
+
debug({ fqname, base, opt })
|
|
35
|
+
const needsFail = !base.endsWith(opt.failSuffix)
|
|
36
|
+
return base + (needsFail ? opt.failSuffix : '') + (opt.fifo ? fifoSuffix : '')
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
//
|
|
28
|
-
// Normalizes
|
|
40
|
+
// Normalizes dlq queue name, appending both --dlq-suffix and .fifo depending on opt
|
|
29
41
|
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
return (base + options['fail-suffix']) + (options.fifo ? fifoSuffix : '')
|
|
42
|
+
export function normalizeDLQName (dqname, opt) {
|
|
43
|
+
const newopt = Object.assign({}, opt, { fifo: false })
|
|
44
|
+
const base = normalizeQueueName(dqname, newopt) // strip fifo
|
|
45
|
+
const needsDead = !base.endsWith(opt.dlqSuffix)
|
|
46
|
+
return base + (needsDead ? opt.dlqSuffix : '') + (opt.fifo ? fifoSuffix : '')
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
//
|
|
39
50
|
// Clear cache
|
|
40
51
|
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
delete qcache[key]
|
|
44
|
-
})
|
|
52
|
+
export function qrlCacheClear () {
|
|
53
|
+
qcache.clear()
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
//
|
|
48
57
|
// Get QRL (Queue URL)
|
|
49
58
|
// Returns a promise for the queue name
|
|
50
59
|
//
|
|
51
|
-
|
|
60
|
+
export async function qrlCacheGet (qname) {
|
|
52
61
|
debug('get', qname, qcache)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (qcache.has(qname)) return qcache.get(qname)
|
|
63
|
+
const client = getSQSClient()
|
|
64
|
+
const input = { QueueName: qname }
|
|
65
|
+
const cmd = new GetQueueUrlCommand(input)
|
|
66
|
+
// debug({ cmd })
|
|
67
|
+
const result = await client.send(cmd)
|
|
68
|
+
// debug('result', result)
|
|
69
|
+
// if (!result) throw new Error(`No such queue ${qname}`)
|
|
70
|
+
const { QueueUrl: qrl } = result
|
|
71
|
+
// debug('getQueueUrl returned', data)
|
|
72
|
+
qcache.set(qname, qrl)
|
|
73
|
+
// debug('qcache', Object.keys(qcache), 'get', qname, ' => ', qcache[qname])
|
|
74
|
+
return qrl
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
//
|
|
67
78
|
// Set QRL (Queue URL)
|
|
68
79
|
// Immediately updates the cache
|
|
69
80
|
//
|
|
70
|
-
|
|
71
|
-
qcache
|
|
72
|
-
debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
81
|
+
export function qrlCacheSet (qname, qrl) {
|
|
82
|
+
qcache.set(qname, qrl)
|
|
83
|
+
// debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
//
|
|
76
87
|
// Invalidate cache for given qname
|
|
77
88
|
//
|
|
78
|
-
|
|
79
|
-
debug('qcache', Object.keys(qcache), 'delete', qname, ' (was ', qcache[qname], ')')
|
|
80
|
-
delete
|
|
89
|
+
export function qrlCacheInvalidate (qname) {
|
|
90
|
+
// debug('qcache', Object.keys(qcache), 'delete', qname, ' (was ', qcache[qname], ')')
|
|
91
|
+
qcache.delete(qname)
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
//
|
|
84
95
|
// Ingets multiple QRLs
|
|
85
96
|
// Extracts queue names from an array of QRLs and immediately updates the cache.
|
|
86
97
|
//
|
|
87
|
-
function ingestQRLs (qrls) {
|
|
98
|
+
export function ingestQRLs (qrls) {
|
|
88
99
|
const pairs = []
|
|
89
100
|
debug('ingestQRLs', qrls)
|
|
90
|
-
|
|
91
|
-
const qname =
|
|
92
|
-
|
|
93
|
-
pairs.push({ qname
|
|
94
|
-
debug('qcache', Object.keys(qcache), 'ingestQRLs', qname, ' => ', qcache[qname])
|
|
95
|
-
}
|
|
101
|
+
for (const qrl of qrls) {
|
|
102
|
+
const qname = basename((new URL(qrl)).pathname)
|
|
103
|
+
qrlCacheSet(qname, qrl)
|
|
104
|
+
pairs.push({ qname, qrl })
|
|
105
|
+
// debug('qcache', Object.keys(qcache), 'ingestQRLs', qname, ' => ', qcache[qname])
|
|
106
|
+
}
|
|
96
107
|
return pairs
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Returns qrls for queues matching the given prefix and regex.
|
|
112
|
+
*/
|
|
113
|
+
export async function getMatchingQueues (prefix, nextToken) {
|
|
114
|
+
debug('getMatchingQueues', prefix, nextToken)
|
|
115
|
+
const input = { QueueNamePrefix: prefix, MaxResults: 1000 }
|
|
116
|
+
if (nextToken) input.NextToken = nextToken
|
|
117
|
+
const client = getSQSClient()
|
|
118
|
+
const command = new ListQueuesCommand(input)
|
|
119
|
+
const result = await client.send(command)
|
|
120
|
+
debug({ result })
|
|
121
|
+
const { QueueUrls: qrls, NextToken: keepGoing } = result || {}
|
|
122
|
+
debug({ qrls, keepGoing })
|
|
123
|
+
if (keepGoing) (qrls || []).push(...await getMatchingQueues(prefix, keepGoing))
|
|
124
|
+
return qrls || []
|
|
125
|
+
}
|
|
126
|
+
|
|
99
127
|
//
|
|
100
128
|
// Resolves into a flattened aray of {qname: ..., qrl: ...} objects.
|
|
101
129
|
//
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return ingestQRLs(
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
130
|
+
export async function getQnameUrlPairs (qnames, opt) {
|
|
131
|
+
const promises = qnames.map(
|
|
132
|
+
async function getQueueUrlOrUrls (qname) {
|
|
133
|
+
if (qname.slice(-1) === '*') {
|
|
134
|
+
// Wildcard queues support
|
|
135
|
+
const prefix = qname.slice(0, -1)
|
|
136
|
+
const queues = await getMatchingQueues(prefix)
|
|
137
|
+
debug('listQueues return', queues)
|
|
138
|
+
if (opt.fifo) {
|
|
139
|
+
// Remove non-fifo queues
|
|
140
|
+
const filteredQueues = queues.filter(url => url.slice(-fifoSuffix.length) === fifoSuffix)
|
|
141
|
+
return ingestQRLs(filteredQueues)
|
|
142
|
+
} else {
|
|
143
|
+
return ingestQRLs(queues)
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Normal, non wildcard queue support
|
|
147
|
+
try {
|
|
148
|
+
return { qname, qrl: await qrlCacheGet(qname) }
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (!(err instanceof QueueDoesNotExist)) throw err
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Flatten nested results
|
|
157
|
+
const results = await Promise.all(promises)
|
|
158
|
+
return ([].concat.apply([], results)).filter(r => r)
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
debug('loaded')
|
package/src/sentry.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routines for handling Sentry instrumentation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Debug from 'debug'
|
|
6
|
+
import { init, captureException } from '@sentry/node'
|
|
7
|
+
|
|
8
|
+
const debug = Debug('qdone:sentry')
|
|
9
|
+
|
|
10
|
+
let sentryWasInit = false
|
|
11
|
+
export async function withSentry (callback, opt) {
|
|
12
|
+
// Bail if sentry isn't enabled
|
|
13
|
+
if (!opt.sentryDsn) return callback()
|
|
14
|
+
|
|
15
|
+
// Init sentry if it's not already
|
|
16
|
+
if (!sentryWasInit) {
|
|
17
|
+
init({ dsn: opt.sentryDsn, traceSampleRate: 0 })
|
|
18
|
+
sentryWasInit = true
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const result = await callback()
|
|
22
|
+
debug({ result })
|
|
23
|
+
return result
|
|
24
|
+
} catch (err) {
|
|
25
|
+
debug({ err })
|
|
26
|
+
const sentryResult = await captureException(err)
|
|
27
|
+
debug({ sentryResult })
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/sqs.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functions that deal with SQS
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SQSClient, ListQueuesCommand, GetQueueAttributesCommand } from '@aws-sdk/client-sqs'
|
|
6
|
+
import { basename } from 'path'
|
|
7
|
+
import Debug from 'debug'
|
|
8
|
+
const debug = Debug('qdone:sqs')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utility function to return an instantiated, shared SQSClient.
|
|
12
|
+
*/
|
|
13
|
+
let client
|
|
14
|
+
export function getSQSClient () {
|
|
15
|
+
if (client) return client
|
|
16
|
+
client = new SQSClient()
|
|
17
|
+
return client
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Utility function to set the client explicitly, used in testing.
|
|
22
|
+
*/
|
|
23
|
+
export function setSQSClient (explicitClient) {
|
|
24
|
+
client = explicitClient
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns qrls for queues matching the given prefix and regex.
|
|
29
|
+
*/
|
|
30
|
+
export async function getMatchingQueues (prefix, regex) {
|
|
31
|
+
const input = { QueueNamePrefix: prefix, MaxResults: 1000 }
|
|
32
|
+
const client = getSQSClient()
|
|
33
|
+
async function processQueues (nextToken) {
|
|
34
|
+
if (nextToken) input.NextToken = nextToken
|
|
35
|
+
const command = new ListQueuesCommand(input)
|
|
36
|
+
// debug({ nextToken, input, command })
|
|
37
|
+
const result = await client.send(command)
|
|
38
|
+
// debug({ result })
|
|
39
|
+
const { QueueUrls: qrls, NextToken: nextToken2 } = result
|
|
40
|
+
// debug({ qrls, nextToken2 })
|
|
41
|
+
return (qrls || []).filter(q => regex.test(q)).concat(nextToken2 ? await processQueues(nextToken2) : [])
|
|
42
|
+
}
|
|
43
|
+
return processQueues()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets attributes on every queue in parallel.
|
|
48
|
+
*/
|
|
49
|
+
export async function getQueueAttributes (qrls) {
|
|
50
|
+
const promises = []
|
|
51
|
+
// debug({ qrls })
|
|
52
|
+
for (const qrl of qrls) {
|
|
53
|
+
const input = {
|
|
54
|
+
QueueUrl: qrl,
|
|
55
|
+
AttributeNames: [
|
|
56
|
+
'ApproximateNumberOfMessages',
|
|
57
|
+
'ApproximateNumberOfMessagesNotVisible',
|
|
58
|
+
'ApproximateNumberOfMessagesDelayed'
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
const command = new GetQueueAttributesCommand(input)
|
|
62
|
+
// debug({ input, command })
|
|
63
|
+
promises.push((async () => {
|
|
64
|
+
const queue = basename(qrl)
|
|
65
|
+
const result = await client.send(command)
|
|
66
|
+
// debug({ queue, result })
|
|
67
|
+
return { queue, result }
|
|
68
|
+
})())
|
|
69
|
+
}
|
|
70
|
+
return Promise.all(promises)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
debug('loaded')
|