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.
@@ -0,0 +1,466 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.idleQueues = exports.processQueuePair = exports.processQueue = exports.deleteQueue = exports.checkIdle = exports.getMetric = exports.cheapIdleCheck = exports._cheapIdleCheck = exports.attributeNames = void 0;
52
+ /**
53
+ * Implementation of checks and caching of checks to determine if queues are idle.
54
+ */
55
+ var chalk_1 = __importDefault(require("chalk"));
56
+ var sqs_js_1 = require("./sqs.js");
57
+ var cloudWatch_js_1 = require("./cloudWatch.js");
58
+ var defaults_js_1 = require("./defaults.js");
59
+ var client_sqs_1 = require("@aws-sdk/client-sqs");
60
+ var client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
61
+ var qrlCache_js_1 = require("./qrlCache.js");
62
+ var cache_js_1 = require("./cache.js");
63
+ // const AWS = require('aws-sdk')
64
+ var debug_1 = __importDefault(require("debug"));
65
+ var debug = (0, debug_1.default)('qdone:idleQueues');
66
+ // Queue attributes we check to determine idle
67
+ exports.attributeNames = [
68
+ 'ApproximateNumberOfMessages',
69
+ 'ApproximateNumberOfMessagesNotVisible',
70
+ 'ApproximateNumberOfMessagesDelayed'
71
+ ];
72
+ // CloudWatch metrics we check to determine idle
73
+ var metricNames = [
74
+ 'NumberOfMessagesSent',
75
+ 'NumberOfMessagesReceived',
76
+ 'NumberOfMessagesDeleted',
77
+ 'ApproximateNumberOfMessagesVisible',
78
+ 'ApproximateNumberOfMessagesNotVisible',
79
+ 'ApproximateNumberOfMessagesDelayed',
80
+ // 'NumberOfEmptyReceives',
81
+ 'ApproximateAgeOfOldestMessage'
82
+ ];
83
+ /**
84
+ * Actual SQS call, used in conjunction with cache.
85
+ */
86
+ function _cheapIdleCheck(qname, qrl, opt) {
87
+ return __awaiter(this, void 0, void 0, function () {
88
+ var client, cmd, data, result;
89
+ return __generator(this, function (_a) {
90
+ switch (_a.label) {
91
+ case 0:
92
+ client = (0, sqs_js_1.getSQSClient)();
93
+ cmd = new client_sqs_1.GetQueueAttributesCommand({ AttributeNames: exports.attributeNames, QueueUrl: qrl });
94
+ return [4 /*yield*/, client.send(cmd)
95
+ // debug('data', data)
96
+ ];
97
+ case 1:
98
+ data = _a.sent();
99
+ result = data.Attributes;
100
+ result.queue = qname.slice(opt.prefix.length);
101
+ // We are idle if all the messages attributes are zero
102
+ result.idle = exports.attributeNames.filter(function (k) { return result[k] === '0'; }).length === exports.attributeNames.length;
103
+ return [2 /*return*/, { result: result, SQS: 1 }];
104
+ }
105
+ });
106
+ });
107
+ }
108
+ exports._cheapIdleCheck = _cheapIdleCheck;
109
+ /**
110
+ * Gets queue attributes from the SQS api and assesses whether queue is idle
111
+ * at this immediate moment.
112
+ */
113
+ function cheapIdleCheck(qname, qrl, opt) {
114
+ return __awaiter(this, void 0, void 0, function () {
115
+ var key, cacheResult, _a, result, SQS, ok;
116
+ return __generator(this, function (_b) {
117
+ switch (_b.label) {
118
+ case 0:
119
+ // Just call the API if we don't have a cache
120
+ if (!opt.cacheUri)
121
+ return [2 /*return*/, _cheapIdleCheck(qname, qrl, opt)
122
+ // Otherwise check cache
123
+ ];
124
+ key = 'cheap-idle-check:' + qrl;
125
+ return [4 /*yield*/, (0, cache_js_1.getCache)(key, opt)];
126
+ case 1:
127
+ cacheResult = _b.sent();
128
+ debug({ cacheResult: cacheResult });
129
+ if (!cacheResult) return [3 /*break*/, 2];
130
+ debug({ action: 'return resolved' });
131
+ return [2 /*return*/, { result: cacheResult, SQS: 0 }];
132
+ case 2:
133
+ // Cache miss, make call
134
+ debug({ action: 'do real check' });
135
+ return [4 /*yield*/, _cheapIdleCheck(qname, qrl, opt)];
136
+ case 3:
137
+ _a = _b.sent(), result = _a.result, SQS = _a.SQS;
138
+ debug({ action: 'setCache', key: key, result: result });
139
+ return [4 /*yield*/, (0, cache_js_1.setCache)(key, result, opt)];
140
+ case 4:
141
+ ok = _b.sent();
142
+ debug({ action: 'return result of set cache', ok: ok });
143
+ return [2 /*return*/, { result: result, SQS: SQS }];
144
+ }
145
+ });
146
+ });
147
+ }
148
+ exports.cheapIdleCheck = cheapIdleCheck;
149
+ /**
150
+ * Gets a single metric from the CloudWatch api.
151
+ */
152
+ function getMetric(qname, qrl, metricName, opt) {
153
+ return __awaiter(this, void 0, void 0, function () {
154
+ var now, params, client, cmd, data;
155
+ var _a;
156
+ return __generator(this, function (_b) {
157
+ switch (_b.label) {
158
+ case 0:
159
+ debug('getMetric', qname, qrl, metricName);
160
+ now = new Date();
161
+ params = {
162
+ StartTime: new Date(now.getTime() - 1000 * 60 * opt.idleFor),
163
+ EndTime: now,
164
+ MetricName: metricName,
165
+ Namespace: 'AWS/SQS',
166
+ Period: 3600,
167
+ Dimensions: [{ Name: 'QueueName', Value: qname }],
168
+ Statistics: ['Sum']
169
+ // Unit: ['']
170
+ };
171
+ client = (0, cloudWatch_js_1.getCloudWatchClient)();
172
+ cmd = new client_cloudwatch_1.GetMetricStatisticsCommand(params);
173
+ return [4 /*yield*/, client.send(cmd)];
174
+ case 1:
175
+ data = _b.sent();
176
+ debug('getMetric data', data);
177
+ return [2 /*return*/, (_a = {},
178
+ _a[metricName] = data.Datapoints.map(function (d) { return d.Sum; }).reduce(function (a, b) { return a + b; }, 0),
179
+ _a)];
180
+ }
181
+ });
182
+ });
183
+ }
184
+ exports.getMetric = getMetric;
185
+ /**
186
+ * Checks if a single queue is idle. First queries the cheap ($0.40/1M) SQS API and
187
+ * continues to the expensive ($10/1M) CloudWatch API, checking to make sure there
188
+ * were no messages in the queue in the specified time period. Only if all relevant
189
+ * metrics show no messages, is it ok to delete the queue.
190
+ *
191
+ * Realistically, the number of CloudWatch calls will depend on usage patterns, but
192
+ * I see a ~70% reduction in calls by checking metrics in the given order.
193
+ * We could randomize the order, but for my test use case, it's always cheaper
194
+ * to check NumberOfMessagesSent first, and is the primary indicator of use.
195
+ */
196
+ function checkIdle(qname, qrl, opt) {
197
+ return __awaiter(this, void 0, void 0, function () {
198
+ var _a, cheapResult, SQS, apiCalls, results, idle, _i, metricNames_1, metricName, result, stats;
199
+ return __generator(this, function (_b) {
200
+ switch (_b.label) {
201
+ case 0:
202
+ // Do the cheap check first to make sure there is no data in flight at the moment
203
+ debug('checkIdle', qname, qrl);
204
+ return [4 /*yield*/, cheapIdleCheck(qname, qrl, opt)];
205
+ case 1:
206
+ _a = _b.sent(), cheapResult = _a.result, SQS = _a.SQS;
207
+ debug('cheapResult', cheapResult);
208
+ // Short circuit further calls if cheap result shows data
209
+ if (cheapResult.idle === false) {
210
+ return [2 /*return*/, {
211
+ queue: qname.slice(opt.prefix.length),
212
+ cheap: cheapResult,
213
+ idle: false,
214
+ apiCalls: { SQS: SQS, CloudWatch: 0 }
215
+ }];
216
+ }
217
+ apiCalls = { SQS: 1, CloudWatch: 0 };
218
+ results = [];
219
+ idle = true;
220
+ _i = 0, metricNames_1 = metricNames;
221
+ _b.label = 2;
222
+ case 2:
223
+ if (!(_i < metricNames_1.length)) return [3 /*break*/, 5];
224
+ metricName = metricNames_1[_i];
225
+ return [4 /*yield*/, getMetric(qname, qrl, metricName, opt)];
226
+ case 3:
227
+ result = _b.sent();
228
+ results.push(result);
229
+ debug('getMetric result', result);
230
+ apiCalls.CloudWatch++;
231
+ // Recalculate idle
232
+ idle = result[metricName] === 0;
233
+ if (!idle)
234
+ return [3 /*break*/, 5]; // and stop checking metrics if we find evidence of activity
235
+ _b.label = 4;
236
+ case 4:
237
+ _i++;
238
+ return [3 /*break*/, 2];
239
+ case 5:
240
+ stats = Object.assign.apply(Object, __spreadArray([{
241
+ queue: qname.slice(opt.prefix.length),
242
+ cheap: cheapResult,
243
+ apiCalls: apiCalls,
244
+ idle: idle
245
+ }], results // merge in results from CloudWatch
246
+ , false));
247
+ debug('checkIdle stats', stats);
248
+ return [2 /*return*/, stats];
249
+ }
250
+ });
251
+ });
252
+ }
253
+ exports.checkIdle = checkIdle;
254
+ /**
255
+ * Just deletes a queue.
256
+ */
257
+ function deleteQueue(qname, qrl, opt) {
258
+ return __awaiter(this, void 0, void 0, function () {
259
+ var cmd, result;
260
+ return __generator(this, function (_a) {
261
+ switch (_a.label) {
262
+ case 0:
263
+ cmd = new client_sqs_1.DeleteQueueCommand({ QueueUrl: qrl });
264
+ return [4 /*yield*/, (0, sqs_js_1.getSQSClient)().send(cmd)];
265
+ case 1:
266
+ result = _a.sent();
267
+ debug(result);
268
+ if (opt.verbose)
269
+ console.error(chalk_1.default.blue('Deleted ') + qname.slice(opt.prefix.length));
270
+ return [2 /*return*/, {
271
+ deleted: true,
272
+ apiCalls: { SQS: 1, CloudWatch: 0 }
273
+ }];
274
+ }
275
+ });
276
+ });
277
+ }
278
+ exports.deleteQueue = deleteQueue;
279
+ /**
280
+ * Processes a single queue, checking for idle, deleting if applicable.
281
+ */
282
+ function processQueue(qname, qrl, opt) {
283
+ return __awaiter(this, void 0, void 0, function () {
284
+ var result, deleteResult, resultIncludingDelete;
285
+ return __generator(this, function (_a) {
286
+ switch (_a.label) {
287
+ case 0: return [4 /*yield*/, checkIdle(qname, qrl, opt)];
288
+ case 1:
289
+ result = _a.sent();
290
+ debug(qname, result);
291
+ // Queue is active
292
+ if (!result.idle) {
293
+ // Notify and return
294
+ if (opt.verbose)
295
+ 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.'));
296
+ return [2 /*return*/, result];
297
+ }
298
+ // Queue is idle
299
+ if (opt.verbose)
300
+ 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.'));
301
+ if (!opt.delete) return [3 /*break*/, 3];
302
+ return [4 /*yield*/, deleteQueue(qname, qrl, opt)];
303
+ case 2:
304
+ deleteResult = _a.sent();
305
+ resultIncludingDelete = Object.assign(result, {
306
+ deleted: deleteResult.deleted,
307
+ apiCalls: {
308
+ SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
309
+ CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
310
+ }
311
+ });
312
+ return [2 /*return*/, resultIncludingDelete];
313
+ case 3: return [2 /*return*/];
314
+ }
315
+ });
316
+ });
317
+ }
318
+ exports.processQueue = processQueue;
319
+ /**
320
+ * Processes a queue and its fail queue, treating them as a unit.
321
+ */
322
+ function processQueuePair(qname, qrl, opt) {
323
+ return __awaiter(this, void 0, void 0, function () {
324
+ var isFifo, normalizeOptions, fqname, fqrl, result, active, fresult, idleCheckResult, factive, _a, dresult, dfresult, e_1, deleteResult, resultIncludingDelete;
325
+ return __generator(this, function (_b) {
326
+ switch (_b.label) {
327
+ case 0:
328
+ isFifo = qname.endsWith('.fifo');
329
+ normalizeOptions = Object.assign({}, opt, { fifo: isFifo });
330
+ fqname = (0, qrlCache_js_1.normalizeFailQueueName)(qname, normalizeOptions);
331
+ fqrl = (0, qrlCache_js_1.normalizeFailQueueName)(qrl, normalizeOptions);
332
+ return [4 /*yield*/, checkIdle(qname, qrl, opt)];
333
+ case 1:
334
+ result = _b.sent();
335
+ debug('result', result);
336
+ active = !result.idle;
337
+ if (active) {
338
+ if (opt.verbose)
339
+ 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.'));
340
+ return [2 /*return*/, result];
341
+ }
342
+ // Queue is idle
343
+ if (opt.verbose)
344
+ 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.'));
345
+ _b.label = 2;
346
+ case 2:
347
+ _b.trys.push([2, 5, , 7]);
348
+ return [4 /*yield*/, checkIdle(fqname, fqrl, opt)];
349
+ case 3:
350
+ fresult = _b.sent();
351
+ debug('fresult', fresult);
352
+ idleCheckResult = Object.assign(result, { idle: result.idle && fresult.idle, failq: fresult }, {
353
+ apiCalls: {
354
+ SQS: result.apiCalls.SQS + fresult.apiCalls.SQS,
355
+ CloudWatch: result.apiCalls.CloudWatch + fresult.apiCalls.CloudWatch
356
+ }
357
+ });
358
+ factive = !fresult.idle;
359
+ if (factive) {
360
+ if (opt.verbose)
361
+ console.error(chalk_1.default.blue('Queue ') + fqname.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.'));
362
+ return [2 /*return*/, idleCheckResult];
363
+ }
364
+ // Queue is idle
365
+ if (opt.verbose)
366
+ 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.'));
367
+ // Trigger a delete if the user wants it
368
+ if (!opt.delete)
369
+ return [2 /*return*/, idleCheckResult];
370
+ return [4 /*yield*/, Promise.all([
371
+ deleteQueue(qname, qrl, opt),
372
+ deleteQueue(fqname, fqrl, opt)
373
+ ])];
374
+ case 4:
375
+ _a = _b.sent(), dresult = _a[0], dfresult = _a[1];
376
+ return [2 /*return*/, Object.assign(idleCheckResult, {
377
+ apiCalls: {
378
+ // Sum the SQS calls across all four
379
+ SQS: [result, fresult, dresult, dfresult]
380
+ .map(function (r) { return r.apiCalls.SQS; })
381
+ .reduce(function (a, b) { return a + b; }, 0),
382
+ // Sum the CloudWatch calls across all four
383
+ CloudWatch: [result, fresult, dresult, dfresult]
384
+ .map(function (r) { return r.apiCalls.CloudWatch; })
385
+ .reduce(function (a, b) { return a + b; }, 0)
386
+ }
387
+ })];
388
+ case 5:
389
+ e_1 = _b.sent();
390
+ // Handle the case where the fail queue has been deleted or was never
391
+ // created for some reason
392
+ if (!(e_1 instanceof client_sqs_1.QueueDoesNotExist))
393
+ throw e_1;
394
+ // Fail queue doesn't exist if we get here
395
+ if (opt.verbose)
396
+ console.error(chalk_1.default.blue('Queue ') + fqname.slice(opt.prefix.length) + chalk_1.default.blue(' does not exist.'));
397
+ // Handle delete
398
+ if (!opt.delete)
399
+ return [2 /*return*/, result];
400
+ return [4 /*yield*/, deleteQueue(qname, qrl, opt)];
401
+ case 6:
402
+ deleteResult = _b.sent();
403
+ resultIncludingDelete = Object.assign(result, {
404
+ deleted: deleteResult.deleted,
405
+ apiCalls: {
406
+ SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
407
+ CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
408
+ }
409
+ });
410
+ return [2 /*return*/, resultIncludingDelete];
411
+ case 7: return [2 /*return*/];
412
+ }
413
+ });
414
+ });
415
+ }
416
+ exports.processQueuePair = processQueuePair;
417
+ //
418
+ // Resolve queues for listening loop listen
419
+ //
420
+ function idleQueues(queues, options) {
421
+ return __awaiter(this, void 0, void 0, function () {
422
+ var opt, qnames, entries, filteredEntries;
423
+ return __generator(this, function (_a) {
424
+ switch (_a.label) {
425
+ case 0:
426
+ opt = (0, defaults_js_1.getOptionsWithDefaults)(options);
427
+ if (opt.verbose)
428
+ console.error(chalk_1.default.blue('Resolving queues: ') + queues.join(' '));
429
+ qnames = queues.map(function (queue) { return opt.prefix + queue; });
430
+ return [4 /*yield*/, (0, qrlCache_js_1.getQnameUrlPairs)(qnames, opt)];
431
+ case 1:
432
+ entries = _a.sent();
433
+ debug('getQnameUrlPairs.then');
434
+ if (opt.verbose) {
435
+ console.error(chalk_1.default.blue(' done'));
436
+ console.error();
437
+ }
438
+ filteredEntries = entries.filter(function (entry) {
439
+ var suf = opt.failSuffix;
440
+ var sufFifo = opt.failSuffix + qrlCache_js_1.fifoSuffix;
441
+ var isFail = entry.qname.endsWith(suf);
442
+ var isFifoFail = entry.qname.endsWith(sufFifo);
443
+ return opt.includeFailed ? true : (!isFail && !isFifoFail);
444
+ });
445
+ // But only if we have queues to remove
446
+ if (filteredEntries.length) {
447
+ if (opt.verbose) {
448
+ console.error(chalk_1.default.blue('Checking queues (in this order):'));
449
+ console.error(filteredEntries.map(function (e) {
450
+ return ' ' + e.qname.slice(opt.prefix.length) + chalk_1.default.blue(' - ' + e.qrl);
451
+ }).join('\n'));
452
+ console.error();
453
+ }
454
+ // Check each queue in parallel
455
+ if (opt.unpair)
456
+ return [2 /*return*/, Promise.all(filteredEntries.map(function (e) { return processQueue(e.qname, e.qrl, opt); }))];
457
+ return [2 /*return*/, Promise.all(filteredEntries.map(function (e) { return processQueuePair(e.qname, e.qrl, opt); }))];
458
+ }
459
+ // Otherwise, let caller know
460
+ return [2 /*return*/, 'noQueues'];
461
+ }
462
+ });
463
+ });
464
+ }
465
+ exports.idleQueues = idleQueues;
466
+ debug('loaded');