qdone 1.7.0 → 2.0.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/idleQueues.js CHANGED
@@ -1,12 +1,21 @@
1
+ /**
2
+ * Implementation of checks and caching of checks to determine if queues are idle.
3
+ */
4
+ import chalk from 'chalk'
5
+ import { getSQSClient } from './sqs.js'
6
+ import { getCloudWatchClient } from './cloudWatch.js'
7
+ import { getOptionsWithDefaults } from './defaults.js'
8
+ import { GetQueueAttributesCommand, DeleteQueueCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
9
+ import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
10
+ import { normalizeFailQueueName, getQnameUrlPairs, fifoSuffix } from './qrlCache.js'
11
+ import { getCache, setCache } from './cache.js'
12
+ // const AWS = require('aws-sdk')
1
13
 
2
- const debug = require('debug')('qdone:idleQueues')
3
- const chalk = require('chalk')
4
- const qrlCache = require('./qrlCache')
5
- const cache = require('./cache')
6
- const AWS = require('aws-sdk')
14
+ import Debug from 'debug'
15
+ const debug = Debug('qdone:idleQueues')
7
16
 
8
17
  // Queue attributes we check to determine idle
9
- const attributeNames = [
18
+ export const attributeNames = [
10
19
  'ApproximateNumberOfMessages',
11
20
  'ApproximateNumberOfMessagesNotVisible',
12
21
  'ApproximateNumberOfMessagesDelayed'
@@ -24,58 +33,55 @@ const metricNames = [
24
33
  'ApproximateAgeOfOldestMessage'
25
34
  ]
26
35
 
27
- function _cheapIdleCheck (qname, qrl, options) {
28
- const sqs = new AWS.SQS()
29
- return sqs
30
- .getQueueAttributes({ AttributeNames: attributeNames, QueueUrl: qrl })
31
- .promise()
32
- .then(data => {
33
- // debug('data', data)
34
- const result = data.Attributes
35
- result.queue = qname.slice(options.prefix.length)
36
- result.idle = attributeNames.filter(k => result[k] === '0').length === attributeNames.length
37
- return Promise.resolve({ result, SQS: 1 })
38
- })
36
+ /**
37
+ * Actual SQS call, used in conjunction with cache.
38
+ */
39
+ export async function _cheapIdleCheck (qname, qrl, opt) {
40
+ const client = getSQSClient()
41
+ const cmd = new GetQueueAttributesCommand({ AttributeNames: attributeNames, QueueUrl: qrl })
42
+ const data = await client.send(cmd)
43
+ // debug('data', data)
44
+ const result = data.Attributes
45
+ result.queue = qname.slice(opt.prefix.length)
46
+ // We are idle if all the messages attributes are zero
47
+ result.idle = attributeNames.filter(k => result[k] === '0').length === attributeNames.length
48
+ return { result, SQS: 1 }
39
49
  }
40
50
 
41
51
  /**
42
52
  * Gets queue attributes from the SQS api and assesses whether queue is idle
43
53
  * at this immediate moment.
44
54
  */
45
- function cheapIdleCheck (qname, qrl, options) {
46
- if (options['cache-uri']) {
47
- const key = 'cheap-idle-check:' + qrl
48
- return cache.getCache(key, options).then(cacheResult => {
49
- debug({ cacheResult })
50
- if (cacheResult) {
51
- debug({ action: 'return resolved' })
52
- return Promise.resolve({ result: cacheResult, SQS: 0 })
53
- } else {
54
- // Cache miss, make actual call
55
- debug({ action: 'do real check' })
56
- return _cheapIdleCheck(qname, qrl, options).then(({ result, SQS }) => {
57
- debug({ action: 'setCache', key, result })
58
- return cache.setCache(key, result, options).then(ok => {
59
- debug({ action: 'return result of set cache', result })
60
- return Promise.resolve({ result, SQS })
61
- })
62
- })
63
- }
64
- })
55
+ export async function cheapIdleCheck (qname, qrl, opt) {
56
+ // Just call the API if we don't have a cache
57
+ if (!opt.cacheUri) return _cheapIdleCheck(qname, qrl, opt)
58
+
59
+ // Otherwise check cache
60
+ const key = 'cheap-idle-check:' + qrl
61
+ const cacheResult = await getCache(key, opt)
62
+ debug({ cacheResult })
63
+ if (cacheResult) {
64
+ debug({ action: 'return resolved' })
65
+ return { result: cacheResult, SQS: 0 }
65
66
  } else {
66
- return _cheapIdleCheck(qname, qrl, options)
67
+ // Cache miss, make call
68
+ debug({ action: 'do real check' })
69
+ const { result, SQS } = await _cheapIdleCheck(qname, qrl, opt)
70
+ debug({ action: 'setCache', key, result })
71
+ const ok = await setCache(key, result, opt)
72
+ debug({ action: 'return result of set cache', ok })
73
+ return { result, SQS }
67
74
  }
68
75
  }
69
- exports.cheapIdleCheck = cheapIdleCheck
70
76
 
71
77
  /**
72
78
  * Gets a single metric from the CloudWatch api.
73
79
  */
74
- function getMetric (qname, qrl, metricName, options) {
80
+ export async function getMetric (qname, qrl, metricName, opt) {
75
81
  debug('getMetric', qname, qrl, metricName)
76
82
  const now = new Date()
77
83
  const params = {
78
- StartTime: new Date(now.getTime() - 1000 * 60 * options['idle-for']),
84
+ StartTime: new Date(now.getTime() - 1000 * 60 * opt.idleFor),
79
85
  EndTime: now,
80
86
  MetricName: metricName,
81
87
  Namespace: 'AWS/SQS',
@@ -84,16 +90,13 @@ function getMetric (qname, qrl, metricName, options) {
84
90
  Statistics: ['Sum']
85
91
  // Unit: ['']
86
92
  }
87
- const cloudwatch = new AWS.CloudWatch()
88
- return cloudwatch
89
- .getMetricStatistics(params)
90
- .promise()
91
- .then(data => {
92
- debug('getMetric data', data)
93
- return Promise.resolve({
94
- [metricName]: data.Datapoints.map(d => d.Sum).reduce((a, b) => a + b, 0)
95
- })
96
- })
93
+ const client = getCloudWatchClient()
94
+ const cmd = new GetMetricStatisticsCommand(params)
95
+ const data = await client.send(cmd)
96
+ debug('getMetric data', data)
97
+ return {
98
+ [metricName]: data.Datapoints.map(d => d.Sum).reduce((a, b) => a + b, 0)
99
+ }
97
100
  }
98
101
 
99
102
  /**
@@ -107,208 +110,224 @@ function getMetric (qname, qrl, metricName, options) {
107
110
  * We could randomize the order, but for my test use case, it's always cheaper
108
111
  * to check NumberOfMessagesSent first, and is the primary indicator of use.
109
112
  */
110
- function checkIdle (qname, qrl, options) {
113
+ export async function checkIdle (qname, qrl, opt) {
111
114
  // Do the cheap check first to make sure there is no data in flight at the moment
112
115
  debug('checkIdle', qname, qrl)
113
- return cheapIdleCheck(qname, qrl, options)
114
- .then(({ result: cheapResult, SQS }) => {
115
- debug('cheapResult', cheapResult)
116
- // Short circuit further calls if cheap result shows data
117
- if (cheapResult.idle === false) {
118
- return {
119
- queue: qname.slice(options.prefix.length),
120
- cheap: cheapResult,
121
- idle: false,
122
- apiCalls: { SQS, CloudWatch: 0 }
123
- }
124
- }
125
- // If we get here, there's nothing in the queue at the moment,
126
- // so we have to check metrics one at a time
127
- return metricNames.reduce((promiseChain, metricName) => {
128
- return promiseChain.then((soFar = {
129
- queue: qname.slice(options.prefix.length),
130
- cheap: cheapResult,
131
- idle: true,
132
- apiCalls: { SQS: 1, CloudWatch: 0 }
133
- }) => {
134
- debug('soFar', soFar)
135
- // Break out of our call chain if we find one failed check
136
- if (soFar.idle === false) return Promise.resolve(soFar)
137
- return getMetric(qname, qrl, metricName, options)
138
- .then(result => {
139
- debug('getMetric result', result)
140
- return Object.assign(
141
- soFar, // start with soFar object
142
- result, // add in our metricName keyed result
143
- { idle: result[metricName] === 0 }, // and recalculate idle
144
- { apiCalls: {
145
- SQS: soFar.apiCalls.SQS,
146
- CloudWatch: soFar.apiCalls.CloudWatch + 1
147
- } }
148
- )
149
- })
150
- })
151
- }, Promise.resolve())
152
- })
116
+ const { result: cheapResult, SQS } = await cheapIdleCheck(qname, qrl, opt)
117
+ debug('cheapResult', cheapResult)
118
+
119
+ // Short circuit further calls if cheap result shows data
120
+ if (cheapResult.idle === false) {
121
+ return {
122
+ queue: qname.slice(opt.prefix.length),
123
+ cheap: cheapResult,
124
+ idle: false,
125
+ apiCalls: { SQS, CloudWatch: 0 }
126
+ }
127
+ }
128
+
129
+ // If we get here, there's nothing in the queue at the moment,
130
+ // so we have to check metrics one at a time
131
+ const apiCalls = { SQS: 1, CloudWatch: 0 }
132
+ const results = []
133
+ let idle = true
134
+ for (const metricName of metricNames) {
135
+ // Check metrics in order
136
+ const result = await getMetric(qname, qrl, metricName, opt)
137
+ results.push(result)
138
+ debug('getMetric result', result)
139
+ apiCalls.CloudWatch++
140
+
141
+ // Recalculate idle
142
+ idle = result[metricName] === 0
143
+ if (!idle) break // and stop checking metrics if we find evidence of activity
144
+ }
145
+
146
+ // Calculate stats
147
+ const stats = Object.assign(
148
+ {
149
+ queue: qname.slice(opt.prefix.length),
150
+ cheap: cheapResult,
151
+ apiCalls,
152
+ idle
153
+ },
154
+ ...results // merge in results from CloudWatch
155
+ )
156
+ debug('checkIdle stats', stats)
157
+ return stats
153
158
  }
154
159
 
155
160
  /**
156
161
  * Just deletes a queue.
157
162
  */
158
- function deleteQueue (qname, qrl, options) {
159
- const sqs = new AWS.SQS()
160
- return sqs.deleteQueue({ QueueUrl: qrl })
161
- .promise()
162
- .then(result => {
163
- debug(result)
164
- if (options.verbose) console.error(chalk.blue('Deleted ') + qname.slice(options.prefix.length))
165
- return Promise.resolve({
166
- deleted: true,
167
- apiCalls: { SQS: 1, CloudWatch: 0 }
168
- })
169
- })
163
+ export async function deleteQueue (qname, qrl, opt) {
164
+ const cmd = new DeleteQueueCommand({ QueueUrl: qrl })
165
+ const result = await getSQSClient().send(cmd)
166
+ debug(result)
167
+ if (opt.verbose) console.error(chalk.blue('Deleted ') + qname.slice(opt.prefix.length))
168
+ return {
169
+ deleted: true,
170
+ apiCalls: { SQS: 1, CloudWatch: 0 }
171
+ }
170
172
  }
171
173
 
172
174
  /**
173
175
  * Processes a single queue, checking for idle, deleting if applicable.
174
176
  */
175
- function processQueue (qname, qrl, options) {
176
- return checkIdle(qname, qrl, options)
177
- .then(result => {
178
- debug(qname, result)
179
- if (result.idle) {
180
- if (options.verbose) console.error(chalk.blue('Queue ') + qname.slice(options.prefix.length) + chalk.blue(' has been ') + 'idle' + chalk.blue(' for the last ') + options['idle-for'] + chalk.blue(' minutes.'))
181
- // Trigger a delete if the user wants it
182
- if (options.delete) {
183
- // End this branch of the tree
184
- return deleteQueue(qname, qrl, options)
185
- .then(deleteResult => Object.assign(result, {
186
- deleted: deleteResult.deleted,
187
- apiCalls: {
188
- SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
189
- CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
190
- }
191
- }))
192
- }
193
- } else {
194
- if (options.verbose) console.error(chalk.blue('Queue ') + qname.slice(options.prefix.length) + chalk.blue(' has been ') + 'active' + chalk.blue(' in the last ') + options['idle-for'] + chalk.blue(' minutes.'))
177
+ export async function processQueue (qname, qrl, opt) {
178
+ const result = await checkIdle(qname, qrl, opt)
179
+ debug(qname, result)
180
+
181
+ // Queue is active
182
+ if (!result.idle) {
183
+ // Notify and return
184
+ 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.'))
185
+ return result
186
+ }
187
+
188
+ // Queue is idle
189
+ 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.'))
190
+ if (opt.delete) {
191
+ const deleteResult = await deleteQueue(qname, qrl, opt)
192
+ const resultIncludingDelete = Object.assign(result, {
193
+ deleted: deleteResult.deleted,
194
+ apiCalls: {
195
+ SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
196
+ CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
195
197
  }
196
- // End this branch of the tree
197
- return Promise.resolve(result)
198
198
  })
199
+ return resultIncludingDelete
200
+ }
199
201
  }
200
202
 
201
203
  /**
202
204
  * Processes a queue and its fail queue, treating them as a unit.
203
205
  */
204
- function processQueuePair (qname, qrl, options) {
206
+ export async function processQueuePair (qname, qrl, opt) {
205
207
  const isFifo = qname.endsWith('.fifo')
206
- const normalizeOptions = Object.assign({}, options, { fifo: isFifo })
207
- const fqname = qrlCache.normalizeFailQueueName(qname, normalizeOptions)
208
- const fqrl = qrlCache.normalizeFailQueueName(qrl, normalizeOptions)
209
- return checkIdle(qname, qrl, options).then(result => {
210
- debug('result', result)
211
- if (result.idle) {
212
- if (options.verbose) console.error(chalk.blue('Queue ') + qname.slice(options.prefix.length) + chalk.blue(' has been ') + 'idle' + chalk.blue(' for the last ') + options['idle-for'] + chalk.blue(' minutes.'))
213
- // Check fail queue if we get a positive result for normal queue
214
- return checkIdle(fqname, fqrl, options).then(fresult => {
215
- debug('fresult', fresult)
216
- if (fresult.idle) {
217
- if (options.verbose) console.error(chalk.blue('Queue ') + fqname.slice(options.prefix.length) + chalk.blue(' has been ') + 'idle' + chalk.blue(' for the last ') + options['idle-for'] + chalk.blue(' minutes.'))
218
- // Trigger a delete if the user wants it
219
- if (options.delete) {
220
- // End this branch of the tree
221
- return Promise.all([
222
- deleteQueue(qname, qrl, options),
223
- deleteQueue(fqname, fqrl, options)
224
- ]).then(deleteResults =>
225
- deleteResults.reduce((a, b) => Object.assign(a, { apiCalls: {
226
- SQS: a.apiCalls.SQS + b.apiCalls.SQS,
227
- CloudWatch: a.apiCalls.CloudWatch + b.apiCalls.CloudWatch
228
- } }), Object.assign(result, { failq: fresult }, { apiCalls: {
229
- SQS: result.apiCalls.SQS + fresult.apiCalls.SQS,
230
- CloudWatch: result.apiCalls.CloudWatch + fresult.apiCalls.CloudWatch
231
- } }))
232
- )
233
- }
234
- } else {
235
- if (options.verbose) console.error(chalk.blue('Queue ') + fqname.slice(options.prefix.length) + chalk.blue(' has been ') + 'active' + chalk.blue(' in the last ') + options['idle-for'] + chalk.blue(' minutes.'))
236
- }
237
- return Promise.resolve(Object.assign(result, { idle: result.idle && fresult.idle, failq: fresult }, { apiCalls: {
208
+ const normalizeOptions = Object.assign({}, opt, { fifo: isFifo })
209
+ // Generate fail queue name/url
210
+ const fqname = normalizeFailQueueName(qname, normalizeOptions)
211
+ const fqrl = normalizeFailQueueName(qrl, normalizeOptions)
212
+
213
+ // Idle check
214
+ const result = await checkIdle(qname, qrl, opt)
215
+ debug('result', result)
216
+
217
+ // Queue is active
218
+ const active = !result.idle
219
+ if (active) {
220
+ 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.'))
221
+ return result
222
+ }
223
+
224
+ // Queue is idle
225
+ 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.'))
226
+
227
+ // Check fail queue
228
+ try {
229
+ const fresult = await checkIdle(fqname, fqrl, opt)
230
+ debug('fresult', fresult)
231
+ const idleCheckResult = Object.assign(
232
+ result,
233
+ { idle: result.idle && fresult.idle, failq: fresult },
234
+ {
235
+ apiCalls: {
238
236
  SQS: result.apiCalls.SQS + fresult.apiCalls.SQS,
239
237
  CloudWatch: result.apiCalls.CloudWatch + fresult.apiCalls.CloudWatch
240
- } }))
241
- })
242
- .catch(e => {
243
- // Handle the case where the fail queue has been deleted or was never created for some reason
244
- if (e.code === 'AWS.SimpleQueueService.NonExistentQueue') {
245
- if (options.verbose) console.error(chalk.blue('Queue ') + fqname.slice(options.prefix.length) + chalk.blue(' does not exist.'))
246
- if (options.delete) {
247
- return deleteQueue(qname, qrl, options)
248
- .then(deleteResult => Object.assign(result, {
249
- deleted: deleteResult.deleted,
250
- apiCalls: {
251
- SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
252
- CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
253
- }
254
- }))
255
- } else {
256
- return result
257
- }
258
- } else {
259
- throw e
260
- }
261
- })
262
- } else {
263
- if (options.verbose) console.error(chalk.blue('Queue ') + qname.slice(options.prefix.length) + chalk.blue(' has been ') + 'active' + chalk.blue(' in the last ') + options['idle-for'] + chalk.blue(' minutes.'))
238
+ }
239
+ }
240
+ )
241
+
242
+ // Queue is active
243
+ const factive = !fresult.idle
244
+ if (factive) {
245
+ if (opt.verbose) console.error(chalk.blue('Queue ') + fqname.slice(opt.prefix.length) + chalk.blue(' has been ') + 'active' + chalk.blue(' in the last ') + opt.idleFor + chalk.blue(' minutes.'))
246
+ return idleCheckResult
264
247
  }
265
- // End this branch of the tree
266
- return result
267
- })
248
+
249
+ // Queue is idle
250
+ if (opt.verbose) console.error(chalk.blue('Queue ') + fqname.slice(opt.prefix.length) + chalk.blue(' has been ') + 'idle' + chalk.blue(' for the last ') + opt.idleFor + chalk.blue(' minutes.'))
251
+
252
+ // Trigger a delete if the user wants it
253
+ if (!opt.delete) return idleCheckResult
254
+ const [dresult, dfresult] = await Promise.all([
255
+ deleteQueue(qname, qrl, opt),
256
+ deleteQueue(fqname, fqrl, opt)
257
+ ])
258
+ return Object.assign(idleCheckResult, {
259
+ apiCalls: {
260
+ // Sum the SQS calls across all four
261
+ SQS: [result, fresult, dresult, dfresult]
262
+ .map(r => r.apiCalls.SQS)
263
+ .reduce((a, b) => a + b, 0),
264
+ // Sum the CloudWatch calls across all four
265
+ CloudWatch: [result, fresult, dresult, dfresult]
266
+ .map(r => r.apiCalls.CloudWatch)
267
+ .reduce((a, b) => a + b, 0)
268
+ }
269
+ })
270
+ } catch (e) {
271
+ // Handle the case where the fail queue has been deleted or was never
272
+ // created for some reason
273
+ if (!(e instanceof QueueDoesNotExist)) throw e
274
+
275
+ // Fail queue doesn't exist if we get here
276
+ if (opt.verbose) console.error(chalk.blue('Queue ') + fqname.slice(opt.prefix.length) + chalk.blue(' does not exist.'))
277
+
278
+ // Handle delete
279
+ if (!opt.delete) return result
280
+ const deleteResult = await deleteQueue(qname, qrl, opt)
281
+ const resultIncludingDelete = Object.assign(result, {
282
+ deleted: deleteResult.deleted,
283
+ apiCalls: {
284
+ SQS: result.apiCalls.SQS + deleteResult.apiCalls.SQS,
285
+ CloudWatch: result.apiCalls.CloudWatch + deleteResult.apiCalls.CloudWatch
286
+ }
287
+ })
288
+ return resultIncludingDelete
289
+ }
268
290
  }
269
291
 
270
292
  //
271
293
  // Resolve queues for listening loop listen
272
294
  //
273
- exports.idleQueues = function idleQueues (queues, options) {
274
- if (options.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
275
- const qnames = queues.map(function (queue) { return options.prefix + queue })
276
- return qrlCache
277
- .getQnameUrlPairs(qnames, options)
278
- .then(function (entries) {
279
- debug('qrlCache.getQnameUrlPairs.then')
280
- if (options.verbose) {
281
- console.error(chalk.blue(' done'))
282
- console.error()
283
- }
295
+ export async function idleQueues (queues, options) {
296
+ const opt = getOptionsWithDefaults(options)
297
+ if (opt.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
298
+ const qnames = queues.map(queue => opt.prefix + queue)
299
+ const entries = await getQnameUrlPairs(qnames, opt)
300
+ debug('getQnameUrlPairs.then')
301
+ if (opt.verbose) {
302
+ console.error(chalk.blue(' done'))
303
+ console.error()
304
+ }
284
305
 
285
- // Filter out any queue ending in suffix unless --include-failed is set
286
- entries = entries
287
- .filter(function (entry) {
288
- const suf = options['fail-suffix']
289
- const sufFifo = options['fail-suffix'] + qrlCache.fifoSuffix
290
- const isFail = entry.qname.endsWith(suf)
291
- const isFifoFail = entry.qname.endsWith(sufFifo)
292
- return options['include-failed'] ? true : (!isFail && !isFifoFail)
293
- })
306
+ // Filter out any queue ending in suffix unless --include-failed is set
307
+ const filteredEntries = entries.filter(entry => {
308
+ const suf = opt.failSuffix
309
+ const sufFifo = opt.failSuffix + fifoSuffix
310
+ const isFail = entry.qname.endsWith(suf)
311
+ const isFifoFail = entry.qname.endsWith(sufFifo)
312
+ return opt.includeFailed ? true : (!isFail && !isFifoFail)
313
+ })
294
314
 
295
- // But only if we have queues to remove
296
- if (entries.length) {
297
- if (options.verbose) {
298
- console.error(chalk.blue('Checking queues (in this order):'))
299
- console.error(entries.map(e =>
300
- ' ' + e.qname.slice(options.prefix.length) + chalk.blue(' - ' + e.qrl)
301
- ).join('\n'))
302
- console.error()
303
- }
304
- // Check each queue in parallel
305
- if (options.unpair) return Promise.all(entries.map(e => processQueue(e.qname, e.qrl, options)))
306
- return Promise.all(entries.map(e => processQueuePair(e.qname, e.qrl, options)))
307
- }
315
+ // But only if we have queues to remove
316
+ if (filteredEntries.length) {
317
+ if (opt.verbose) {
318
+ console.error(chalk.blue('Checking queues (in this order):'))
319
+ console.error(filteredEntries.map(e =>
320
+ ' ' + e.qname.slice(opt.prefix.length) + chalk.blue(' - ' + e.qrl)
321
+ ).join('\n'))
322
+ console.error()
323
+ }
324
+ // Check each queue in parallel
325
+ if (opt.unpair) return Promise.all(filteredEntries.map(e => processQueue(e.qname, e.qrl, opt)))
326
+ return Promise.all(filteredEntries.map(e => processQueuePair(e.qname, e.qrl, opt)))
327
+ }
308
328
 
309
- // Otherwise, let caller know
310
- return Promise.resolve('noQueues')
311
- })
329
+ // Otherwise, let caller know
330
+ return 'noQueues'
312
331
  }
313
332
 
314
333
  debug('loaded')
package/src/monitor.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Multi-queue monitoring functionaliry
3
+ */
4
+
5
+ import { getMatchingQueues, getQueueAttributes } from './sqs.js'
6
+ import Debug from 'debug'
7
+ const debug = Debug('sd:utils:qmonitor:index')
8
+
9
+ /**
10
+ * Splits a queue name with a single wildcard into prefix and suffix regex.
11
+ */
12
+ export function interpretWildcard (queueName) {
13
+ const [prefix, suffix] = queueName.split('*')
14
+ // Strip anything that could cause backreferences
15
+ const safeSuffix = (suffix || '').replace(/[^a-zA-Z0-9_.]+/g, '').replace(/\./g, '\\.')
16
+ const suffixRegex = new RegExp(`${safeSuffix}$`)
17
+ // debug({ prefix, suffix, safeSuffix, suffixRegex })
18
+ return { prefix, suffix, safeSuffix, suffixRegex }
19
+ }
20
+
21
+ /**
22
+ * Aggregates inmportant attributes across queues and reports a summary.
23
+ * Attributes:
24
+ * - ApproximateNumberOfMessages: Sum
25
+ * - ApproximateNumberOfMessagesDelayed: Sum
26
+ * - ApproximateNumberOfMessagesNotVisible: Sum
27
+ */
28
+ export async function getAggregateData (queueName) {
29
+ const { prefix, suffixRegex } = interpretWildcard(queueName)
30
+ const qrls = await getMatchingQueues(prefix, suffixRegex)
31
+ // debug({ qrls })
32
+ const data = await getQueueAttributes(qrls)
33
+ // debug({ data })
34
+ const total = { totalQueues: 0, contributingQueueNames: new Set() }
35
+ for (const { queue, result } of data) {
36
+ // debug({ row })
37
+ total.totalQueues++
38
+ for (const key in result.Attributes) {
39
+ const newAtrribute = parseInt(result.Attributes[key], 10)
40
+ if (newAtrribute > 0) {
41
+ total.contributingQueueNames.add(queue)
42
+ total[key] = (total[key] || 0) + newAtrribute
43
+ }
44
+ }
45
+ }
46
+ // debug({ total })
47
+ // convert set to array
48
+ total.contributingQueueNames = [...total.contributingQueueNames.values()]
49
+ total.queueName = queueName
50
+ return total
51
+ }
52
+
53
+ debug('loaded')