qdone 2.0.29-alpha → 2.0.31-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/commonjs/src/cache.js +22 -93
- package/commonjs/src/cloudWatch.js +65 -111
- package/commonjs/src/consumer.js +136 -237
- package/commonjs/src/defaults.js +11 -11
- package/commonjs/src/enqueue.js +311 -503
- package/commonjs/src/exponentialBackoff.js +39 -110
- package/commonjs/src/idleQueues.js +255 -396
- package/commonjs/src/monitor.js +42 -115
- package/commonjs/src/qrlCache.js +80 -162
- package/commonjs/src/scheduler/jobExecutor.js +324 -363
- package/commonjs/src/scheduler/queueManager.js +111 -198
- package/commonjs/src/scheduler/systemMonitor.js +24 -28
- package/commonjs/src/sqs.js +58 -141
- package/npm-shrinkwrap.json +15999 -0
- package/package.json +2 -2
- package/src/consumer.js +6 -7
- package/src/defaults.js +3 -3
- package/src/enqueue.js +1 -1
- package/src/scheduler/jobExecutor.js +97 -33
- package/src/scheduler/queueManager.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdone",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.31-alpha",
|
|
4
4
|
"description": "Language agnostic job queue for SQS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"start": "./src/bin.js",
|
|
49
49
|
"test": "NODE_OPTIONS='--experimental-json-modules --experimental-vm-modules --no-warnings' jest",
|
|
50
|
-
"build": "tsc --allowJs index.js --outdir commonjs --esModuleInterop --module commonjs",
|
|
50
|
+
"build": "tsc --allowJs index.js --outdir commonjs --esModuleInterop --module commonjs --target es2021 --strict --skipLibCheck --forceConsistentCasingInFileNames",
|
|
51
51
|
"clean": "rm -rf commonjs/src commonjs/*.js coverage",
|
|
52
52
|
"lint": "standard",
|
|
53
53
|
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
package/src/consumer.js
CHANGED
|
@@ -96,9 +96,7 @@ export async function processMessages (queues, callback, options) {
|
|
|
96
96
|
|
|
97
97
|
if (!shutdownRequested) {
|
|
98
98
|
if (messages.length) {
|
|
99
|
-
|
|
100
|
-
jobExecutor.executeJob(message, callback, qname, qrl)
|
|
101
|
-
}
|
|
99
|
+
jobExecutor.executeJobs(messages, callback, qname, qrl)
|
|
102
100
|
queueManager.updateIcehouse(qrl, false)
|
|
103
101
|
} else {
|
|
104
102
|
// If we didn't get any, update the icehouse so we can back off
|
|
@@ -107,7 +105,7 @@ export async function processMessages (queues, callback, options) {
|
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
// Max job accounting
|
|
110
|
-
maxReturnCount -=
|
|
108
|
+
if (messages.length) maxReturnCount -= messages.length
|
|
111
109
|
activeQrls.delete(qrl)
|
|
112
110
|
} catch (e) {
|
|
113
111
|
// If the queue has been cleaned up, we should back off anyway
|
|
@@ -121,7 +119,7 @@ export async function processMessages (queues, callback, options) {
|
|
|
121
119
|
|
|
122
120
|
while (!shutdownRequested) { // eslint-disable-line
|
|
123
121
|
// Figure out how we are running
|
|
124
|
-
const allowedJobs = Math.max(0, opt.maxConcurrentJobs - jobExecutor.
|
|
122
|
+
const allowedJobs = Math.max(0, opt.maxConcurrentJobs - jobExecutor.runningJobCount() - maxReturnCount)
|
|
125
123
|
|
|
126
124
|
// Latency
|
|
127
125
|
const maxLatency = 100
|
|
@@ -146,10 +144,11 @@ export async function processMessages (queues, callback, options) {
|
|
|
146
144
|
let jobsLeft = targetJobs
|
|
147
145
|
|
|
148
146
|
if (opt.verbose) {
|
|
149
|
-
console.error({ maxConcurrentJobs: opt.maxConcurrentJobs, jobCount: jobExecutor.activeJobCount(), allowedJobs, maxLatency, latencyFactor, freememFactor, loadFactor, overallFactor, targetJobs, activeQrls })
|
|
147
|
+
// console.error({ maxConcurrentJobs: opt.maxConcurrentJobs, jobCount: jobExecutor.activeJobCount(), allowedJobs, maxLatency, latencyFactor, freememFactor, loadFactor, overallFactor, targetJobs, activeQrls })
|
|
150
148
|
}
|
|
151
149
|
for (const { qname, qrl } of queueManager.getPairs()) {
|
|
152
|
-
//
|
|
150
|
+
// const qcount = jobExecutor.runningJobCountForQueue(qname)
|
|
151
|
+
// console.log({ evaluating: { qname, qrl, qcount, jobsLeft, activeQrlsHasQrl: activeQrls.has(qrl) } })
|
|
153
152
|
if (jobsLeft <= 0 || activeQrls.has(qrl)) continue
|
|
154
153
|
const maxMessages = Math.min(10, jobsLeft)
|
|
155
154
|
listen(qname, qrl, maxMessages)
|
package/src/defaults.js
CHANGED
|
@@ -26,8 +26,8 @@ export const defaults = Object.freeze({
|
|
|
26
26
|
messageRetentionPeriod: 1209600,
|
|
27
27
|
delay: 0,
|
|
28
28
|
sendRetries: 6,
|
|
29
|
-
failDelay:
|
|
30
|
-
dlq:
|
|
29
|
+
failDelay: 120,
|
|
30
|
+
dlq: true,
|
|
31
31
|
dlqSuffix: '_dead',
|
|
32
32
|
dlqAfter: 3,
|
|
33
33
|
|
|
@@ -90,7 +90,7 @@ export function getOptionsWithDefaults (options) {
|
|
|
90
90
|
delay: options.delay || defaults.delay,
|
|
91
91
|
sendRetries: options['send-retries'] || defaults.sendRetries,
|
|
92
92
|
failDelay: options.failDelay || options['fail-delay'] || defaults.failDelay,
|
|
93
|
-
dlq: dlq || defaults.dlq,
|
|
93
|
+
dlq: dlq === false ? false : (dlq || defaults.dlq),
|
|
94
94
|
dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || defaults.dlqSuffix,
|
|
95
95
|
dlqAfter: options.dlqAfter || options['dlq-after'] || defaults.dlqAfter,
|
|
96
96
|
tags: options.tags || undefined,
|
package/src/enqueue.js
CHANGED
|
@@ -79,7 +79,7 @@ export async function getOrCreateFailQueue (queue, opt) {
|
|
|
79
79
|
maxReceiveCount: opt.dlqAfter + ''
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
|
-
if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay
|
|
82
|
+
if (opt.failDelay) params.Attributes.DelaySeconds = opt.failDelay + ''
|
|
83
83
|
if (opt.tags) params.tags = opt.tags
|
|
84
84
|
if (opt.fifo) params.Attributes.FifoQueue = 'true'
|
|
85
85
|
const cmd = new CreateQueueCommand(params)
|
|
@@ -19,8 +19,11 @@ export class JobExecutor {
|
|
|
19
19
|
this.opt = opt
|
|
20
20
|
this.jobs = []
|
|
21
21
|
this.jobsByMessageId = {}
|
|
22
|
+
this.jobsByQueue = new Map()
|
|
22
23
|
this.stats = {
|
|
23
24
|
activeJobs: 0,
|
|
25
|
+
waitingJobs: 0,
|
|
26
|
+
runningJobs: 0,
|
|
24
27
|
sqsCalls: 0,
|
|
25
28
|
timeoutsExtended: 0,
|
|
26
29
|
jobsSucceeded: 0,
|
|
@@ -46,6 +49,20 @@ export class JobExecutor {
|
|
|
46
49
|
return this.stats.activeJobs
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
runningJobCount () {
|
|
53
|
+
return this.stats.runningJobs
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the number of jobs running in a queue.
|
|
58
|
+
*/
|
|
59
|
+
runningJobCountForQueue (qname) {
|
|
60
|
+
const jobs = this.jobsByQueue.get(qname) || new Set()
|
|
61
|
+
let runningCount = 0
|
|
62
|
+
for (const job of jobs.values()) runningCount += job.status === 'running'
|
|
63
|
+
return runningCount
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
/**
|
|
50
67
|
* Changes message visibility on all running jobs using as few calls as possible.
|
|
51
68
|
*/
|
|
@@ -71,15 +88,12 @@ export class JobExecutor {
|
|
|
71
88
|
const jobsToDeleteByQrl = {}
|
|
72
89
|
const jobsToCleanup = new Set()
|
|
73
90
|
|
|
74
|
-
if (this.opt.verbose) {
|
|
75
|
-
console.error(chalk.blue('Stats: '), this.stats)
|
|
76
|
-
console.error(chalk.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
91
|
// Build list of jobs we need to deal with
|
|
92
|
+
const jobStatuses = {}
|
|
80
93
|
for (let i = 0; i < this.jobs.length; i++) {
|
|
81
94
|
const job = this.jobs[i]
|
|
82
95
|
const jobRunTime = Math.round((start - job.start) / 1000)
|
|
96
|
+
jobStatuses[job.status] = (jobStatuses[job.status] || 0) + 1
|
|
83
97
|
// debug('considering job', job)
|
|
84
98
|
if (job.status === 'complete') {
|
|
85
99
|
const jobsToDelete = jobsToDeleteByQrl[job.qrl] || []
|
|
@@ -88,7 +102,8 @@ export class JobExecutor {
|
|
|
88
102
|
jobsToDeleteByQrl[job.qrl] = jobsToDelete
|
|
89
103
|
} else if (job.status === 'failed') {
|
|
90
104
|
jobsToCleanup.add(job)
|
|
91
|
-
} else if (job.status
|
|
105
|
+
} else if (job.status !== 'deleting') {
|
|
106
|
+
// Any other job state gets visibility accounting
|
|
92
107
|
debug('processing', { job, jobRunTime })
|
|
93
108
|
if (jobRunTime >= job.extendAtSecond) {
|
|
94
109
|
// Add it to our organized list of jobs
|
|
@@ -106,7 +121,11 @@ export class JobExecutor {
|
|
|
106
121
|
}
|
|
107
122
|
}
|
|
108
123
|
}
|
|
109
|
-
|
|
124
|
+
|
|
125
|
+
if (this.opt.verbose) {
|
|
126
|
+
console.error(chalk.blue('Stats: '), { stats: this.stats, jobStatuses })
|
|
127
|
+
console.error(chalk.blue('Running: '), this.jobs.filter(j => j.status === 'processing').map(({ qname, message }) => ({ qname, payload: message.Body })))
|
|
128
|
+
}
|
|
110
129
|
|
|
111
130
|
// Extend in batches for each queue
|
|
112
131
|
for (const qrl in jobsToExtendByQrl) {
|
|
@@ -199,7 +218,9 @@ export class JobExecutor {
|
|
|
199
218
|
this.jobs = this.jobs.filter(job => {
|
|
200
219
|
if (job.status === 'deleting' || job.status === 'failed') {
|
|
201
220
|
debug('removed', job.message.MessageId)
|
|
221
|
+
// Accounting
|
|
202
222
|
delete this.jobsByMessageId[job.message.MessageId]
|
|
223
|
+
this.jobsByQueue.get(job.qname).delete(job)
|
|
203
224
|
return false
|
|
204
225
|
} else {
|
|
205
226
|
return true
|
|
@@ -207,20 +228,19 @@ export class JobExecutor {
|
|
|
207
228
|
})
|
|
208
229
|
}
|
|
209
230
|
|
|
210
|
-
|
|
211
|
-
if (this.shutdownRequested) throw new Error('jobExecutor is shutting down so cannot execute new job')
|
|
231
|
+
addJob (message, callback, qname, qrl) {
|
|
212
232
|
// Create job entry and track it
|
|
213
|
-
const
|
|
214
|
-
const visibilityTimeout = 60
|
|
233
|
+
const defaultVisibilityTimeout = 60
|
|
215
234
|
const job = {
|
|
216
|
-
status: '
|
|
235
|
+
status: 'waiting',
|
|
217
236
|
start: new Date(),
|
|
218
|
-
visibilityTimeout,
|
|
219
|
-
extendAtSecond:
|
|
237
|
+
visibilityTimeout: defaultVisibilityTimeout,
|
|
238
|
+
extendAtSecond: defaultVisibilityTimeout / 2,
|
|
220
239
|
payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
|
|
221
240
|
message,
|
|
222
241
|
callback,
|
|
223
242
|
qname,
|
|
243
|
+
prettyQname: qname.slice(this.opt.prefix.length),
|
|
224
244
|
qrl
|
|
225
245
|
}
|
|
226
246
|
|
|
@@ -237,29 +257,51 @@ export class JobExecutor {
|
|
|
237
257
|
throw e
|
|
238
258
|
}
|
|
239
259
|
|
|
240
|
-
//
|
|
260
|
+
// Accounting
|
|
241
261
|
this.jobs.push(job)
|
|
242
262
|
this.jobsByMessageId[job.message.MessageId] = job
|
|
263
|
+
|
|
264
|
+
// Track all jobs for each queue
|
|
265
|
+
if (!this.jobsByQueue.has(job.qname)) this.jobsByQueue.set(job.qname, new Set())
|
|
266
|
+
this.jobsByQueue.get(job.qname).add(job)
|
|
267
|
+
|
|
243
268
|
this.stats.activeJobs++
|
|
269
|
+
this.stats.waitingJobs++
|
|
244
270
|
if (this.opt.verbose) {
|
|
245
|
-
console.error(chalk.blue('
|
|
271
|
+
console.error(chalk.blue('Got message:'), job.prettyQname, chalk.blue('-->'), job.payload, job.message.MessageId)
|
|
246
272
|
} else if (!this.opt.disableLog) {
|
|
247
273
|
console.log(JSON.stringify({
|
|
248
|
-
event: '
|
|
274
|
+
event: 'MESSAGE_RECEIVED',
|
|
249
275
|
timestamp: new Date(),
|
|
250
|
-
|
|
276
|
+
queue: job.qname,
|
|
251
277
|
messageId: message.MessageId,
|
|
252
278
|
payload: job.payload
|
|
253
279
|
}))
|
|
254
280
|
}
|
|
281
|
+
return job
|
|
282
|
+
}
|
|
255
283
|
|
|
256
|
-
|
|
284
|
+
async runJob (job) {
|
|
257
285
|
try {
|
|
258
|
-
const queue = qname.slice(this.opt.prefix.length)
|
|
259
|
-
const result = await callback(queue, payload)
|
|
260
|
-
debug('executeJob callback finished', { payload, result })
|
|
261
286
|
if (this.opt.verbose) {
|
|
262
|
-
console.error(chalk.
|
|
287
|
+
console.error(chalk.blue('Running:'), job.prettyQname, chalk.blue('-->'), job.payload, job.message.MessageId)
|
|
288
|
+
} else if (!this.opt.disableLog) {
|
|
289
|
+
console.log(JSON.stringify({
|
|
290
|
+
event: 'MESSAGE_PROCESSING_START',
|
|
291
|
+
timestamp: new Date(),
|
|
292
|
+
queue: job.qname,
|
|
293
|
+
messageId: job.message.MessageId,
|
|
294
|
+
payload: job.payload
|
|
295
|
+
}))
|
|
296
|
+
}
|
|
297
|
+
job.status = 'running'
|
|
298
|
+
this.stats.runningJobs++
|
|
299
|
+
this.stats.waitingJobs--
|
|
300
|
+
const queue = job.qname.slice(this.opt.prefix.length)
|
|
301
|
+
const result = await job.callback(queue, job.payload)
|
|
302
|
+
debug('executeJob callback finished', { payload: job.payload, result })
|
|
303
|
+
if (this.opt.verbose) {
|
|
304
|
+
console.error(chalk.green('SUCCESS'), job.payload)
|
|
263
305
|
}
|
|
264
306
|
job.status = 'complete'
|
|
265
307
|
|
|
@@ -269,35 +311,57 @@ export class JobExecutor {
|
|
|
269
311
|
} else if (!this.opt.disableLog) {
|
|
270
312
|
console.log(JSON.stringify({
|
|
271
313
|
event: 'MESSAGE_PROCESSING_COMPLETE',
|
|
314
|
+
queue: job.qname,
|
|
272
315
|
timestamp: new Date(),
|
|
273
|
-
messageId: message.MessageId,
|
|
274
|
-
payload
|
|
316
|
+
messageId: job.message.MessageId,
|
|
317
|
+
payload: job.payload
|
|
275
318
|
}))
|
|
276
319
|
}
|
|
277
320
|
this.stats.jobsSucceeded++
|
|
278
321
|
} catch (err) {
|
|
279
|
-
|
|
280
|
-
|
|
322
|
+
job.status = 'failed'
|
|
323
|
+
this.stats.jobsFailed++
|
|
281
324
|
// Fail path for job execution
|
|
282
325
|
if (this.opt.verbose) {
|
|
283
|
-
console.error(chalk.red('FAILED'),
|
|
326
|
+
console.error(chalk.red('FAILED'), job.payload)
|
|
284
327
|
console.error(chalk.blue(' error : ') + err)
|
|
285
328
|
} else if (!this.opt.disableLog) {
|
|
286
329
|
// Production error logging
|
|
287
330
|
console.log(JSON.stringify({
|
|
288
331
|
event: 'MESSAGE_PROCESSING_FAILED',
|
|
289
332
|
reason: 'exception thrown',
|
|
290
|
-
|
|
333
|
+
queue: job.qname,
|
|
291
334
|
timestamp: new Date(),
|
|
292
|
-
messageId: message.MessageId,
|
|
293
|
-
payload,
|
|
335
|
+
messageId: job.message.MessageId,
|
|
336
|
+
payload: job.payload,
|
|
294
337
|
errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
|
|
295
338
|
err
|
|
296
339
|
}))
|
|
297
340
|
}
|
|
298
|
-
job.status = 'failed'
|
|
299
|
-
this.stats.jobsFailed++
|
|
300
341
|
}
|
|
301
342
|
this.stats.activeJobs--
|
|
343
|
+
this.stats.runningJobs--
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async executeJobs (messages, callback, qname, qrl) {
|
|
347
|
+
if (this.shutdownRequested) throw new Error('jobExecutor is shutting down so cannot execute new jobs')
|
|
348
|
+
|
|
349
|
+
// Begin tracking jobs
|
|
350
|
+
const jobs = messages.map(message => this.addJob(message, callback, qname, qrl))
|
|
351
|
+
const isFifo = qrl.endsWith('.fifo')
|
|
352
|
+
|
|
353
|
+
// console.log(jobs)
|
|
354
|
+
|
|
355
|
+
// Begin executing
|
|
356
|
+
for (const [job, i] of jobs.map((job, i) => [job, i])) {
|
|
357
|
+
// Figure out if the next job needs to happen in serial, otherwise we can parallel execute
|
|
358
|
+
const nextJob = jobs[i + 1]
|
|
359
|
+
const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.GroupId === nextJob.message?.Attributes?.GroupId
|
|
360
|
+
|
|
361
|
+
// console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
|
|
362
|
+
// Execute serial or parallel
|
|
363
|
+
if (nextJobIsSerial) await this.runJob(job)
|
|
364
|
+
else this.runJob(job)
|
|
365
|
+
}
|
|
302
366
|
}
|
|
303
367
|
}
|
|
@@ -35,7 +35,7 @@ export class QueueManager {
|
|
|
35
35
|
const now = new Date()
|
|
36
36
|
const secondsElapsed = lastCheck - now
|
|
37
37
|
const minWait = 10
|
|
38
|
-
const maxWait =
|
|
38
|
+
const maxWait = 120
|
|
39
39
|
const baseSeconds = numEmptyReceives ** 2 * 20
|
|
40
40
|
const jitterSeconds = Math.round((Math.random() - 0.5) * baseSeconds)
|
|
41
41
|
const newSecondsToWait = Math.max(minWait, Math.min(maxWait, baseSeconds + jitterSeconds))
|