qdone 2.0.51-alpha → 2.0.53-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 +2 -7
- package/commonjs/src/consumer.js +1 -1
- package/commonjs/src/defaults.js +13 -3
- package/commonjs/src/idleQueues.js +20 -42
- package/commonjs/src/monitor.js +3 -1
- package/commonjs/src/qrlCache.js +5 -2
- package/commonjs/src/scheduler/systemMonitor.js +55 -6
- package/package.json +1 -1
- package/src/cli.js +1 -3
- package/src/consumer.js +1 -1
- package/src/defaults.js +11 -2
- package/src/idleQueues.js +18 -40
- package/src/monitor.js +2 -1
- package/src/qrlCache.js +5 -2
- package/src/scheduler/systemMonitor.js +56 -5
- package/npm-shrinkwrap.json +0 -16000
package/README.md
CHANGED
|
@@ -561,13 +561,8 @@ If a queue name ends with the * (wildcard) character, worker will listen on all
|
|
|
561
561
|
|
|
562
562
|
-o, --idle-for number Minutes of inactivity after which a queue is considered
|
|
563
563
|
idle. [default: 60]
|
|
564
|
-
--delete Delete the queue if it is idle
|
|
565
|
-
|
|
566
|
-
--unpair Treat queues and their fail queues as independent. By default
|
|
567
|
-
they are treated as a unit.
|
|
568
|
-
--include-failed When using '*' do not ignore fail queues. This option only
|
|
569
|
-
applies if you use --unpair. Otherwise, queues and fail queues
|
|
570
|
-
are treated as a unit.
|
|
564
|
+
--delete Delete the queue if it is idle along with its corresponding
|
|
565
|
+
fail queue and dlq.
|
|
571
566
|
--prefix string Prefix to place at the front of each SQS queue name [default: qdone_]
|
|
572
567
|
--fail-suffix string Suffix to append to each queue to generate fail queue name [default: _failed]
|
|
573
568
|
--region string AWS region for Queues [default: us-east-1]
|
package/commonjs/src/consumer.js
CHANGED
|
@@ -130,7 +130,7 @@ async function processMessages(queues, callback, options) {
|
|
|
130
130
|
const remainingMemory = Math.max(0, freeMemory - freememThreshold);
|
|
131
131
|
const freememFactor = Math.min(1, Math.max(0, remainingMemory / memoryThreshold));
|
|
132
132
|
// Load
|
|
133
|
-
const oneMinuteLoad =
|
|
133
|
+
const oneMinuteLoad = systemMonitor.getLoad();
|
|
134
134
|
const loadPerCore = oneMinuteLoad / cores;
|
|
135
135
|
const loadFactor = 1 - Math.min(1, Math.max(0, loadPerCore / 3));
|
|
136
136
|
const overallFactor = Math.min(latencyFactor, freememFactor, loadFactor);
|
package/commonjs/src/defaults.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupVerbose = exports.setupAWS = exports.getOptionsWithDefaults = exports.validateMessageOptions = exports.validateInteger = exports.defaults = void 0;
|
|
3
|
+
exports.setupVerbose = exports.setupAWS = exports.getOptionsWithDefaults = exports.validateMessageOptions = exports.validateQueueName = exports.validateInteger = exports.defaults = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Default options for qdone. Accepts a command line options object and
|
|
6
6
|
* returns nicely-named options.
|
|
@@ -46,7 +46,6 @@ exports.defaults = Object.freeze({
|
|
|
46
46
|
// Idle Queues
|
|
47
47
|
idleFor: 60,
|
|
48
48
|
delete: false,
|
|
49
|
-
unpair: false,
|
|
50
49
|
// Check
|
|
51
50
|
create: false,
|
|
52
51
|
overwrite: false
|
|
@@ -58,6 +57,14 @@ function validateInteger(opt, name) {
|
|
|
58
57
|
return parsed;
|
|
59
58
|
}
|
|
60
59
|
exports.validateInteger = validateInteger;
|
|
60
|
+
function validateQueueName(opt, name) {
|
|
61
|
+
if (typeof name !== 'string')
|
|
62
|
+
throw new Error(`${name} must be a string.`);
|
|
63
|
+
if (!name.match(/^[a-z0-9-_]+$/i))
|
|
64
|
+
throw new Error(`${name} can contain only numbers, letters, hypens and underscores.`);
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
exports.validateQueueName = validateQueueName;
|
|
61
68
|
function validateMessageOptions(messageOptions) {
|
|
62
69
|
const validKeys = ['deduplicationId', 'groupId'];
|
|
63
70
|
if (typeof messageOptions === 'object' &&
|
|
@@ -128,7 +135,6 @@ function getOptionsWithDefaults(options) {
|
|
|
128
135
|
// Idle Queues
|
|
129
136
|
idleFor: options.idleFor || options['idle-for'] || process.env.QDONE_IDLE_FOR || exports.defaults.idleFor,
|
|
130
137
|
delete: options.delete || process.env.QDONE_DELETE === 'true' || exports.defaults.delete,
|
|
131
|
-
unpair: options.unpair || process.env.QDONE_UNPAIR === 'true' || exports.defaults.unpair,
|
|
132
138
|
// Check
|
|
133
139
|
create: options.create || process.env.QDONE_CREATE === 'true' || exports.defaults.create,
|
|
134
140
|
overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || exports.defaults.overwrite
|
|
@@ -148,6 +154,10 @@ function getOptionsWithDefaults(options) {
|
|
|
148
154
|
opt.maxConcurrentJobs = validateInteger(opt, 'maxConcurrentJobs');
|
|
149
155
|
opt.maxMemoryPercent = validateInteger(opt, 'maxMemoryPercent');
|
|
150
156
|
opt.idleFor = validateInteger(opt, 'idleFor');
|
|
157
|
+
validateQueueName(opt, 'region');
|
|
158
|
+
validateQueueName(opt, 'prefix');
|
|
159
|
+
validateQueueName(opt, 'failSuffix');
|
|
160
|
+
validateQueueName(opt, 'dlqSuffix');
|
|
151
161
|
// Validate dedup args
|
|
152
162
|
if (opt.externalDedup && !opt.cacheUri)
|
|
153
163
|
throw new Error('--external-dedup requires the --cache-uri argument');
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.idleQueues = exports.
|
|
6
|
+
exports.idleQueues = exports.stripSuffixes = exports.processQueueSet = exports.deleteQueue = exports.checkIdle = exports.getMetric = exports.cheapIdleCheck = exports._cheapIdleCheck = exports.attributeNames = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Implementation of checks and caching of checks to determine if queues are idle.
|
|
9
9
|
*/
|
|
@@ -187,35 +187,6 @@ async function deleteQueue(qname, qrl, opt) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
exports.deleteQueue = deleteQueue;
|
|
190
|
-
/**
|
|
191
|
-
* Processes a single queue, checking for idle, deleting if applicable.
|
|
192
|
-
*/
|
|
193
|
-
async function processQueue(qname, qrl, opt) {
|
|
194
|
-
const result = await checkIdle(qname, qrl, opt);
|
|
195
|
-
debug(qname, result);
|
|
196
|
-
// Queue is active
|
|
197
|
-
if (!result.idle) {
|
|
198
|
-
// Notify and return
|
|
199
|
-
if (opt.verbose)
|
|
200
|
-
console.error(chalk_1.default.blue('Queue ') + qname.slice(opt.prefix.length) + chalk_1.default.blue(' has been ') + 'active' + chalk_1.default.blue(' in the last ') + opt.idleFor + chalk_1.default.blue(' minutes.'));
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
// Queue is idle
|
|
204
|
-
if (opt.verbose)
|
|
205
|
-
console.error(chalk_1.default.blue('Queue ') + qname.slice(opt.prefix.length) + chalk_1.default.blue(' has been ') + 'idle' + chalk_1.default.blue(' for the last ') + opt.idleFor + chalk_1.default.blue(' minutes.'));
|
|
206
|
-
if (opt.delete) {
|
|
207
|
-
const deleteResult = await deleteQueue(qname, qrl, opt);
|
|
208
|
-
const resultIncludingDelete = Object.assign(result, {
|
|
209
|
-
deleted: deleteResult.deleted,
|
|
210
|
-
apiCalls: {
|
|
211
|
-
SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
|
|
212
|
-
CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
return resultIncludingDelete;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
exports.processQueue = processQueue;
|
|
219
190
|
/**
|
|
220
191
|
* Processes a queue and its fail and delete queue, treating them as a unit.
|
|
221
192
|
*/
|
|
@@ -311,6 +282,14 @@ async function processQueueSet(qname, qrl, opt) {
|
|
|
311
282
|
}
|
|
312
283
|
exports.processQueueSet = processQueueSet;
|
|
313
284
|
//
|
|
285
|
+
// Strips failed and dlq suffix from a queue name or URL
|
|
286
|
+
//
|
|
287
|
+
function stripSuffixes(queueName, opt) {
|
|
288
|
+
const suffixFinder = new RegExp(`(${opt.dlqSuffix}|${opt.failSuffix}){1}(|${qrlCache_js_1.fifoSuffix})$`);
|
|
289
|
+
return queueName.replace(suffixFinder, '$2');
|
|
290
|
+
}
|
|
291
|
+
exports.stripSuffixes = stripSuffixes;
|
|
292
|
+
//
|
|
314
293
|
// Resolve queues for listening loop listen
|
|
315
294
|
//
|
|
316
295
|
async function idleQueues(queues, options) {
|
|
@@ -324,17 +303,18 @@ async function idleQueues(queues, options) {
|
|
|
324
303
|
console.error(chalk_1.default.blue(' done'));
|
|
325
304
|
console.error();
|
|
326
305
|
}
|
|
327
|
-
// Filter out
|
|
306
|
+
// Filter out failed and dead queues, but if we have an orphaned fail or
|
|
307
|
+
// dead queue, keep the original parent queue name so that orphans can be
|
|
308
|
+
// deleted.
|
|
309
|
+
const queueNames = new Set();
|
|
328
310
|
const filteredEntries = entries.filter(entry => {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const isFifoDead = entry.qname.endsWith(sufFifoDead);
|
|
337
|
-
return opt.includeFailed ? true : (!isFail && !isFifoFail && !isDead && !isFifoDead);
|
|
311
|
+
const stripped = stripSuffixes(entry.qname, opt);
|
|
312
|
+
if (queueNames.has(stripped))
|
|
313
|
+
return false;
|
|
314
|
+
queueNames.add(stripped);
|
|
315
|
+
entry.qname = stripped;
|
|
316
|
+
entry.qrl = stripSuffixes(entry.qrl, opt);
|
|
317
|
+
return true;
|
|
338
318
|
});
|
|
339
319
|
// But only if we have queues to remove
|
|
340
320
|
if (filteredEntries.length) {
|
|
@@ -344,8 +324,6 @@ async function idleQueues(queues, options) {
|
|
|
344
324
|
console.error();
|
|
345
325
|
}
|
|
346
326
|
// Check each queue in parallel
|
|
347
|
-
if (opt.unpair)
|
|
348
|
-
return Promise.all(filteredEntries.map(e => processQueue(e.qname, e.qrl, opt)));
|
|
349
327
|
return Promise.all(filteredEntries.map(e => processQueueSet(e.qname, e.qrl, opt)));
|
|
350
328
|
}
|
|
351
329
|
// Otherwise, let caller know
|
package/commonjs/src/monitor.js
CHANGED
|
@@ -17,9 +17,11 @@ const debug = (0, debug_1.default)('qdone:monitor');
|
|
|
17
17
|
* Splits a queue name with a single wildcard into prefix and suffix regex.
|
|
18
18
|
*/
|
|
19
19
|
async function monitor(queue, save, options) {
|
|
20
|
+
if (queue.endsWith('.fifo'))
|
|
21
|
+
options.fifo = true;
|
|
20
22
|
const opt = (0, defaults_js_1.getOptionsWithDefaults)(options);
|
|
21
23
|
const queueName = (0, qrlCache_js_1.normalizeQueueName)(queue, opt);
|
|
22
|
-
debug({ opt, queueName });
|
|
24
|
+
debug({ options, opt, queue, queueName });
|
|
23
25
|
const data = await getAggregateData(queueName);
|
|
24
26
|
console.log(data);
|
|
25
27
|
if (save) {
|
package/commonjs/src/qrlCache.js
CHANGED
|
@@ -70,8 +70,10 @@ async function qrlCacheGet(qname) {
|
|
|
70
70
|
// debug({ cmd })
|
|
71
71
|
const result = await client.send(cmd);
|
|
72
72
|
// debug('result', result)
|
|
73
|
-
if (!result)
|
|
73
|
+
if (!result) {
|
|
74
|
+
qrlCacheInvalidate(qname);
|
|
74
75
|
throw new client_sqs_1.QueueDoesNotExist(qname);
|
|
76
|
+
}
|
|
75
77
|
const { QueueUrl: qrl } = result;
|
|
76
78
|
// debug('getQueueUrl returned', data)
|
|
77
79
|
qcache.set(qname, qrl);
|
|
@@ -84,7 +86,8 @@ exports.qrlCacheGet = qrlCacheGet;
|
|
|
84
86
|
// Immediately updates the cache
|
|
85
87
|
//
|
|
86
88
|
function qrlCacheSet(qname, qrl) {
|
|
87
|
-
|
|
89
|
+
if (qrl)
|
|
90
|
+
qcache.set(qname, qrl);
|
|
88
91
|
// debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
89
92
|
}
|
|
90
93
|
exports.qrlCacheSet = qrlCacheSet;
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
* Component to track event loop latency, which can be used as a metric for
|
|
4
4
|
* backpressure.
|
|
5
5
|
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
6
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
10
|
exports.SystemMonitor = void 0;
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
8
12
|
class SystemMonitor {
|
|
9
13
|
constructor(reportCallback, reportSeconds = 1) {
|
|
10
14
|
this.reportCallback = reportCallback || console.log;
|
|
11
15
|
this.reportSeconds = reportSeconds;
|
|
12
|
-
this.
|
|
16
|
+
this.latencies = [];
|
|
17
|
+
this.oneMinuteLoad = os_1.default.loadavg()[0];
|
|
18
|
+
this.instantaneousLoad = this.oneMinuteLoad;
|
|
13
19
|
this.measure();
|
|
14
20
|
this.reportLatency();
|
|
15
21
|
}
|
|
@@ -17,15 +23,19 @@ class SystemMonitor {
|
|
|
17
23
|
clearTimeout(this.measureTimeout);
|
|
18
24
|
const start = new Date();
|
|
19
25
|
this.measureTimeout = setTimeout(() => {
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
if (this.measurements.length > 1000)
|
|
23
|
-
this.measurements.shift();
|
|
26
|
+
this.measureLatency(start);
|
|
27
|
+
this.measureLoad();
|
|
24
28
|
this.measure();
|
|
25
29
|
});
|
|
26
30
|
}
|
|
31
|
+
measureLatency(start) {
|
|
32
|
+
const latency = new Date() - start;
|
|
33
|
+
this.latencies.push(latency);
|
|
34
|
+
if (this.latencies.length > 1000)
|
|
35
|
+
this.latencies.shift();
|
|
36
|
+
}
|
|
27
37
|
getLatency() {
|
|
28
|
-
return this.
|
|
38
|
+
return this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0;
|
|
29
39
|
}
|
|
30
40
|
reportLatency() {
|
|
31
41
|
clearTimeout(this.reportTimeout);
|
|
@@ -37,6 +47,45 @@ class SystemMonitor {
|
|
|
37
47
|
this.reportLatency();
|
|
38
48
|
}, this.reportSeconds * 1000);
|
|
39
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Measures load over the last five seconds instead of being averaged over one
|
|
52
|
+
* minute. This lets the scheduler respond much faster to dips in load.
|
|
53
|
+
*
|
|
54
|
+
* Theory:
|
|
55
|
+
*
|
|
56
|
+
* The Linux kernel calculates the moving average something like:
|
|
57
|
+
* A_1 = A_0 * e + A_now (1 - e)
|
|
58
|
+
* Where:
|
|
59
|
+
* - A_now is the number of processes active/waiting
|
|
60
|
+
* - A_1 is the new one-minute load average after the measurement of A_now
|
|
61
|
+
* - A_0 is the previous one-minute average
|
|
62
|
+
* - e is 1884/2048.
|
|
63
|
+
*
|
|
64
|
+
* Solving this for A_now, which we want to access, we get:
|
|
65
|
+
* A_now = (A_1 - A_0 * e) / (1 - e)
|
|
66
|
+
*
|
|
67
|
+
* We use this formula below to extract A_now when we detect a change in A_1.
|
|
68
|
+
*
|
|
69
|
+
* Note: this code assums that we are observing the average often enough to
|
|
70
|
+
* detect each change. So you have to call it at least every 5 seconds. 1
|
|
71
|
+
* second is better to reduce latency of detecting the change.
|
|
72
|
+
*/
|
|
73
|
+
measureLoad() {
|
|
74
|
+
const [newLoad,] = os_1.default.loadavg();
|
|
75
|
+
const previousLoad = this.oneMinuteLoad;
|
|
76
|
+
if (previousLoad !== newLoad) {
|
|
77
|
+
const e = 1884 / 2048; // see include/linux/sched/loadavg.h
|
|
78
|
+
const active = (newLoad - previousLoad * e) / (1 - e);
|
|
79
|
+
// We take the min here so that spikes up in load are averaged out. We
|
|
80
|
+
// care about detecting spikes downward so we can allow more jobs to run.
|
|
81
|
+
this.instantaneousLoad = Math.min(active, newLoad);
|
|
82
|
+
this.oneMinuteLoad = newLoad;
|
|
83
|
+
console.log({ newLoad, previousLoad, active, instantaneousLoad: this.instantaneousLoad, oneMinuteLoad: this.oneMinuteLoad });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
getLoad() {
|
|
87
|
+
return this.instantaneousLoad;
|
|
88
|
+
}
|
|
40
89
|
shutdown() {
|
|
41
90
|
clearTimeout(this.measureTimeout);
|
|
42
91
|
clearTimeout(this.reportTimeout);
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -440,9 +440,7 @@ export async function worker (argv, testHook) {
|
|
|
440
440
|
export async function idleQueues (argv, testHook) {
|
|
441
441
|
const optionDefinitions = [
|
|
442
442
|
{ name: 'idle-for', alias: 'o', type: Number, defaultValue: defaults.idleFor, description: `Minutes of inactivity after which a queue is considered idle. [default: ${defaults.idleFor}]` },
|
|
443
|
-
{ name: 'delete', type: Boolean, description: 'Delete the queue if it is idle
|
|
444
|
-
{ name: 'unpair', type: Boolean, description: 'Treat queues and their fail queues as independent. By default they are treated as a unit.' },
|
|
445
|
-
{ 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.' }
|
|
443
|
+
{ name: 'delete', type: Boolean, description: 'Delete the queue if it is idle along with its corresponding fail queue and dlq.' }
|
|
446
444
|
].concat(globalOptionDefinitions)
|
|
447
445
|
|
|
448
446
|
const usageSections = [
|
package/src/consumer.js
CHANGED
|
@@ -136,7 +136,7 @@ export async function processMessages (queues, callback, options) {
|
|
|
136
136
|
const freememFactor = Math.min(1, Math.max(0, remainingMemory / memoryThreshold))
|
|
137
137
|
|
|
138
138
|
// Load
|
|
139
|
-
const oneMinuteLoad =
|
|
139
|
+
const oneMinuteLoad = systemMonitor.getLoad()
|
|
140
140
|
const loadPerCore = oneMinuteLoad / cores
|
|
141
141
|
const loadFactor = 1 - Math.min(1, Math.max(0, loadPerCore / 3))
|
|
142
142
|
|
package/src/defaults.js
CHANGED
|
@@ -47,7 +47,6 @@ export const defaults = Object.freeze({
|
|
|
47
47
|
// Idle Queues
|
|
48
48
|
idleFor: 60,
|
|
49
49
|
delete: false,
|
|
50
|
-
unpair: false,
|
|
51
50
|
|
|
52
51
|
// Check
|
|
53
52
|
create: false,
|
|
@@ -60,6 +59,12 @@ export function validateInteger (opt, name) {
|
|
|
60
59
|
return parsed
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
export function validateQueueName (opt, name) {
|
|
63
|
+
if (typeof name !== 'string') throw new Error(`${name} must be a string.`)
|
|
64
|
+
if (!name.match(/^[a-z0-9-_]+$/i)) throw new Error(`${name} can contain only numbers, letters, hypens and underscores.`)
|
|
65
|
+
return name
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
export function validateMessageOptions (messageOptions) {
|
|
64
69
|
const validKeys = ['deduplicationId', 'groupId']
|
|
65
70
|
if (typeof messageOptions === 'object' &&
|
|
@@ -134,7 +139,6 @@ export function getOptionsWithDefaults (options) {
|
|
|
134
139
|
// Idle Queues
|
|
135
140
|
idleFor: options.idleFor || options['idle-for'] || process.env.QDONE_IDLE_FOR || defaults.idleFor,
|
|
136
141
|
delete: options.delete || process.env.QDONE_DELETE === 'true' || defaults.delete,
|
|
137
|
-
unpair: options.unpair || process.env.QDONE_UNPAIR === 'true' || defaults.unpair,
|
|
138
142
|
|
|
139
143
|
// Check
|
|
140
144
|
create: options.create || process.env.QDONE_CREATE === 'true' || defaults.create,
|
|
@@ -158,6 +162,11 @@ export function getOptionsWithDefaults (options) {
|
|
|
158
162
|
opt.maxMemoryPercent = validateInteger(opt, 'maxMemoryPercent')
|
|
159
163
|
opt.idleFor = validateInteger(opt, 'idleFor')
|
|
160
164
|
|
|
165
|
+
validateQueueName(opt, 'region')
|
|
166
|
+
validateQueueName(opt, 'prefix')
|
|
167
|
+
validateQueueName(opt, 'failSuffix')
|
|
168
|
+
validateQueueName(opt, 'dlqSuffix')
|
|
169
|
+
|
|
161
170
|
// Validate dedup args
|
|
162
171
|
if (opt.externalDedup && !opt.cacheUri) throw new Error('--external-dedup requires the --cache-uri argument')
|
|
163
172
|
if (opt.externalDedup && (opt.dedupPeriod < 1)) throw new Error('--external-dedup of redis requires a --dedup-period > 1 second')
|
package/src/idleQueues.js
CHANGED
|
@@ -186,35 +186,6 @@ export async function deleteQueue (qname, qrl, opt) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
/**
|
|
190
|
-
* Processes a single queue, checking for idle, deleting if applicable.
|
|
191
|
-
*/
|
|
192
|
-
export async function processQueue (qname, qrl, opt) {
|
|
193
|
-
const result = await checkIdle(qname, qrl, opt)
|
|
194
|
-
debug(qname, result)
|
|
195
|
-
|
|
196
|
-
// Queue is active
|
|
197
|
-
if (!result.idle) {
|
|
198
|
-
// Notify and return
|
|
199
|
-
if (opt.verbose) console.error(chalk.blue('Queue ') + qname.slice(opt.prefix.length) + chalk.blue(' has been ') + 'active' + chalk.blue(' in the last ') + opt.idleFor + chalk.blue(' minutes.'))
|
|
200
|
-
return result
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Queue is idle
|
|
204
|
-
if (opt.verbose) console.error(chalk.blue('Queue ') + qname.slice(opt.prefix.length) + chalk.blue(' has been ') + 'idle' + chalk.blue(' for the last ') + opt.idleFor + chalk.blue(' minutes.'))
|
|
205
|
-
if (opt.delete) {
|
|
206
|
-
const deleteResult = await deleteQueue(qname, qrl, opt)
|
|
207
|
-
const resultIncludingDelete = Object.assign(result, {
|
|
208
|
-
deleted: deleteResult.deleted,
|
|
209
|
-
apiCalls: {
|
|
210
|
-
SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
|
|
211
|
-
CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
return resultIncludingDelete
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
189
|
/**
|
|
219
190
|
* Processes a queue and its fail and delete queue, treating them as a unit.
|
|
220
191
|
*/
|
|
@@ -308,6 +279,14 @@ export async function processQueueSet (qname, qrl, opt) {
|
|
|
308
279
|
return result
|
|
309
280
|
}
|
|
310
281
|
|
|
282
|
+
//
|
|
283
|
+
// Strips failed and dlq suffix from a queue name or URL
|
|
284
|
+
//
|
|
285
|
+
export function stripSuffixes (queueName, opt) {
|
|
286
|
+
const suffixFinder = new RegExp(`(${opt.dlqSuffix}|${opt.failSuffix}){1}(|${fifoSuffix})$`)
|
|
287
|
+
return queueName.replace(suffixFinder, '$2')
|
|
288
|
+
}
|
|
289
|
+
|
|
311
290
|
//
|
|
312
291
|
// Resolve queues for listening loop listen
|
|
313
292
|
//
|
|
@@ -322,17 +301,17 @@ export async function idleQueues (queues, options) {
|
|
|
322
301
|
console.error()
|
|
323
302
|
}
|
|
324
303
|
|
|
325
|
-
// Filter out
|
|
304
|
+
// Filter out failed and dead queues, but if we have an orphaned fail or
|
|
305
|
+
// dead queue, keep the original parent queue name so that orphans can be
|
|
306
|
+
// deleted.
|
|
307
|
+
const queueNames = new Set()
|
|
326
308
|
const filteredEntries = entries.filter(entry => {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const isDead = entry.qname.endsWith(sufDead)
|
|
334
|
-
const isFifoDead = entry.qname.endsWith(sufFifoDead)
|
|
335
|
-
return opt.includeFailed ? true : (!isFail && !isFifoFail && !isDead && !isFifoDead)
|
|
309
|
+
const stripped = stripSuffixes(entry.qname, opt)
|
|
310
|
+
if (queueNames.has(stripped)) return false
|
|
311
|
+
queueNames.add(stripped)
|
|
312
|
+
entry.qname = stripped
|
|
313
|
+
entry.qrl = stripSuffixes(entry.qrl, opt)
|
|
314
|
+
return true
|
|
336
315
|
})
|
|
337
316
|
|
|
338
317
|
// But only if we have queues to remove
|
|
@@ -345,7 +324,6 @@ export async function idleQueues (queues, options) {
|
|
|
345
324
|
console.error()
|
|
346
325
|
}
|
|
347
326
|
// Check each queue in parallel
|
|
348
|
-
if (opt.unpair) return Promise.all(filteredEntries.map(e => processQueue(e.qname, e.qrl, opt)))
|
|
349
327
|
return Promise.all(filteredEntries.map(e => processQueueSet(e.qname, e.qrl, opt)))
|
|
350
328
|
}
|
|
351
329
|
|
package/src/monitor.js
CHANGED
|
@@ -13,9 +13,10 @@ const debug = Debug('qdone:monitor')
|
|
|
13
13
|
* Splits a queue name with a single wildcard into prefix and suffix regex.
|
|
14
14
|
*/
|
|
15
15
|
export async function monitor (queue, save, options) {
|
|
16
|
+
if (queue.endsWith('.fifo')) options.fifo = true
|
|
16
17
|
const opt = getOptionsWithDefaults(options)
|
|
17
18
|
const queueName = normalizeQueueName(queue, opt)
|
|
18
|
-
debug({ opt, queueName })
|
|
19
|
+
debug({ options, opt, queue, queueName })
|
|
19
20
|
const data = await getAggregateData(queueName)
|
|
20
21
|
console.log(data)
|
|
21
22
|
if (save) {
|
package/src/qrlCache.js
CHANGED
|
@@ -66,7 +66,10 @@ export async function qrlCacheGet (qname) {
|
|
|
66
66
|
// debug({ cmd })
|
|
67
67
|
const result = await client.send(cmd)
|
|
68
68
|
// debug('result', result)
|
|
69
|
-
if (!result)
|
|
69
|
+
if (!result) {
|
|
70
|
+
qrlCacheInvalidate(qname)
|
|
71
|
+
throw new QueueDoesNotExist(qname)
|
|
72
|
+
}
|
|
70
73
|
const { QueueUrl: qrl } = result
|
|
71
74
|
// debug('getQueueUrl returned', data)
|
|
72
75
|
qcache.set(qname, qrl)
|
|
@@ -79,7 +82,7 @@ export async function qrlCacheGet (qname) {
|
|
|
79
82
|
// Immediately updates the cache
|
|
80
83
|
//
|
|
81
84
|
export function qrlCacheSet (qname, qrl) {
|
|
82
|
-
qcache.set(qname, qrl)
|
|
85
|
+
if (qrl) qcache.set(qname, qrl)
|
|
83
86
|
// debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
* backpressure.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import os from 'os'
|
|
7
|
+
|
|
6
8
|
export class SystemMonitor {
|
|
7
9
|
constructor (reportCallback, reportSeconds = 1) {
|
|
8
10
|
this.reportCallback = reportCallback || console.log
|
|
9
11
|
this.reportSeconds = reportSeconds
|
|
10
|
-
this.
|
|
12
|
+
this.latencies = []
|
|
13
|
+
this.oneMinuteLoad = os.loadavg()[0]
|
|
14
|
+
this.instantaneousLoad = this.oneMinuteLoad
|
|
11
15
|
this.measure()
|
|
12
16
|
this.reportLatency()
|
|
13
17
|
}
|
|
@@ -16,15 +20,20 @@ export class SystemMonitor {
|
|
|
16
20
|
clearTimeout(this.measureTimeout)
|
|
17
21
|
const start = new Date()
|
|
18
22
|
this.measureTimeout = setTimeout(() => {
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
if (this.measurements.length > 1000) this.measurements.shift()
|
|
23
|
+
this.measureLatency(start)
|
|
24
|
+
this.measureLoad()
|
|
22
25
|
this.measure()
|
|
23
26
|
})
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
measureLatency (start) {
|
|
30
|
+
const latency = new Date() - start
|
|
31
|
+
this.latencies.push(latency)
|
|
32
|
+
if (this.latencies.length > 1000) this.latencies.shift()
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
getLatency () {
|
|
27
|
-
return this.
|
|
36
|
+
return this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
reportLatency () {
|
|
@@ -37,6 +46,48 @@ export class SystemMonitor {
|
|
|
37
46
|
}, this.reportSeconds * 1000)
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Measures load over the last five seconds instead of being averaged over one
|
|
51
|
+
* minute. This lets the scheduler respond much faster to dips in load.
|
|
52
|
+
*
|
|
53
|
+
* Theory:
|
|
54
|
+
*
|
|
55
|
+
* The Linux kernel calculates the moving average something like:
|
|
56
|
+
* A_1 = A_0 * e + A_now (1 - e)
|
|
57
|
+
* Where:
|
|
58
|
+
* - A_now is the number of processes active/waiting
|
|
59
|
+
* - A_1 is the new one-minute load average after the measurement of A_now
|
|
60
|
+
* - A_0 is the previous one-minute average
|
|
61
|
+
* - e is 1884/2048.
|
|
62
|
+
*
|
|
63
|
+
* Solving this for A_now, which we want to access, we get:
|
|
64
|
+
* A_now = (A_1 - A_0 * e) / (1 - e)
|
|
65
|
+
*
|
|
66
|
+
* We use this formula below to extract A_now when we detect a change in A_1.
|
|
67
|
+
*
|
|
68
|
+
* Note: this code assums that we are observing the average often enough to
|
|
69
|
+
* detect each change. So you have to call it at least every 5 seconds. 1
|
|
70
|
+
* second is better to reduce latency of detecting the change.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
measureLoad () {
|
|
74
|
+
const [newLoad, ] = os.loadavg()
|
|
75
|
+
const previousLoad = this.oneMinuteLoad
|
|
76
|
+
if (previousLoad !== newLoad) {
|
|
77
|
+
const e = 1884 / 2048 // see include/linux/sched/loadavg.h
|
|
78
|
+
const active = (newLoad - previousLoad * e) / (1 - e)
|
|
79
|
+
// We take the min here so that spikes up in load are averaged out. We
|
|
80
|
+
// care about detecting spikes downward so we can allow more jobs to run.
|
|
81
|
+
this.instantaneousLoad = Math.min(active, newLoad)
|
|
82
|
+
this.oneMinuteLoad = newLoad
|
|
83
|
+
console.log({ newLoad, previousLoad, active, instantaneousLoad: this.instantaneousLoad, oneMinuteLoad: this.oneMinuteLoad })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getLoad() {
|
|
88
|
+
return this.instantaneousLoad
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
shutdown () {
|
|
41
92
|
clearTimeout(this.measureTimeout)
|
|
42
93
|
clearTimeout(this.reportTimeout)
|