qdone 1.6.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.
Files changed (43) hide show
  1. package/README.md +9 -1
  2. package/commonjs/index.js +10 -0
  3. package/commonjs/package.json +3 -0
  4. package/commonjs/src/cache.js +142 -0
  5. package/commonjs/src/cloudWatch.js +148 -0
  6. package/commonjs/src/consumer.js +483 -0
  7. package/commonjs/src/defaults.js +107 -0
  8. package/commonjs/src/enqueue.js +498 -0
  9. package/commonjs/src/idleQueues.js +466 -0
  10. package/commonjs/src/qrlCache.js +250 -0
  11. package/commonjs/src/sqs.js +160 -0
  12. package/npm-shrinkwrap.json +17598 -264
  13. package/package.json +41 -29
  14. package/src/bin.js +3 -0
  15. package/src/cache.js +21 -25
  16. package/src/cli.js +269 -181
  17. package/src/cloudWatch.js +97 -0
  18. package/src/consumer.js +346 -0
  19. package/src/defaults.js +114 -0
  20. package/src/enqueue.js +239 -196
  21. package/src/idleQueues.js +242 -223
  22. package/src/monitor.js +53 -0
  23. package/src/qrlCache.js +110 -83
  24. package/src/sentry.js +30 -0
  25. package/src/sqs.js +73 -0
  26. package/src/worker.js +197 -202
  27. package/.DS_Store +0 -0
  28. package/.coveralls.yml +0 -2
  29. package/.travis.yml +0 -17
  30. package/CHANGELOG.md +0 -107
  31. package/dump.rdb +0 -0
  32. package/index.js +0 -6
  33. package/package-lock.json.old +0 -3939
  34. package/qdone +0 -2
  35. package/test/fixtures/test-child-kill-linux.sh +0 -9
  36. package/test/fixtures/test-fifo01-x24.batch +0 -24
  37. package/test/fixtures/test-too-big-1.batch +0 -10
  38. package/test/fixtures/test-unique01-x24.batch +0 -24
  39. package/test/fixtures/test-unique02-x24.batch +0 -24
  40. package/test/fixtures/test-unique24-x24.batch +0 -24
  41. package/test/fixtures/test-unique24-x240.batch +0 -240
  42. package/test/test.cache.js +0 -61
  43. package/test/test.cli.js +0 -1609
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Functions that deal with CloudWatch
3
+ */
4
+
5
+ import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'
6
+ import Debug from 'debug'
7
+ const debug = Debug('qdone:cloudWatch')
8
+
9
+ /**
10
+ * Utility function to return an instantiated, shared CloudWatchClient.
11
+ */
12
+ let client
13
+ export function getCloudWatchClient () {
14
+ if (client) return client
15
+ client = new CloudWatchClient()
16
+ return client
17
+ }
18
+
19
+ /**
20
+ * Utility function to set the client explicitly, used in testing.
21
+ */
22
+ export function setCloudWatchClient (explicitClient) {
23
+ client = explicitClient
24
+ }
25
+
26
+ /**
27
+ * Takes data in the form returned by getAggregageData() and pushes it to
28
+ * CloudWatch metrics under the given queueName.
29
+ *
30
+ * @param queueName {String} - The name of the wildcard queue these metrics are for.
31
+ * @param total {Object} - returned object from getAggregateData()
32
+ */
33
+ export async function putAggregateData (total, timestamp) {
34
+ const client = getCloudWatchClient()
35
+ const now = timestamp || new Date()
36
+ const input = {
37
+ Namespace: 'qmonitor',
38
+ MetricData: [
39
+ {
40
+ MetricName: 'totalQueues',
41
+ Dimensions: [{
42
+ Name: 'queueName',
43
+ Value: total.queueName
44
+ }],
45
+ Timestamp: now,
46
+ Value: total.totalQueues,
47
+ Unit: 'Count'
48
+ },
49
+ {
50
+ MetricName: 'contributingQueueCount',
51
+ Dimensions: [{
52
+ Name: 'queueName',
53
+ Value: total.queueName
54
+ }],
55
+ Timestamp: now,
56
+ Value: total.contributingQueueNames.length,
57
+ Unit: 'Count'
58
+ },
59
+ {
60
+ MetricName: 'ApproximateNumberOfMessages',
61
+ Dimensions: [{
62
+ Name: 'queueName',
63
+ Value: total.queueName
64
+ }],
65
+ Timestamp: now,
66
+ Value: total.ApproximateNumberOfMessages || 0,
67
+ Unit: 'Count'
68
+ },
69
+ {
70
+ MetricName: 'ApproximateNumberOfMessagesDelayed',
71
+ Dimensions: [{
72
+ Name: 'queueName',
73
+ Value: total.queueName
74
+ }],
75
+ Timestamp: now,
76
+ Value: total.ApproximateNumberOfMessagesDelayed || 0,
77
+ Unit: 'Count'
78
+ },
79
+ {
80
+ MetricName: 'ApproximateNumberOfMessagesNotVisible',
81
+ Dimensions: [{
82
+ Name: 'queueName',
83
+ Value: total.queueName
84
+ }],
85
+ Timestamp: now,
86
+ Value: total.ApproximateNumberOfMessagesNotVisible || 0,
87
+ Unit: 'Count'
88
+ }
89
+ ]
90
+ }
91
+ const command = new PutMetricDataCommand(input)
92
+ // debug({ input, command })
93
+ const response = await client.send(command)
94
+ debug({ response })
95
+ }
96
+
97
+ debug('loaded')
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Consumer implementation.
3
+ */
4
+
5
+ import {
6
+ ChangeMessageVisibilityCommand,
7
+ ReceiveMessageCommand,
8
+ DeleteMessageCommand
9
+ } from '@aws-sdk/client-sqs'
10
+ import chalk from 'chalk'
11
+ import Debug from 'debug'
12
+
13
+ import { normalizeQueueName, getQnameUrlPairs } from './qrlCache.js'
14
+ import { cheapIdleCheck } from './idleQueues.js'
15
+ import { getOptionsWithDefaults } from './defaults.js'
16
+ import { getSQSClient } from './sqs.js'
17
+
18
+ //
19
+ // Throwing an instance of this Error allows the processMessages callback to
20
+ // refuse a message which then gets immediately returned to the queue.
21
+ //
22
+ // This has the side effect of throtting the queue since it stops polling on
23
+ // the queue until the next queue resolution in processMessages.
24
+ //
25
+ // This is useful for implementing schedulers on top of qdone, for example, to
26
+ // look at the queue name and decide whether to take on a new message.
27
+ //
28
+ export class DoNotProcess extends Error {}
29
+
30
+ const debug = Debug('qdone:worker')
31
+
32
+ // Global flag for shutdown request
33
+ let shutdownRequested = false
34
+ const shutdownCallbacks = []
35
+
36
+ export function requestShutdown () {
37
+ shutdownRequested = true
38
+ for (const callback of shutdownCallbacks) {
39
+ try { callback() } catch (e) { }
40
+ }
41
+ }
42
+
43
+ export async function processMessage (message, callback, qname, qrl, opt) {
44
+ debug('processMessage', message, qname, qrl)
45
+ const payload = JSON.parse(message.Body)
46
+ if (opt.verbose) {
47
+ console.error(chalk.blue(' Processing payload:'), payload)
48
+ } else if (!opt.disableLog) {
49
+ console.log(JSON.stringify({
50
+ event: 'MESSAGE_PROCESSING_START',
51
+ timestamp: new Date(),
52
+ messageId: message.MessageId,
53
+ payload
54
+ }))
55
+ }
56
+
57
+ const jobStart = new Date()
58
+ let visibilityTimeout = 30 // this should be the queue timeout
59
+ let timeoutExtender
60
+
61
+ async function extendTimeout () {
62
+ debug('extendTimeout')
63
+ const maxJobRun = 12 * 60 * 60
64
+ const jobRunTime = ((new Date()) - jobStart) / 1000
65
+ // Double every time, up to max
66
+ visibilityTimeout = Math.min(visibilityTimeout * 2, maxJobRun - jobRunTime, opt.killAfter - jobRunTime)
67
+ if (opt.verbose) {
68
+ console.error(
69
+ chalk.blue(' Ran for ') + jobRunTime +
70
+ chalk.blue(' seconds, requesting another ') + visibilityTimeout +
71
+ chalk.blue(' seconds')
72
+ )
73
+ }
74
+
75
+ try {
76
+ const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
77
+ QueueUrl: qrl,
78
+ ReceiptHandle: message.ReceiptHandle,
79
+ VisibilityTimeout: visibilityTimeout
80
+ }))
81
+ debug('ChangeMessageVisibility returned', result)
82
+ if (
83
+ jobRunTime + visibilityTimeout >= maxJobRun ||
84
+ jobRunTime + visibilityTimeout >= opt.killAfter
85
+ ) {
86
+ if (opt.verbose) console.error(chalk.yellow(' warning: this is our last time extension'))
87
+ } else {
88
+ // Extend when we get 50% of the way to timeout
89
+ timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
90
+ }
91
+ } catch (err) {
92
+ debug('ChangeMessageVisibility threw', err)
93
+ // Rejection means we're ouuta time, whatever, let the job die
94
+ if (opt.verbose) {
95
+ console.error(chalk.red(' failed to extend job: ') + err)
96
+ } else if (!opt.disableLog) {
97
+ // Production error logging
98
+ console.log(JSON.stringify({
99
+ event: 'MESSAGE_PROCESSING_FAILED',
100
+ reason: 'ran longer than --kill-after',
101
+ timestamp: new Date(),
102
+ messageId: message.MessageId,
103
+ payload,
104
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
105
+ err
106
+ }))
107
+ }
108
+ }
109
+ }
110
+
111
+ // Extend when we get 50% of the way to timeout
112
+ timeoutExtender = setTimeout(extendTimeout, visibilityTimeout * 1000 * 0.5)
113
+ debug('timeout', visibilityTimeout * 1000 * 0.5)
114
+
115
+ try {
116
+ // Process message
117
+ const queue = qname.slice(opt.prefix.length)
118
+ const result = await callback(queue, payload)
119
+ debug('processMessage callback finished', { payload, result })
120
+ clearTimeout(timeoutExtender)
121
+ if (opt.verbose) {
122
+ console.error(chalk.green(' SUCCESS'))
123
+ console.error(chalk.blue(' cleaning up (removing message) ...'))
124
+ }
125
+ await getSQSClient().send(new DeleteMessageCommand({
126
+ QueueUrl: qrl,
127
+ ReceiptHandle: message.ReceiptHandle
128
+ }))
129
+ if (opt.verbose) {
130
+ console.error(chalk.blue(' done'))
131
+ console.error()
132
+ } else if (!opt.disableLog) {
133
+ console.log(JSON.stringify({
134
+ event: 'MESSAGE_PROCESSING_COMPLETE',
135
+ timestamp: new Date(),
136
+ messageId: message.MessageId,
137
+ payload
138
+ }))
139
+ }
140
+ return { noJobs: 0, jobsSucceeded: 1, jobsFailed: 0 }
141
+ } catch (err) {
142
+ debug('exec.catch')
143
+ clearTimeout(timeoutExtender)
144
+
145
+ // If the callback does not want to process this message, return to queue
146
+ if (err instanceof DoNotProcess) {
147
+ if (opt.verbose) {
148
+ console.error(chalk.blue(' callback ') + chalk.yellow('REFUSED'))
149
+ console.error(chalk.blue(' cleaning up (removing message) ...'))
150
+ }
151
+ const result = await getSQSClient().send(new ChangeMessageVisibilityCommand({
152
+ QueueUrl: qrl,
153
+ ReceiptHandle: message.ReceiptHandle,
154
+ VisibilityTimeout: 0
155
+ }))
156
+ debug('ChangeMessageVisibility returned', result)
157
+ return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
158
+ }
159
+
160
+ // Fail path for job execution
161
+ if (opt.verbose) {
162
+ console.error(chalk.red(' FAILED'))
163
+ console.error(chalk.blue(' error : ') + err)
164
+ } else if (!opt.disableLog) {
165
+ // Production error logging
166
+ console.log(JSON.stringify({
167
+ event: 'MESSAGE_PROCESSING_FAILED',
168
+ reason: 'exception thrown',
169
+ timestamp: new Date(),
170
+ messageId: message.MessageId,
171
+ payload,
172
+ errorMessage: err.toString().split('\n').slice(1).join('\n').trim() || undefined,
173
+ err
174
+ }))
175
+ }
176
+ return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 1 }
177
+ }
178
+ }
179
+
180
+ //
181
+ // Pull work off of a single queue
182
+ //
183
+ export async function pollSingleQueue (qname, qrl, callback, opt) {
184
+ debug('pollSingleQueue', { qname, qrl, callback, opt })
185
+ const params = {
186
+ AttributeNames: ['All'],
187
+ MaxNumberOfMessages: 1,
188
+ MessageAttributeNames: ['All'],
189
+ QueueUrl: qrl,
190
+ VisibilityTimeout: 30,
191
+ WaitTimeSeconds: opt.waitTime
192
+ }
193
+ const response = await getSQSClient().send(new ReceiveMessageCommand(params))
194
+ debug('ReceiveMessage response', response)
195
+ if (shutdownRequested) return { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
196
+ if (response.Messages) {
197
+ const message = response.Messages[0]
198
+ if (opt.verbose) console.error(chalk.blue(' Found message ' + message.MessageId))
199
+ return processMessage(message, callback, qname, qrl, opt)
200
+ } else {
201
+ return { noJobs: 1, jobsSucceeded: 0, jobsFailed: 0 }
202
+ }
203
+ }
204
+
205
+ //
206
+ // Resolve a set of queues
207
+ //
208
+ export async function resolveQueues (queues, opt) {
209
+ // Start processing
210
+ if (opt.verbose) console.error(chalk.blue('Resolving queues: ') + queues.join(' '))
211
+ const qnames = queues.map(queue => normalizeQueueName(queue, opt))
212
+ const pairs = await getQnameUrlPairs(qnames, opt)
213
+
214
+ // Figure out which pairs are active
215
+ const activePairs = []
216
+ if (opt.activeOnly) {
217
+ debug({ pairsBeforeCheck: pairs })
218
+ await Promise.all(pairs.map(async pair => {
219
+ const { idle } = await cheapIdleCheck(pair.qname, pair.qrl, opt)
220
+ if (!idle) activePairs.push(pair)
221
+ }))
222
+ }
223
+
224
+ // Finished resolving
225
+ debug('getQnameUrlPairs.then')
226
+ if (opt.verbose) {
227
+ console.error(chalk.blue(' done'))
228
+ console.error()
229
+ }
230
+
231
+ // Figure out which queues we want to listen on, choosing between active and
232
+ // all, filtering out failed queues if the user wants that
233
+ const selectedPairs = (opt.activeOnly ? activePairs : pairs)
234
+ .filter(({ qname }) => {
235
+ const suf = opt.failSuffix + (opt.fifo ? '.fifo' : '')
236
+ const isFailQueue = qname.slice(-suf.length) === suf
237
+ const shouldInclude = opt.includeFailed ? true : !isFailQueue
238
+ return shouldInclude
239
+ })
240
+
241
+ return selectedPairs
242
+ }
243
+
244
+ //
245
+ // Consumer
246
+ //
247
+ export async function processMessages (queues, callback, options) {
248
+ const opt = getOptionsWithDefaults(options)
249
+ debug('processMessages', { queues, callback, options, opt })
250
+
251
+ const stats = { noJobs: 0, jobsSucceeded: 0, jobsFailed: 0 }
252
+ const activeLoops = {}
253
+
254
+ // This delay function keeps a timeout reference around so it can be
255
+ // cancelled at shutdown
256
+ let delayTimeout
257
+ const delay = (ms) => new Promise(resolve => {
258
+ delayTimeout = setTimeout(resolve, ms)
259
+ })
260
+
261
+ // Callback to help facilitate better UX at shutdown
262
+ function shutdownCallback () {
263
+ if (opt.verbose) {
264
+ debug({ activeLoops })
265
+ const activeQueues = Object.keys(activeLoops).filter(q => activeLoops[q]).map(q => q.slice(opt.prefix.length))
266
+ if (activeQueues.length) {
267
+ console.error(chalk.blue('Waiting for work to finish on the following queues: ') + activeQueues.join(chalk.blue(', ')))
268
+ }
269
+ clearTimeout(delayTimeout)
270
+ }
271
+ }
272
+ shutdownCallbacks.push(shutdownCallback)
273
+
274
+ // Listen to a queue until it is out of messages
275
+ async function listenLoop (qname, qrl) {
276
+ try {
277
+ if (shutdownRequested) return
278
+ if (opt.verbose) {
279
+ console.error(
280
+ chalk.blue('Looking for work on ') +
281
+ qname.slice(opt.prefix.length) +
282
+ chalk.blue(' (' + qrl + ')')
283
+ )
284
+ }
285
+ // Aggregate the results
286
+ const { noJobs, jobsSucceeded, jobsFailed } = await pollSingleQueue(qname, qrl, callback, opt)
287
+ stats.noJobs += noJobs
288
+ stats.jobsFailed += jobsFailed
289
+ stats.jobsSucceeded += jobsSucceeded
290
+
291
+ // No work? return to outer loop
292
+ if (noJobs) return
293
+
294
+ // Otherwise keep going
295
+ return listenLoop(qname, qrl)
296
+ } catch (err) {
297
+ // TODO: Sentry
298
+ console.error(chalk.red(' ERROR in listenLoop'))
299
+ console.error(chalk.blue(' error : ') + err)
300
+ } finally {
301
+ delete activeLoops[qname]
302
+ }
303
+ }
304
+
305
+ // Resolve loop
306
+ while (!shutdownRequested) { // eslint-disable-line
307
+ const start = new Date()
308
+ const selectedPairs = await resolveQueues(queues, opt)
309
+ if (shutdownRequested) break
310
+
311
+ // But only if we have queues to listen on
312
+ if (selectedPairs.length) {
313
+ // Randomize order
314
+ selectedPairs.sort(() => 0.5 - Math.random())
315
+
316
+ if (opt.verbose) {
317
+ console.error(chalk.blue('Listening to queues (in this order):'))
318
+ console.error(selectedPairs.map(({ qname, qrl }) =>
319
+ ' ' + qname.slice(opt.prefix.length) + chalk.blue(' - ' + qrl)
320
+ ).join('\n'))
321
+ console.error()
322
+ }
323
+
324
+ // Launch listen loop for each queue
325
+ for (const { qname, qrl } of selectedPairs) {
326
+ if (!activeLoops[qname]) activeLoops[qname] = listenLoop(qname, qrl)
327
+ }
328
+ }
329
+ // Wait until the next time we need to resolve
330
+ if (!shutdownRequested) {
331
+ const msSoFar = Math.max(0, new Date() - start)
332
+ const msUntilNextResolve = Math.max(0, opt.waitTime * 1000 - msSoFar)
333
+ debug({ msSoFar, msUntilNextResolve })
334
+ if (msUntilNextResolve) {
335
+ if (opt.verbose) console.error(chalk.blue('Will resolve queues again in ' + Math.round(msUntilNextResolve / 1000) + ' seconds'))
336
+ await delay(msUntilNextResolve)
337
+ }
338
+ }
339
+ }
340
+
341
+ // Wait on all work to finish
342
+ // shutdownCallback()
343
+ await Promise.all(Object.values(activeLoops))
344
+ }
345
+
346
+ debug('loaded')
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Default options for qdone. Accepts a command line options object and
3
+ * returns nicely-named options.
4
+ */
5
+ import { v1 as uuidv1 } from 'uuid'
6
+
7
+ export const defaults = Object.freeze({
8
+ // Shared
9
+ prefix: 'qdone_',
10
+ failSuffix: '_failed',
11
+ region: 'us-east-1',
12
+ quiet: false,
13
+ verbose: false,
14
+ cache: false,
15
+ cacheUri: undefined,
16
+ cachePrefix: 'qdone:',
17
+ cacheTtlSeconds: 10,
18
+ fifo: false,
19
+ disableLog: false,
20
+ includeFailed: false,
21
+
22
+ // Enqueue
23
+ groupId: uuidv1(),
24
+ groupIdPerMessage: false,
25
+ deduplicationId: uuidv1(),
26
+ messageRetentionPeriod: 1209600,
27
+ delay: 0,
28
+ dlq: false,
29
+ dlqSuffix: '_dead',
30
+ dlqAfter: 3,
31
+
32
+ // Worker
33
+ waitTime: 20,
34
+ killAfter: 30,
35
+ archive: false,
36
+ activeOnly: false,
37
+
38
+ // Idle Queues
39
+ idleFor: 60,
40
+ delete: false,
41
+ unpair: false
42
+ })
43
+
44
+ /**
45
+ * This function should be called by each exposed API entry point on the
46
+ * options passed in from the caller. It supports options named in camelCase
47
+ * and also in command-line-style returned by our parsers.
48
+ *
49
+ * By convention, we name the variable "options" if it comes from the user
50
+ * and "opt" if it has already passed through this function.
51
+ */
52
+ export function getOptionsWithDefaults (options) {
53
+ // For API invocations don't force caller to supply default options
54
+ if (!options) options = {}
55
+
56
+ // Activate DLQ if any option is set
57
+ const dlq = options.dlq || !!(options['dlq-suffix'] || options['dlq-after'] || options['dlq-name'])
58
+
59
+ const opt = {
60
+ // Shared
61
+ prefix: options.prefix === '' ? options.prefix : (options.prefix || defaults.prefix),
62
+ failSuffix: options.failSuffix || options['fail-suffix'] || defaults.failSuffix,
63
+ region: options.region || process.env.AWS_REGION || defaults.region,
64
+ quiet: options.quiet || defaults.quiet,
65
+ verbose: options.verbose || defaults.verbose,
66
+ fifo: options.fifo || defaults.fifo,
67
+ sentryDsn: options.sentryDsn || options['sentry-dsn'],
68
+ disableLog: options.disableLog || options['disable-log'] || defaults.disableLog,
69
+
70
+ // Cache
71
+ cacheUri: options.cacheUri || options['cache-uri'] || defaults.cacheUri,
72
+ cachePrefix: options.cachePrefix || options['cache-prefix'] || defaults.cachePrefix,
73
+ cacheTtlSeconds: options.cacheTtlSeconds || options['cache-ttl-seconds'] || defaults.cacheTtlSeconds,
74
+
75
+ // Enqueue
76
+ groupId: options.groupId || options['group-id'] || defaults.groupId,
77
+ groupIdPerMessage: false,
78
+ deduplicationId: options.deduplicationId || options['deduplication-id'] || defaults.deduplicationId,
79
+ messageRetentionPeriod: options.messageRetentionPeriod || options['message-retention-period'] || defaults.messageRetentionPeriod,
80
+ delay: options.delay || defaults.delay,
81
+ dlq: dlq || defaults.dlq,
82
+ dlqSuffix: options.dlqSuffix || options['dlq-suffix'] || defaults.dlqSuffix,
83
+ dlqAfter: options.dlqAfter || options['dlq-after'] || defaults.dlqAfter,
84
+ tags: options.tags || undefined,
85
+
86
+ // Worker
87
+ waitTime: options.waitTime || options['wait-time'] || defaults.waitTime,
88
+ killAfter: options.killAfter || options['kill-after'] || defaults.killAfter,
89
+ archive: options.archive || defaults.archive,
90
+ activeOnly: options.activeOnly || options['active-only'] || defaults.activeOnly,
91
+ includeFailed: options.includeFailed || options['include-failed'] || defaults.includeFailed,
92
+
93
+ // Idle Queues
94
+ idleFor: options.idleFor || options['idle-for'] || defaults.idleFor,
95
+ delete: options.delete || defaults.delete,
96
+ unpair: options.delete || defaults.unpair
97
+ }
98
+ process.env.AWS_REGION = opt.region
99
+
100
+ // TODO: validate options
101
+ return opt
102
+ }
103
+
104
+ export function setupAWS (options) {
105
+ const opt = getOptionsWithDefaults(options)
106
+ process.env.AWS_REGION = opt.region
107
+ }
108
+
109
+ export function setupVerbose (options) {
110
+ const verbose = options.verbose || (process.stderr.isTTY && !options.quiet)
111
+ const quiet = options.quiet || (!process.stderr.isTTY && !options.verbose)
112
+ options.verbose = verbose
113
+ options.quiet = quiet
114
+ }