qdone 2.1.0 → 2.1.1
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/LICENSE +2 -2
- package/README.md +1 -4
- package/commonjs/index.js +11 -0
- package/commonjs/src/cache.js +72 -0
- package/commonjs/src/cloudWatch.js +102 -0
- package/commonjs/src/consumer.js +173 -0
- package/commonjs/src/dedup.js +266 -0
- package/commonjs/src/defaults.js +182 -0
- package/commonjs/src/enqueue.js +521 -0
- package/commonjs/src/exponentialBackoff.js +101 -0
- package/commonjs/src/idleQueues.js +333 -0
- package/commonjs/src/monitor.js +80 -0
- package/commonjs/src/qrlCache.js +172 -0
- package/commonjs/src/scheduler/jobExecutor.js +383 -0
- package/commonjs/src/scheduler/queueManager.js +161 -0
- package/commonjs/src/scheduler/systemMonitor.js +94 -0
- package/commonjs/src/sqs.js +97 -0
- package/package.json +7 -3
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.idleQueues = exports.stripSuffixes = exports.processQueueSet = exports.deleteQueue = exports.checkIdle = exports.getMetric = exports.cheapIdleCheck = exports._cheapIdleCheck = exports.attributeNames = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Implementation of checks and caching of checks to determine if queues are idle.
|
|
9
|
+
*/
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const sqs_js_1 = require("./sqs.js");
|
|
12
|
+
const cloudWatch_js_1 = require("./cloudWatch.js");
|
|
13
|
+
const defaults_js_1 = require("./defaults.js");
|
|
14
|
+
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
15
|
+
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
16
|
+
const qrlCache_js_1 = require("./qrlCache.js");
|
|
17
|
+
const cache_js_1 = require("./cache.js");
|
|
18
|
+
// const AWS = require('aws-sdk')
|
|
19
|
+
const debug_1 = __importDefault(require("debug"));
|
|
20
|
+
const debug = (0, debug_1.default)('qdone:idleQueues');
|
|
21
|
+
// Queue attributes we check to determine idle
|
|
22
|
+
exports.attributeNames = [
|
|
23
|
+
'ApproximateNumberOfMessages',
|
|
24
|
+
'ApproximateNumberOfMessagesNotVisible',
|
|
25
|
+
'ApproximateNumberOfMessagesDelayed'
|
|
26
|
+
];
|
|
27
|
+
// CloudWatch metrics we check to determine idle
|
|
28
|
+
const metricNames = [
|
|
29
|
+
'NumberOfMessagesSent',
|
|
30
|
+
'NumberOfMessagesReceived',
|
|
31
|
+
'NumberOfMessagesDeleted',
|
|
32
|
+
'ApproximateNumberOfMessagesVisible',
|
|
33
|
+
'ApproximateNumberOfMessagesNotVisible',
|
|
34
|
+
'ApproximateNumberOfMessagesDelayed',
|
|
35
|
+
// 'NumberOfEmptyReceives',
|
|
36
|
+
'ApproximateAgeOfOldestMessage'
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Actual SQS call, used in conjunction with cache.
|
|
40
|
+
*/
|
|
41
|
+
async function _cheapIdleCheck(qname, qrl, opt) {
|
|
42
|
+
debug('_cheapIdleCheck', qname, qrl);
|
|
43
|
+
try {
|
|
44
|
+
const client = (0, sqs_js_1.getSQSClient)();
|
|
45
|
+
const cmd = new client_sqs_1.GetQueueAttributesCommand({ AttributeNames: exports.attributeNames, QueueUrl: qrl });
|
|
46
|
+
const data = await client.send(cmd);
|
|
47
|
+
debug('data', data);
|
|
48
|
+
const result = data.Attributes;
|
|
49
|
+
result.queue = qname.slice(opt.prefix.length);
|
|
50
|
+
// We are idle if all the messages attributes are zero
|
|
51
|
+
result.idle = exports.attributeNames.filter(k => result[k] === '0').length === exports.attributeNames.length;
|
|
52
|
+
result.exists = true;
|
|
53
|
+
debug({ result, SQS: 1 });
|
|
54
|
+
return { result, SQS: 1 };
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
debug({ _cheapIdleCheck: e });
|
|
58
|
+
if (e instanceof client_sqs_1.QueueDoesNotExist) {
|
|
59
|
+
return { result: { idle: undefined, exists: false }, SQS: 1 };
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports._cheapIdleCheck = _cheapIdleCheck;
|
|
67
|
+
/**
|
|
68
|
+
* Gets queue attributes from the SQS api and assesses whether queue is idle
|
|
69
|
+
* at this immediate moment.
|
|
70
|
+
*/
|
|
71
|
+
async function cheapIdleCheck(qname, qrl, opt) {
|
|
72
|
+
debug('cheapIdleCheck', qname, qrl);
|
|
73
|
+
// Just call the API if we don't have a cache
|
|
74
|
+
if (!opt.cacheUri)
|
|
75
|
+
return _cheapIdleCheck(qname, qrl, opt);
|
|
76
|
+
// Otherwise check cache
|
|
77
|
+
const key = 'cheap-idle-check:' + qrl;
|
|
78
|
+
const cacheResult = await (0, cache_js_1.getCache)(key, opt);
|
|
79
|
+
debug({ cacheResult });
|
|
80
|
+
if (cacheResult) {
|
|
81
|
+
debug({ action: 'return resolved' });
|
|
82
|
+
return { result: cacheResult, SQS: 0 };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Cache miss, make call
|
|
86
|
+
debug({ action: 'do real check' });
|
|
87
|
+
const { result, SQS } = await _cheapIdleCheck(qname, qrl, opt);
|
|
88
|
+
debug({ action: 'setCache', key, result });
|
|
89
|
+
const ok = await (0, cache_js_1.setCache)(key, result, opt);
|
|
90
|
+
debug({ action: 'return result of set cache', ok });
|
|
91
|
+
return { result, SQS };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.cheapIdleCheck = cheapIdleCheck;
|
|
95
|
+
/**
|
|
96
|
+
* Gets a single metric from the CloudWatch api.
|
|
97
|
+
*/
|
|
98
|
+
async function getMetric(qname, qrl, metricName, opt) {
|
|
99
|
+
debug('getMetric', qname, qrl, metricName);
|
|
100
|
+
const now = new Date();
|
|
101
|
+
const params = {
|
|
102
|
+
StartTime: new Date(now.getTime() - 1000 * 60 * opt.idleFor),
|
|
103
|
+
EndTime: now,
|
|
104
|
+
MetricName: metricName,
|
|
105
|
+
Namespace: 'AWS/SQS',
|
|
106
|
+
Period: 3600,
|
|
107
|
+
Dimensions: [{ Name: 'QueueName', Value: qname }],
|
|
108
|
+
Statistics: ['Sum']
|
|
109
|
+
// Unit: ['']
|
|
110
|
+
};
|
|
111
|
+
const client = (0, cloudWatch_js_1.getCloudWatchClient)();
|
|
112
|
+
const cmd = new client_cloudwatch_1.GetMetricStatisticsCommand(params);
|
|
113
|
+
const data = await client.send(cmd);
|
|
114
|
+
debug('getMetric data', data);
|
|
115
|
+
return {
|
|
116
|
+
[metricName]: data.Datapoints.map(d => d.Sum).reduce((a, b) => a + b, 0)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
exports.getMetric = getMetric;
|
|
120
|
+
/**
|
|
121
|
+
* Checks if a single queue is idle. First queries the cheap ($0.40/1M) SQS API and
|
|
122
|
+
* continues to the expensive ($10/1M) CloudWatch API, checking to make sure there
|
|
123
|
+
* were no messages in the queue in the specified time period. Only if all relevant
|
|
124
|
+
* metrics show no messages, is it ok to delete the queue.
|
|
125
|
+
*
|
|
126
|
+
* Realistically, the number of CloudWatch calls will depend on usage patterns, but
|
|
127
|
+
* I see a ~70% reduction in calls by checking metrics in the given order.
|
|
128
|
+
* We could randomize the order, but for my test use case, it's always cheaper
|
|
129
|
+
* to check NumberOfMessagesSent first, and is the primary indicator of use.
|
|
130
|
+
*/
|
|
131
|
+
async function checkIdle(qname, qrl, opt) {
|
|
132
|
+
// Do the cheap check first to make sure there is no data in flight at the moment
|
|
133
|
+
debug('checkIdle', qname, qrl);
|
|
134
|
+
const { result: cheapResult, SQS } = await cheapIdleCheck(qname, qrl, opt);
|
|
135
|
+
debug('cheapResult', cheapResult);
|
|
136
|
+
// Short circuit further calls if cheap result is conclusive
|
|
137
|
+
if (cheapResult.idle === false || cheapResult.exists === false) {
|
|
138
|
+
return {
|
|
139
|
+
queue: qname.slice(opt.prefix.length),
|
|
140
|
+
cheap: { SQS, result: cheapResult },
|
|
141
|
+
idle: cheapResult.idle,
|
|
142
|
+
exists: cheapResult.exists,
|
|
143
|
+
apiCalls: { SQS, CloudWatch: 0 }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// If we get here, there's nothing in the queue at the moment,
|
|
147
|
+
// so we have to check metrics one at a time
|
|
148
|
+
const apiCalls = { SQS, CloudWatch: 0 };
|
|
149
|
+
const results = [];
|
|
150
|
+
let idle = true;
|
|
151
|
+
for (const metricName of metricNames) {
|
|
152
|
+
// Check metrics in order
|
|
153
|
+
const result = await getMetric(qname, qrl, metricName, opt);
|
|
154
|
+
results.push(result);
|
|
155
|
+
debug('getMetric result', result);
|
|
156
|
+
apiCalls.CloudWatch++;
|
|
157
|
+
// Recalculate idle
|
|
158
|
+
idle = result[metricName] === 0;
|
|
159
|
+
if (!idle)
|
|
160
|
+
break; // and stop checking metrics if we find evidence of activity
|
|
161
|
+
}
|
|
162
|
+
// Calculate stats
|
|
163
|
+
const stats = Object.assign({
|
|
164
|
+
queue: qname.slice(opt.prefix.length),
|
|
165
|
+
cheap: cheapResult,
|
|
166
|
+
apiCalls,
|
|
167
|
+
idle,
|
|
168
|
+
exists: true
|
|
169
|
+
}, ...results // merge in results from CloudWatch
|
|
170
|
+
);
|
|
171
|
+
debug('checkIdle stats', stats);
|
|
172
|
+
return stats;
|
|
173
|
+
}
|
|
174
|
+
exports.checkIdle = checkIdle;
|
|
175
|
+
/**
|
|
176
|
+
* Just deletes a queue.
|
|
177
|
+
*/
|
|
178
|
+
async function deleteQueue(qname, qrl, opt) {
|
|
179
|
+
const cmd = new client_sqs_1.DeleteQueueCommand({ QueueUrl: qrl });
|
|
180
|
+
const result = await (0, sqs_js_1.getSQSClient)().send(cmd);
|
|
181
|
+
debug(result);
|
|
182
|
+
if (opt.verbose)
|
|
183
|
+
console.error(chalk_1.default.blue('Deleted ') + qname.slice(opt.prefix.length));
|
|
184
|
+
return {
|
|
185
|
+
deleted: true,
|
|
186
|
+
apiCalls: { SQS: 1, CloudWatch: 0 }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
exports.deleteQueue = deleteQueue;
|
|
190
|
+
/**
|
|
191
|
+
* Processes a queue and its fail and delete queue, treating them as a unit.
|
|
192
|
+
*/
|
|
193
|
+
async function processQueueSet(qname, qrl, opt) {
|
|
194
|
+
const isFifo = qname.endsWith('.fifo');
|
|
195
|
+
const normalizeOptions = Object.assign({}, opt, { fifo: isFifo });
|
|
196
|
+
// Generate DLQ name/url
|
|
197
|
+
const dqname = (0, qrlCache_js_1.normalizeDLQName)(qname, normalizeOptions);
|
|
198
|
+
const dqrl = (0, qrlCache_js_1.normalizeDLQName)(dqname, normalizeOptions);
|
|
199
|
+
// Generate fail queue name/url
|
|
200
|
+
const fqname = (0, qrlCache_js_1.normalizeFailQueueName)(qname, normalizeOptions);
|
|
201
|
+
const fqrl = (0, qrlCache_js_1.normalizeFailQueueName)(fqname, normalizeOptions);
|
|
202
|
+
debug({ qname, qrl, dqname, dqrl, fqname, fqrl });
|
|
203
|
+
// Idle check
|
|
204
|
+
const qresult = await checkIdle(qname, qrl, opt);
|
|
205
|
+
const fqresult = await checkIdle(fqname, fqrl, opt);
|
|
206
|
+
const dqresult = await checkIdle(dqname, dqrl, opt);
|
|
207
|
+
debug({ qresult, fqresult, dqresult });
|
|
208
|
+
// Start building return value
|
|
209
|
+
const result = Object.assign({
|
|
210
|
+
queue: qname,
|
|
211
|
+
idle: (qresult.idle &&
|
|
212
|
+
(!fqresult.exists || fqresult.idle) &&
|
|
213
|
+
(!fqresult.exists || dqresult.idle)),
|
|
214
|
+
apiCalls: {
|
|
215
|
+
SQS: qresult.apiCalls.SQS + fqresult.apiCalls.SQS + dqresult.apiCalls.SQS,
|
|
216
|
+
CloudWatch: qresult.apiCalls.CloudWatch + fqresult.apiCalls.CloudWatch + dqresult.apiCalls.CloudWatch
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// Queue is idle
|
|
220
|
+
if (qresult.idle && opt.verbose)
|
|
221
|
+
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.'));
|
|
222
|
+
if (fqresult.idle && opt.verbose)
|
|
223
|
+
console.error(chalk_1.default.blue('Queue ') + fqname.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.'));
|
|
224
|
+
if (dqresult.idle && opt.verbose)
|
|
225
|
+
console.error(chalk_1.default.blue('Queue ') + dqname.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.'));
|
|
226
|
+
// Delete if all are idle
|
|
227
|
+
const canDelete = ((qresult.idle || qresult.exists === false) &&
|
|
228
|
+
(fqresult.idle || fqresult.exists === false) &&
|
|
229
|
+
(dqresult.idle || dqresult.exists === false));
|
|
230
|
+
debug({ canDelete });
|
|
231
|
+
if (opt.delete && canDelete) {
|
|
232
|
+
// Normal
|
|
233
|
+
const qdresult = await (async () => {
|
|
234
|
+
debug({ qresult });
|
|
235
|
+
try {
|
|
236
|
+
if (qresult.idle)
|
|
237
|
+
return deleteQueue(qname, qrl, opt);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
if (!(e instanceof client_sqs_1.QueueDoesNotExist))
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
if (qdresult) {
|
|
245
|
+
result.apiCalls.SQS += qdresult.apiCalls.SQS;
|
|
246
|
+
}
|
|
247
|
+
debug({ qdresult });
|
|
248
|
+
// Fail
|
|
249
|
+
const fqdresult = await (async () => {
|
|
250
|
+
debug({ fqresult });
|
|
251
|
+
try {
|
|
252
|
+
if (fqresult.idle)
|
|
253
|
+
return deleteQueue(fqname, fqrl, opt);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
if (!(e instanceof client_sqs_1.QueueDoesNotExist))
|
|
257
|
+
throw e;
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
if (fqdresult) {
|
|
261
|
+
result.apiCalls.SQS += fqdresult.apiCalls.SQS;
|
|
262
|
+
}
|
|
263
|
+
debug({ fqdresult });
|
|
264
|
+
// Dead
|
|
265
|
+
const dqdresult = await (async () => {
|
|
266
|
+
debug({ dqresult });
|
|
267
|
+
try {
|
|
268
|
+
if (dqresult.idle)
|
|
269
|
+
return deleteQueue(dqname, dqrl, opt);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
if (!(e instanceof client_sqs_1.QueueDoesNotExist))
|
|
273
|
+
throw e;
|
|
274
|
+
}
|
|
275
|
+
})();
|
|
276
|
+
if (dqdresult) {
|
|
277
|
+
result.apiCalls.SQS += dqdresult.apiCalls.SQS;
|
|
278
|
+
}
|
|
279
|
+
debug({ dqdresult });
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
exports.processQueueSet = processQueueSet;
|
|
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
|
+
//
|
|
293
|
+
// Resolve queues for listening loop listen
|
|
294
|
+
//
|
|
295
|
+
async function idleQueues(queues, options) {
|
|
296
|
+
const opt = (0, defaults_js_1.getOptionsWithDefaults)(options);
|
|
297
|
+
if (opt.verbose)
|
|
298
|
+
console.error(chalk_1.default.blue('Resolving queues: ') + queues.join(' '));
|
|
299
|
+
const qnames = queues.map(queue => opt.prefix + queue);
|
|
300
|
+
const entries = await (0, qrlCache_js_1.getQnameUrlPairs)(qnames, opt);
|
|
301
|
+
debug('getQnameUrlPairs.then');
|
|
302
|
+
if (opt.verbose) {
|
|
303
|
+
console.error(chalk_1.default.blue(' done'));
|
|
304
|
+
console.error();
|
|
305
|
+
}
|
|
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();
|
|
310
|
+
const filteredEntries = entries.filter(entry => {
|
|
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;
|
|
318
|
+
});
|
|
319
|
+
// But only if we have queues to remove
|
|
320
|
+
if (filteredEntries.length) {
|
|
321
|
+
if (opt.verbose) {
|
|
322
|
+
console.error(chalk_1.default.blue('Checking queues (in this order):'));
|
|
323
|
+
console.error(filteredEntries.map(e => ' ' + e.qname.slice(opt.prefix.length) + chalk_1.default.blue(' - ' + e.qrl)).join('\n'));
|
|
324
|
+
console.error();
|
|
325
|
+
}
|
|
326
|
+
// Check each queue in parallel
|
|
327
|
+
return Promise.all(filteredEntries.map(e => processQueueSet(e.qname, e.qrl, opt)));
|
|
328
|
+
}
|
|
329
|
+
// Otherwise, let caller know
|
|
330
|
+
return 'noQueues';
|
|
331
|
+
}
|
|
332
|
+
exports.idleQueues = idleQueues;
|
|
333
|
+
debug('loaded');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-queue monitoring functionaliry
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getAggregateData = exports.interpretWildcard = exports.monitor = void 0;
|
|
10
|
+
const sqs_js_1 = require("./sqs.js");
|
|
11
|
+
const cloudWatch_js_1 = require("./cloudWatch.js");
|
|
12
|
+
const defaults_js_1 = require("./defaults.js");
|
|
13
|
+
const qrlCache_js_1 = require("./qrlCache.js");
|
|
14
|
+
const debug_1 = __importDefault(require("debug"));
|
|
15
|
+
const debug = (0, debug_1.default)('qdone:monitor');
|
|
16
|
+
/**
|
|
17
|
+
* Splits a queue name with a single wildcard into prefix and suffix regex.
|
|
18
|
+
*/
|
|
19
|
+
async function monitor(queue, save, options) {
|
|
20
|
+
if (queue.endsWith('.fifo'))
|
|
21
|
+
options.fifo = true;
|
|
22
|
+
const opt = (0, defaults_js_1.getOptionsWithDefaults)(options);
|
|
23
|
+
const queueName = (0, qrlCache_js_1.normalizeQueueName)(queue, opt);
|
|
24
|
+
debug({ options, opt, queue, queueName });
|
|
25
|
+
const data = await getAggregateData(queueName);
|
|
26
|
+
console.log(data);
|
|
27
|
+
if (save) {
|
|
28
|
+
if (opt.verbose)
|
|
29
|
+
process.stderr.write('Saving to CloudWatch...');
|
|
30
|
+
await (0, cloudWatch_js_1.putAggregateData)(data);
|
|
31
|
+
if (opt.verbose)
|
|
32
|
+
process.stderr.write('done\n');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.monitor = monitor;
|
|
36
|
+
/**
|
|
37
|
+
* Splits a queue name with a single wildcard into prefix and suffix regex.
|
|
38
|
+
*/
|
|
39
|
+
function interpretWildcard(queueName) {
|
|
40
|
+
const [prefix, suffix] = queueName.split('*');
|
|
41
|
+
// Strip anything that could cause backreferences
|
|
42
|
+
const safeSuffix = (suffix || '').replace(/[^a-zA-Z0-9_.]+/g, '').replace(/\./g, '\\.');
|
|
43
|
+
const suffixRegex = new RegExp(`${safeSuffix}$`);
|
|
44
|
+
// debug({ prefix, suffix, safeSuffix, suffixRegex })
|
|
45
|
+
return { prefix, suffix, safeSuffix, suffixRegex };
|
|
46
|
+
}
|
|
47
|
+
exports.interpretWildcard = interpretWildcard;
|
|
48
|
+
/**
|
|
49
|
+
* Aggregates inmportant attributes across queues and reports a summary.
|
|
50
|
+
* Attributes:
|
|
51
|
+
* - ApproximateNumberOfMessages: Sum
|
|
52
|
+
* - ApproximateNumberOfMessagesDelayed: Sum
|
|
53
|
+
* - ApproximateNumberOfMessagesNotVisible: Sum
|
|
54
|
+
*/
|
|
55
|
+
async function getAggregateData(queueName) {
|
|
56
|
+
const { prefix, suffixRegex } = interpretWildcard(queueName);
|
|
57
|
+
const qrls = await (0, sqs_js_1.getMatchingQueues)(prefix, suffixRegex);
|
|
58
|
+
// debug({ qrls })
|
|
59
|
+
const data = await (0, sqs_js_1.getQueueAttributes)(qrls);
|
|
60
|
+
// debug({ data })
|
|
61
|
+
const total = { totalQueues: 0, contributingQueueNames: new Set() };
|
|
62
|
+
for (const { queue, result } of data) {
|
|
63
|
+
// debug({ row })
|
|
64
|
+
total.totalQueues++;
|
|
65
|
+
for (const key in result.Attributes) {
|
|
66
|
+
const newAtrribute = parseInt(result.Attributes[key], 10);
|
|
67
|
+
if (newAtrribute > 0) {
|
|
68
|
+
total.contributingQueueNames.add(queue);
|
|
69
|
+
total[key] = (total[key] || 0) + newAtrribute;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// debug({ total })
|
|
74
|
+
// convert set to array
|
|
75
|
+
total.contributingQueueNames = [...total.contributingQueueNames.values()];
|
|
76
|
+
total.queueName = queueName;
|
|
77
|
+
return total;
|
|
78
|
+
}
|
|
79
|
+
exports.getAggregateData = getAggregateData;
|
|
80
|
+
debug('loaded');
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A cache for maintaining queue urls. Avoids having to call the GetQueueUrl
|
|
4
|
+
* api too often.
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.getQnameUrlPairs = exports.getMatchingQueues = exports.ingestQRLs = exports.qrlCacheInvalidate = exports.qrlCacheSet = exports.qrlCacheGet = exports.qrlCacheClear = exports.normalizeDLQName = exports.normalizeFailQueueName = exports.normalizeQueueName = exports.fifoSuffix = void 0;
|
|
11
|
+
const url_1 = require("url");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
14
|
+
const sqs_js_1 = require("./sqs.js");
|
|
15
|
+
const debug_1 = __importDefault(require("debug"));
|
|
16
|
+
const debug = (0, debug_1.default)('qdone:qrlCache');
|
|
17
|
+
// The actual cache is shared across callers.
|
|
18
|
+
const qcache = new Map();
|
|
19
|
+
//
|
|
20
|
+
// Normalizes a queue name to end with .fifo if opt.fifo is set
|
|
21
|
+
//
|
|
22
|
+
exports.fifoSuffix = '.fifo';
|
|
23
|
+
function normalizeQueueName(qname, opt) {
|
|
24
|
+
const hasFifo = qname.endsWith(exports.fifoSuffix);
|
|
25
|
+
const needsFifo = opt.fifo && qname.slice(-1) !== '*';
|
|
26
|
+
const needsPrefix = !qname.startsWith(opt.prefix);
|
|
27
|
+
const base = hasFifo ? qname.slice(0, -exports.fifoSuffix.length) : qname;
|
|
28
|
+
return (needsPrefix ? opt.prefix : '') + base + (needsFifo ? exports.fifoSuffix : '');
|
|
29
|
+
}
|
|
30
|
+
exports.normalizeQueueName = normalizeQueueName;
|
|
31
|
+
//
|
|
32
|
+
// Normalizes fail queue name, appending both --fail-suffix and .fifo depending on opt
|
|
33
|
+
//
|
|
34
|
+
function normalizeFailQueueName(fqname, opt) {
|
|
35
|
+
const newopt = Object.assign({}, opt, { fifo: false });
|
|
36
|
+
const base = normalizeQueueName(fqname, newopt); // strip fifo
|
|
37
|
+
debug({ fqname, base, opt });
|
|
38
|
+
const needsFail = !base.endsWith(opt.failSuffix);
|
|
39
|
+
return base + (needsFail ? opt.failSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
|
|
40
|
+
}
|
|
41
|
+
exports.normalizeFailQueueName = normalizeFailQueueName;
|
|
42
|
+
//
|
|
43
|
+
// Normalizes dlq queue name, appending both --dlq-suffix and .fifo depending on opt
|
|
44
|
+
//
|
|
45
|
+
function normalizeDLQName(dqname, opt) {
|
|
46
|
+
const newopt = Object.assign({}, opt, { fifo: false });
|
|
47
|
+
const base = normalizeQueueName(dqname, newopt); // strip fifo
|
|
48
|
+
const needsDead = !base.endsWith(opt.dlqSuffix);
|
|
49
|
+
return base + (needsDead ? opt.dlqSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
|
|
50
|
+
}
|
|
51
|
+
exports.normalizeDLQName = normalizeDLQName;
|
|
52
|
+
//
|
|
53
|
+
// Clear cache
|
|
54
|
+
//
|
|
55
|
+
function qrlCacheClear() {
|
|
56
|
+
qcache.clear();
|
|
57
|
+
}
|
|
58
|
+
exports.qrlCacheClear = qrlCacheClear;
|
|
59
|
+
//
|
|
60
|
+
// Get QRL (Queue URL)
|
|
61
|
+
// Returns a promise for the queue name
|
|
62
|
+
//
|
|
63
|
+
async function qrlCacheGet(qname) {
|
|
64
|
+
debug('get', qname, qcache);
|
|
65
|
+
if (qcache.has(qname))
|
|
66
|
+
return qcache.get(qname);
|
|
67
|
+
const client = (0, sqs_js_1.getSQSClient)();
|
|
68
|
+
const input = { QueueName: qname };
|
|
69
|
+
const cmd = new client_sqs_1.GetQueueUrlCommand(input);
|
|
70
|
+
// debug({ cmd })
|
|
71
|
+
const result = await client.send(cmd);
|
|
72
|
+
// debug('result', result)
|
|
73
|
+
if (!result) {
|
|
74
|
+
qrlCacheInvalidate(qname);
|
|
75
|
+
throw new client_sqs_1.QueueDoesNotExist(qname);
|
|
76
|
+
}
|
|
77
|
+
const { QueueUrl: qrl } = result;
|
|
78
|
+
// debug('getQueueUrl returned', data)
|
|
79
|
+
qcache.set(qname, qrl);
|
|
80
|
+
// debug('qcache', Object.keys(qcache), 'get', qname, ' => ', qcache[qname])
|
|
81
|
+
return qrl;
|
|
82
|
+
}
|
|
83
|
+
exports.qrlCacheGet = qrlCacheGet;
|
|
84
|
+
//
|
|
85
|
+
// Set QRL (Queue URL)
|
|
86
|
+
// Immediately updates the cache
|
|
87
|
+
//
|
|
88
|
+
function qrlCacheSet(qname, qrl) {
|
|
89
|
+
if (qrl)
|
|
90
|
+
qcache.set(qname, qrl);
|
|
91
|
+
// debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
92
|
+
}
|
|
93
|
+
exports.qrlCacheSet = qrlCacheSet;
|
|
94
|
+
//
|
|
95
|
+
// Invalidate cache for given qname
|
|
96
|
+
//
|
|
97
|
+
function qrlCacheInvalidate(qname) {
|
|
98
|
+
// debug('qcache', Object.keys(qcache), 'delete', qname, ' (was ', qcache[qname], ')')
|
|
99
|
+
qcache.delete(qname);
|
|
100
|
+
}
|
|
101
|
+
exports.qrlCacheInvalidate = qrlCacheInvalidate;
|
|
102
|
+
//
|
|
103
|
+
// Ingets multiple QRLs
|
|
104
|
+
// Extracts queue names from an array of QRLs and immediately updates the cache.
|
|
105
|
+
//
|
|
106
|
+
function ingestQRLs(qrls) {
|
|
107
|
+
const pairs = [];
|
|
108
|
+
debug('ingestQRLs', qrls);
|
|
109
|
+
for (const qrl of qrls) {
|
|
110
|
+
const qname = (0, path_1.basename)((new url_1.URL(qrl)).pathname);
|
|
111
|
+
qrlCacheSet(qname, qrl);
|
|
112
|
+
pairs.push({ qname, qrl });
|
|
113
|
+
// debug('qcache', Object.keys(qcache), 'ingestQRLs', qname, ' => ', qcache[qname])
|
|
114
|
+
}
|
|
115
|
+
return pairs;
|
|
116
|
+
}
|
|
117
|
+
exports.ingestQRLs = ingestQRLs;
|
|
118
|
+
/**
|
|
119
|
+
* Returns qrls for queues matching the given prefix and regex.
|
|
120
|
+
*/
|
|
121
|
+
async function getMatchingQueues(prefix, nextToken) {
|
|
122
|
+
debug('getMatchingQueues', prefix, nextToken);
|
|
123
|
+
const input = { QueueNamePrefix: prefix, MaxResults: 1000 };
|
|
124
|
+
if (nextToken)
|
|
125
|
+
input.NextToken = nextToken;
|
|
126
|
+
const client = (0, sqs_js_1.getSQSClient)();
|
|
127
|
+
const command = new client_sqs_1.ListQueuesCommand(input);
|
|
128
|
+
const result = await client.send(command);
|
|
129
|
+
debug({ result });
|
|
130
|
+
const { QueueUrls: qrls, NextToken: keepGoing } = result || {};
|
|
131
|
+
debug({ qrls, keepGoing });
|
|
132
|
+
if (keepGoing)
|
|
133
|
+
(qrls || []).push(...await getMatchingQueues(prefix, keepGoing));
|
|
134
|
+
return qrls || [];
|
|
135
|
+
}
|
|
136
|
+
exports.getMatchingQueues = getMatchingQueues;
|
|
137
|
+
//
|
|
138
|
+
// Resolves into a flattened aray of {qname: ..., qrl: ...} objects.
|
|
139
|
+
//
|
|
140
|
+
async function getQnameUrlPairs(qnames, opt) {
|
|
141
|
+
const promises = qnames.map(async function getQueueUrlOrUrls(qname) {
|
|
142
|
+
if (qname.slice(-1) === '*') {
|
|
143
|
+
// Wildcard queues support
|
|
144
|
+
const prefix = qname.slice(0, -1);
|
|
145
|
+
const queues = await getMatchingQueues(prefix);
|
|
146
|
+
debug('listQueues return', queues);
|
|
147
|
+
if (opt.fifo) {
|
|
148
|
+
// Remove non-fifo queues
|
|
149
|
+
const filteredQueues = queues.filter(url => url.slice(-exports.fifoSuffix.length) === exports.fifoSuffix);
|
|
150
|
+
return ingestQRLs(filteredQueues);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return ingestQRLs(queues);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Normal, non wildcard queue support
|
|
158
|
+
try {
|
|
159
|
+
return { qname, qrl: await qrlCacheGet(qname) };
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (!(err instanceof client_sqs_1.QueueDoesNotExist))
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// Flatten nested results
|
|
168
|
+
const results = await Promise.all(promises);
|
|
169
|
+
return ([].concat.apply([], results)).filter(r => r);
|
|
170
|
+
}
|
|
171
|
+
exports.getQnameUrlPairs = getQnameUrlPairs;
|
|
172
|
+
debug('loaded');
|