qdone 2.1.1 → 2.2.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/commonjs/src/cache.js +4 -5
- package/commonjs/src/cloudWatch.js +13 -4
- package/commonjs/src/consumer.js +3 -4
- package/commonjs/src/dedup.js +9 -10
- package/commonjs/src/defaults.js +11 -9
- package/commonjs/src/enqueue.js +14 -15
- package/commonjs/src/idleQueues.js +9 -9
- package/commonjs/src/monitor.js +41 -5
- package/commonjs/src/qrlCache.js +11 -11
- package/commonjs/src/scheduler/jobExecutor.js +10 -2
- package/commonjs/src/sqs.js +4 -5
- package/package.json +10 -10
- package/src/cloudWatch.js +10 -0
- package/src/defaults.js +5 -2
- package/src/idleQueues.js +1 -1
- package/src/monitor.js +41 -2
- package/src/scheduler/jobExecutor.js +10 -2
- package/src/worker.js +12 -1
package/commonjs/src/cache.js
CHANGED
|
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getCacheClient = getCacheClient;
|
|
7
|
+
exports.shutdownCache = shutdownCache;
|
|
8
|
+
exports.getCache = getCache;
|
|
9
|
+
exports.setCache = setCache;
|
|
7
10
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
11
|
const url_1 = require("url");
|
|
9
12
|
const debug_1 = __importDefault(require("debug"));
|
|
@@ -38,13 +41,11 @@ function getCacheClient(opt) {
|
|
|
38
41
|
throw new UsageError('Caching requires the --cache-uri option');
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
exports.getCacheClient = getCacheClient;
|
|
42
44
|
function shutdownCache() {
|
|
43
45
|
if (client)
|
|
44
46
|
client.quit();
|
|
45
47
|
client = undefined;
|
|
46
48
|
}
|
|
47
|
-
exports.shutdownCache = shutdownCache;
|
|
48
49
|
/**
|
|
49
50
|
* Returns a promise for the item. Resolves to false if cache is empty, object
|
|
50
51
|
* if it is found.
|
|
@@ -57,7 +58,6 @@ async function getCache(key, opt) {
|
|
|
57
58
|
debug({ action: 'getCache got', cacheKey, result });
|
|
58
59
|
return result ? JSON.parse(result) : undefined;
|
|
59
60
|
}
|
|
60
|
-
exports.getCache = getCache;
|
|
61
61
|
/**
|
|
62
62
|
* Returns a promise for the status. Encodes object as JSON
|
|
63
63
|
*/
|
|
@@ -68,5 +68,4 @@ async function setCache(key, value, opt) {
|
|
|
68
68
|
debug({ action: 'setCache', cacheKey, value });
|
|
69
69
|
return client.setex(cacheKey, opt.cacheTtlSeconds, encoded);
|
|
70
70
|
}
|
|
71
|
-
exports.setCache = setCache;
|
|
72
71
|
debug('loaded');
|
|
@@ -6,7 +6,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
9
|
+
exports.getCloudWatchClient = getCloudWatchClient;
|
|
10
|
+
exports.setCloudWatchClient = setCloudWatchClient;
|
|
11
|
+
exports.putAggregateData = putAggregateData;
|
|
10
12
|
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
11
13
|
const debug_1 = __importDefault(require("debug"));
|
|
12
14
|
const debug = (0, debug_1.default)('qdone:cloudWatch');
|
|
@@ -20,14 +22,12 @@ function getCloudWatchClient() {
|
|
|
20
22
|
client = new client_cloudwatch_1.CloudWatchClient();
|
|
21
23
|
return client;
|
|
22
24
|
}
|
|
23
|
-
exports.getCloudWatchClient = getCloudWatchClient;
|
|
24
25
|
/**
|
|
25
26
|
* Utility function to set the client explicitly, used in testing.
|
|
26
27
|
*/
|
|
27
28
|
function setCloudWatchClient(explicitClient) {
|
|
28
29
|
client = explicitClient;
|
|
29
30
|
}
|
|
30
|
-
exports.setCloudWatchClient = setCloudWatchClient;
|
|
31
31
|
/**
|
|
32
32
|
* Takes data in the form returned by getAggregageData() and pushes it to
|
|
33
33
|
* CloudWatch metrics under the given queueName.
|
|
@@ -90,6 +90,16 @@ async function putAggregateData(total, timestamp) {
|
|
|
90
90
|
Timestamp: now,
|
|
91
91
|
Value: total.ApproximateNumberOfMessagesNotVisible || 0,
|
|
92
92
|
Unit: 'Count'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
MetricName: 'ApproximateAgeOfOldestMessage',
|
|
96
|
+
Dimensions: [{
|
|
97
|
+
Name: 'queueName',
|
|
98
|
+
Value: total.queueName
|
|
99
|
+
}],
|
|
100
|
+
Timestamp: now,
|
|
101
|
+
Value: total.ApproximateAgeOfOldestMessage || 0,
|
|
102
|
+
Unit: 'Seconds'
|
|
93
103
|
}
|
|
94
104
|
]
|
|
95
105
|
};
|
|
@@ -98,5 +108,4 @@ async function putAggregateData(total, timestamp) {
|
|
|
98
108
|
const response = await client.send(command);
|
|
99
109
|
debug({ response });
|
|
100
110
|
}
|
|
101
|
-
exports.putAggregateData = putAggregateData;
|
|
102
111
|
debug('loaded');
|
package/commonjs/src/consumer.js
CHANGED
|
@@ -6,7 +6,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
9
|
+
exports.requestShutdown = requestShutdown;
|
|
10
|
+
exports.getMessages = getMessages;
|
|
11
|
+
exports.processMessages = processMessages;
|
|
10
12
|
const os_1 = require("os");
|
|
11
13
|
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
12
14
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -30,7 +32,6 @@ async function requestShutdown() {
|
|
|
30
32
|
}
|
|
31
33
|
debug('requestShutdown done');
|
|
32
34
|
}
|
|
33
|
-
exports.requestShutdown = requestShutdown;
|
|
34
35
|
async function getMessages(qrl, opt, maxMessages) {
|
|
35
36
|
const params = {
|
|
36
37
|
AttributeNames: ['All'],
|
|
@@ -44,7 +45,6 @@ async function getMessages(qrl, opt, maxMessages) {
|
|
|
44
45
|
// debug('ReceiveMessage response', response)
|
|
45
46
|
return response.Messages || [];
|
|
46
47
|
}
|
|
47
|
-
exports.getMessages = getMessages;
|
|
48
48
|
//
|
|
49
49
|
// Consumer
|
|
50
50
|
//
|
|
@@ -170,4 +170,3 @@ async function processMessages(queues, callback, options) {
|
|
|
170
170
|
}
|
|
171
171
|
debug('after all');
|
|
172
172
|
}
|
|
173
|
-
exports.processMessages = processMessages;
|
package/commonjs/src/dedup.js
CHANGED
|
@@ -3,7 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getDeduplicationId = getDeduplicationId;
|
|
7
|
+
exports.getCacheKey = getCacheKey;
|
|
8
|
+
exports.addDedupParamsToMessage = addDedupParamsToMessage;
|
|
9
|
+
exports.updateStats = updateStats;
|
|
10
|
+
exports.statMaintenance = statMaintenance;
|
|
11
|
+
exports.dedupShouldEnqueue = dedupShouldEnqueue;
|
|
12
|
+
exports.dedupShouldEnqueueMulti = dedupShouldEnqueueMulti;
|
|
13
|
+
exports.dedupSuccessfullyProcessed = dedupSuccessfullyProcessed;
|
|
14
|
+
exports.dedupSuccessfullyProcessedMulti = dedupSuccessfullyProcessedMulti;
|
|
7
15
|
const crypto_1 = require("crypto");
|
|
8
16
|
const uuid_1 = require("uuid");
|
|
9
17
|
const cache_js_1 = require("./cache.js");
|
|
@@ -30,7 +38,6 @@ function getDeduplicationId(dedupContent, opt) {
|
|
|
30
38
|
const id = `sha1:{${hash}}:body:${truncated}`;
|
|
31
39
|
return id;
|
|
32
40
|
}
|
|
33
|
-
exports.getDeduplicationId = getDeduplicationId;
|
|
34
41
|
/**
|
|
35
42
|
* Returns the cache key given a deduplication id.
|
|
36
43
|
* @param {String} dedupId - a deduplication id returned from getDeduplicationId
|
|
@@ -42,7 +49,6 @@ function getCacheKey(dedupId, opt) {
|
|
|
42
49
|
debug({ getCacheKey: { cacheKey } });
|
|
43
50
|
return cacheKey;
|
|
44
51
|
}
|
|
45
|
-
exports.getCacheKey = getCacheKey;
|
|
46
52
|
/**
|
|
47
53
|
* Modifies a message (parameters to SendMessageCommand) to add the parameters
|
|
48
54
|
* for whatever deduplication options the caller has set.
|
|
@@ -88,7 +94,6 @@ function addDedupParamsToMessage(message, opt, messageOptions) {
|
|
|
88
94
|
}
|
|
89
95
|
return message;
|
|
90
96
|
}
|
|
91
|
-
exports.addDedupParamsToMessage = addDedupParamsToMessage;
|
|
92
97
|
/**
|
|
93
98
|
* Updates statistics in redis, of which there are two:
|
|
94
99
|
* 1. duplicateSet - a set who's members are cache keys and scores are the number of duplicate
|
|
@@ -113,7 +118,6 @@ async function updateStats(cacheKey, duplicates, expireAt, opt, pipeline) {
|
|
|
113
118
|
await pipeline.exec();
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
exports.updateStats = updateStats;
|
|
117
121
|
/**
|
|
118
122
|
* Removes expired items from stats.
|
|
119
123
|
*/
|
|
@@ -135,7 +139,6 @@ async function statMaintenance(opt) {
|
|
|
135
139
|
debug({ statMaintenance: { result } });
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
|
-
exports.statMaintenance = statMaintenance;
|
|
139
142
|
/**
|
|
140
143
|
* Determines whether we should enqueue this message or whether it is a duplicate.
|
|
141
144
|
* Returns true if enqueuing the message would not result in a duplicate.
|
|
@@ -160,7 +163,6 @@ async function dedupShouldEnqueue(message, opt) {
|
|
|
160
163
|
}
|
|
161
164
|
return false;
|
|
162
165
|
}
|
|
163
|
-
exports.dedupShouldEnqueue = dedupShouldEnqueue;
|
|
164
166
|
/**
|
|
165
167
|
* Determines which messages we should enqueue, returning only those that
|
|
166
168
|
* would not be duplicates.
|
|
@@ -203,7 +205,6 @@ async function dedupShouldEnqueueMulti(messages, opt) {
|
|
|
203
205
|
await statsPipeline.exec();
|
|
204
206
|
return messagesToEnqueue;
|
|
205
207
|
}
|
|
206
|
-
exports.dedupShouldEnqueueMulti = dedupShouldEnqueueMulti;
|
|
207
208
|
/**
|
|
208
209
|
* Marks a message as processed so that subsequent calls to dedupShouldEnqueue
|
|
209
210
|
* and dedupShouldEnqueueMulti will allow a message to be enqueued again
|
|
@@ -229,7 +230,6 @@ async function dedupSuccessfullyProcessed(message, opt) {
|
|
|
229
230
|
}
|
|
230
231
|
return 0;
|
|
231
232
|
}
|
|
232
|
-
exports.dedupSuccessfullyProcessed = dedupSuccessfullyProcessed;
|
|
233
233
|
/**
|
|
234
234
|
* Marks an array of messages as processed so that subsequent calls to
|
|
235
235
|
* dedupShouldEnqueue and dedupShouldEnqueueMulti will allow a message to be
|
|
@@ -263,4 +263,3 @@ async function dedupSuccessfullyProcessedMulti(messages, opt) {
|
|
|
263
263
|
}
|
|
264
264
|
return 0;
|
|
265
265
|
}
|
|
266
|
-
exports.dedupSuccessfullyProcessedMulti = dedupSuccessfullyProcessedMulti;
|
package/commonjs/src/defaults.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.defaults = void 0;
|
|
4
|
+
exports.validateInteger = validateInteger;
|
|
5
|
+
exports.validateQueueName = validateQueueName;
|
|
6
|
+
exports.validateMessageOptions = validateMessageOptions;
|
|
7
|
+
exports.getOptionsWithDefaults = getOptionsWithDefaults;
|
|
8
|
+
exports.setupAWS = setupAWS;
|
|
9
|
+
exports.setupVerbose = setupVerbose;
|
|
4
10
|
/**
|
|
5
11
|
* Default options for qdone. Accepts a command line options object and
|
|
6
12
|
* returns nicely-named options.
|
|
@@ -56,7 +62,6 @@ function validateInteger(opt, name) {
|
|
|
56
62
|
throw new Error(`${name} needs to be an integer.`);
|
|
57
63
|
return parsed;
|
|
58
64
|
}
|
|
59
|
-
exports.validateInteger = validateInteger;
|
|
60
65
|
function validateQueueName(opt, name) {
|
|
61
66
|
if (typeof name !== 'string')
|
|
62
67
|
throw new Error(`${name} must be a string.`);
|
|
@@ -64,9 +69,8 @@ function validateQueueName(opt, name) {
|
|
|
64
69
|
throw new Error(`${name} can contain only numbers, letters, hypens and underscores.`);
|
|
65
70
|
return name;
|
|
66
71
|
}
|
|
67
|
-
exports.validateQueueName = validateQueueName;
|
|
68
72
|
function validateMessageOptions(messageOptions) {
|
|
69
|
-
const validKeys = ['deduplicationId', 'groupId'];
|
|
73
|
+
const validKeys = ['deduplicationId', 'groupId', 'delay'];
|
|
70
74
|
if (typeof messageOptions === 'object' &&
|
|
71
75
|
!Array.isArray(messageOptions) &&
|
|
72
76
|
messageOptions !== null) {
|
|
@@ -78,7 +82,6 @@ function validateMessageOptions(messageOptions) {
|
|
|
78
82
|
}
|
|
79
83
|
return {};
|
|
80
84
|
}
|
|
81
|
-
exports.validateMessageOptions = validateMessageOptions;
|
|
82
85
|
/**
|
|
83
86
|
* This function should be called by each exposed API entry point on the
|
|
84
87
|
* options passed in from the caller. It supports options named in camelCase
|
|
@@ -137,7 +140,9 @@ function getOptionsWithDefaults(options) {
|
|
|
137
140
|
delete: options.delete || process.env.QDONE_DELETE === 'true' || exports.defaults.delete,
|
|
138
141
|
// Check
|
|
139
142
|
create: options.create || process.env.QDONE_CREATE === 'true' || exports.defaults.create,
|
|
140
|
-
overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || exports.defaults.overwrite
|
|
143
|
+
overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || exports.defaults.overwrite,
|
|
144
|
+
// Dependency injection
|
|
145
|
+
Redis: options.Redis
|
|
141
146
|
};
|
|
142
147
|
// Setting this env here means we don't have to in AWS SDK constructors
|
|
143
148
|
process.env.AWS_REGION = opt.region;
|
|
@@ -167,16 +172,13 @@ function getOptionsWithDefaults(options) {
|
|
|
167
172
|
throw new Error('Use either --deduplication-id or --dedup-id-per-message but not both');
|
|
168
173
|
return opt;
|
|
169
174
|
}
|
|
170
|
-
exports.getOptionsWithDefaults = getOptionsWithDefaults;
|
|
171
175
|
function setupAWS(options) {
|
|
172
176
|
const opt = getOptionsWithDefaults(options);
|
|
173
177
|
process.env.AWS_REGION = opt.region;
|
|
174
178
|
}
|
|
175
|
-
exports.setupAWS = setupAWS;
|
|
176
179
|
function setupVerbose(options) {
|
|
177
180
|
const verbose = options.verbose || (process.stderr.isTTY && !options.quiet);
|
|
178
181
|
const quiet = options.quiet || (!process.stderr.isTTY && !options.verbose);
|
|
179
182
|
options.verbose = verbose;
|
|
180
183
|
options.quiet = quiet;
|
|
181
184
|
}
|
|
182
|
-
exports.setupVerbose = setupVerbose;
|
package/commonjs/src/enqueue.js
CHANGED
|
@@ -3,7 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getDLQParams = getDLQParams;
|
|
7
|
+
exports.getOrCreateDLQ = getOrCreateDLQ;
|
|
8
|
+
exports.getFailParams = getFailParams;
|
|
9
|
+
exports.getOrCreateFailQueue = getOrCreateFailQueue;
|
|
10
|
+
exports.getQueueParams = getQueueParams;
|
|
11
|
+
exports.getOrCreateQueue = getOrCreateQueue;
|
|
12
|
+
exports.getQueueAttributes = getQueueAttributes;
|
|
13
|
+
exports.formatMessage = formatMessage;
|
|
14
|
+
exports.sendMessage = sendMessage;
|
|
15
|
+
exports.sendMessageBatch = sendMessageBatch;
|
|
16
|
+
exports.flushMessages = flushMessages;
|
|
17
|
+
exports.addMessage = addMessage;
|
|
18
|
+
exports.enqueue = enqueue;
|
|
19
|
+
exports.enqueueBatch = enqueueBatch;
|
|
7
20
|
const node_1 = require("@sentry/node");
|
|
8
21
|
const uuid_1 = require("uuid");
|
|
9
22
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -27,7 +40,6 @@ function getDLQParams(queue, opt) {
|
|
|
27
40
|
params.Attributes.FifoQueue = 'true';
|
|
28
41
|
return { dqname, params };
|
|
29
42
|
}
|
|
30
|
-
exports.getDLQParams = getDLQParams;
|
|
31
43
|
async function getOrCreateDLQ(queue, opt) {
|
|
32
44
|
debug('getOrCreateDLQ(', queue, ')');
|
|
33
45
|
const { dqname, params } = getDLQParams(queue, opt);
|
|
@@ -51,7 +63,6 @@ async function getOrCreateDLQ(queue, opt) {
|
|
|
51
63
|
return dqrl;
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
|
-
exports.getOrCreateDLQ = getOrCreateDLQ;
|
|
55
66
|
/**
|
|
56
67
|
* Returns the parameters needed for creating a failed queue. If DLQ options
|
|
57
68
|
* are set, it makes an API call to get this DLQ's ARN.
|
|
@@ -81,7 +92,6 @@ async function getFailParams(queue, opt) {
|
|
|
81
92
|
params.Attributes.FifoQueue = 'true';
|
|
82
93
|
return params;
|
|
83
94
|
}
|
|
84
|
-
exports.getFailParams = getFailParams;
|
|
85
95
|
/**
|
|
86
96
|
* Returns the qrl for the failed queue for the given queue. Creates the queue
|
|
87
97
|
* if it does not exist.
|
|
@@ -124,7 +134,6 @@ async function getOrCreateFailQueue(queue, opt, doesNotExist) {
|
|
|
124
134
|
return fqrl;
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
|
-
exports.getOrCreateFailQueue = getOrCreateFailQueue;
|
|
128
137
|
/**
|
|
129
138
|
* Returns the parameters needed for creating a queue. If fail options
|
|
130
139
|
* are set, it makes an API call to get the fail queue's ARN.
|
|
@@ -150,7 +159,6 @@ async function getQueueParams(queue, opt) {
|
|
|
150
159
|
params.Attributes.FifoQueue = 'true';
|
|
151
160
|
return params;
|
|
152
161
|
}
|
|
153
|
-
exports.getQueueParams = getQueueParams;
|
|
154
162
|
/**
|
|
155
163
|
* Returns a qrl for a queue that either exists or does not
|
|
156
164
|
*/
|
|
@@ -190,7 +198,6 @@ async function getOrCreateQueue(queue, opt) {
|
|
|
190
198
|
return qrl;
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
|
-
exports.getOrCreateQueue = getOrCreateQueue;
|
|
194
201
|
async function getQueueAttributes(qrl) {
|
|
195
202
|
debug('getQueueAttributes(', qrl, ')');
|
|
196
203
|
const client = (0, sqs_js_1.getSQSClient)();
|
|
@@ -201,7 +208,6 @@ async function getQueueAttributes(qrl) {
|
|
|
201
208
|
debug('GetQueueAttributes returned', data);
|
|
202
209
|
return data;
|
|
203
210
|
}
|
|
204
|
-
exports.getQueueAttributes = getQueueAttributes;
|
|
205
211
|
function formatMessage(body, id, opt, messageOptions) {
|
|
206
212
|
const message = { MessageBody: body };
|
|
207
213
|
if (typeof id !== 'undefined')
|
|
@@ -216,7 +222,6 @@ function formatMessage(body, id, opt, messageOptions) {
|
|
|
216
222
|
message.DelaySeconds = messageOptions.delay;
|
|
217
223
|
return message;
|
|
218
224
|
}
|
|
219
|
-
exports.formatMessage = formatMessage;
|
|
220
225
|
// Retry happens within the context of the send functions
|
|
221
226
|
const retryableExceptions = [
|
|
222
227
|
client_sqs_1.RequestThrottled,
|
|
@@ -272,7 +277,6 @@ async function sendMessage(qrl, queue, command, opt, messageOptions) {
|
|
|
272
277
|
debug({ sendMessageResult: result });
|
|
273
278
|
return result;
|
|
274
279
|
}
|
|
275
|
-
exports.sendMessage = sendMessage;
|
|
276
280
|
async function sendMessageBatch(qrl, queue, messages, opt) {
|
|
277
281
|
debug('sendMessageBatch(', qrl, messages.map(e => Object.assign(Object.assign({}, e), { MessageBody: e.MessageBody.slice(0, 10) + '...' })), ')');
|
|
278
282
|
const params = { Entries: messages, QueueUrl: qrl };
|
|
@@ -350,7 +354,6 @@ async function sendMessageBatch(qrl, queue, messages, opt) {
|
|
|
350
354
|
};
|
|
351
355
|
return backoff.run(send, shouldRetry);
|
|
352
356
|
}
|
|
353
|
-
exports.sendMessageBatch = sendMessageBatch;
|
|
354
357
|
let requestCount = 0;
|
|
355
358
|
//
|
|
356
359
|
// Flushes the internal message buffer for qrl.
|
|
@@ -421,7 +424,6 @@ async function flushMessages(qrl, queue, opt, sendBuffer) {
|
|
|
421
424
|
}
|
|
422
425
|
return whileNotEmpty();
|
|
423
426
|
}
|
|
424
|
-
exports.flushMessages = flushMessages;
|
|
425
427
|
//
|
|
426
428
|
// Adds a message to the inernal message buffer for the given qrl.
|
|
427
429
|
// Automaticaly flushes if queue has >= 10 messages.
|
|
@@ -438,7 +440,6 @@ async function addMessage(qrl, queue, command, messageIndex, opt, sendBuffer, me
|
|
|
438
440
|
}
|
|
439
441
|
return { numFlushed: 0, results: [] };
|
|
440
442
|
}
|
|
441
|
-
exports.addMessage = addMessage;
|
|
442
443
|
//
|
|
443
444
|
// Enqueue a single command
|
|
444
445
|
// Returns a promise for the SQS API response.
|
|
@@ -458,7 +459,6 @@ async function enqueue(queue, command, options) {
|
|
|
458
459
|
throw e;
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
|
-
exports.enqueue = enqueue;
|
|
462
462
|
//
|
|
463
463
|
// Enqueue many commands formatted as an array of {queue: ..., command: ...} pairs.
|
|
464
464
|
// Returns a promise for the total number of messages enqueued.
|
|
@@ -517,5 +517,4 @@ async function enqueueBatch(pairs, options) {
|
|
|
517
517
|
throw e;
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
|
-
exports.enqueueBatch = enqueueBatch;
|
|
521
520
|
debug('loaded');
|
|
@@ -3,7 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.attributeNames = void 0;
|
|
7
|
+
exports._cheapIdleCheck = _cheapIdleCheck;
|
|
8
|
+
exports.cheapIdleCheck = cheapIdleCheck;
|
|
9
|
+
exports.getMetric = getMetric;
|
|
10
|
+
exports.checkIdle = checkIdle;
|
|
11
|
+
exports.deleteQueue = deleteQueue;
|
|
12
|
+
exports.processQueueSet = processQueueSet;
|
|
13
|
+
exports.stripSuffixes = stripSuffixes;
|
|
14
|
+
exports.idleQueues = idleQueues;
|
|
7
15
|
/**
|
|
8
16
|
* Implementation of checks and caching of checks to determine if queues are idle.
|
|
9
17
|
*/
|
|
@@ -63,7 +71,6 @@ async function _cheapIdleCheck(qname, qrl, opt) {
|
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
|
-
exports._cheapIdleCheck = _cheapIdleCheck;
|
|
67
74
|
/**
|
|
68
75
|
* Gets queue attributes from the SQS api and assesses whether queue is idle
|
|
69
76
|
* at this immediate moment.
|
|
@@ -91,7 +98,6 @@ async function cheapIdleCheck(qname, qrl, opt) {
|
|
|
91
98
|
return { result, SQS };
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
exports.cheapIdleCheck = cheapIdleCheck;
|
|
95
101
|
/**
|
|
96
102
|
* Gets a single metric from the CloudWatch api.
|
|
97
103
|
*/
|
|
@@ -116,7 +122,6 @@ async function getMetric(qname, qrl, metricName, opt) {
|
|
|
116
122
|
[metricName]: data.Datapoints.map(d => d.Sum).reduce((a, b) => a + b, 0)
|
|
117
123
|
};
|
|
118
124
|
}
|
|
119
|
-
exports.getMetric = getMetric;
|
|
120
125
|
/**
|
|
121
126
|
* Checks if a single queue is idle. First queries the cheap ($0.40/1M) SQS API and
|
|
122
127
|
* continues to the expensive ($10/1M) CloudWatch API, checking to make sure there
|
|
@@ -171,7 +176,6 @@ async function checkIdle(qname, qrl, opt) {
|
|
|
171
176
|
debug('checkIdle stats', stats);
|
|
172
177
|
return stats;
|
|
173
178
|
}
|
|
174
|
-
exports.checkIdle = checkIdle;
|
|
175
179
|
/**
|
|
176
180
|
* Just deletes a queue.
|
|
177
181
|
*/
|
|
@@ -186,7 +190,6 @@ async function deleteQueue(qname, qrl, opt) {
|
|
|
186
190
|
apiCalls: { SQS: 1, CloudWatch: 0 }
|
|
187
191
|
};
|
|
188
192
|
}
|
|
189
|
-
exports.deleteQueue = deleteQueue;
|
|
190
193
|
/**
|
|
191
194
|
* Processes a queue and its fail and delete queue, treating them as a unit.
|
|
192
195
|
*/
|
|
@@ -280,7 +283,6 @@ async function processQueueSet(qname, qrl, opt) {
|
|
|
280
283
|
}
|
|
281
284
|
return result;
|
|
282
285
|
}
|
|
283
|
-
exports.processQueueSet = processQueueSet;
|
|
284
286
|
//
|
|
285
287
|
// Strips failed and dlq suffix from a queue name or URL
|
|
286
288
|
//
|
|
@@ -288,7 +290,6 @@ function stripSuffixes(queueName, opt) {
|
|
|
288
290
|
const suffixFinder = new RegExp(`(${opt.dlqSuffix}|${opt.failSuffix}){1}(|${qrlCache_js_1.fifoSuffix})$`);
|
|
289
291
|
return queueName.replace(suffixFinder, '$2');
|
|
290
292
|
}
|
|
291
|
-
exports.stripSuffixes = stripSuffixes;
|
|
292
293
|
//
|
|
293
294
|
// Resolve queues for listening loop listen
|
|
294
295
|
//
|
|
@@ -329,5 +330,4 @@ async function idleQueues(queues, options) {
|
|
|
329
330
|
// Otherwise, let caller know
|
|
330
331
|
return 'noQueues';
|
|
331
332
|
}
|
|
332
|
-
exports.idleQueues = idleQueues;
|
|
333
333
|
debug('loaded');
|
package/commonjs/src/monitor.js
CHANGED
|
@@ -6,9 +6,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
9
|
+
exports.monitor = monitor;
|
|
10
|
+
exports.interpretWildcard = interpretWildcard;
|
|
11
|
+
exports.getQueueAge = getQueueAge;
|
|
12
|
+
exports.getAggregateData = getAggregateData;
|
|
10
13
|
const sqs_js_1 = require("./sqs.js");
|
|
11
14
|
const cloudWatch_js_1 = require("./cloudWatch.js");
|
|
15
|
+
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
12
16
|
const defaults_js_1 = require("./defaults.js");
|
|
13
17
|
const qrlCache_js_1 = require("./qrlCache.js");
|
|
14
18
|
const debug_1 = __importDefault(require("debug"));
|
|
@@ -32,7 +36,6 @@ async function monitor(queue, save, options) {
|
|
|
32
36
|
process.stderr.write('done\n');
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
|
-
exports.monitor = monitor;
|
|
36
39
|
/**
|
|
37
40
|
* Splits a queue name with a single wildcard into prefix and suffix regex.
|
|
38
41
|
*/
|
|
@@ -44,13 +47,43 @@ function interpretWildcard(queueName) {
|
|
|
44
47
|
// debug({ prefix, suffix, safeSuffix, suffixRegex })
|
|
45
48
|
return { prefix, suffix, safeSuffix, suffixRegex };
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Gets ApproximateAgeOfOldestMessage for a single queue from CloudWatch.
|
|
52
|
+
* This metric is not available via the SQS GetQueueAttributes API.
|
|
53
|
+
*/
|
|
54
|
+
async function getQueueAge(queueName) {
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const params = {
|
|
57
|
+
StartTime: new Date(now.getTime() - 1000 * 60 * 5),
|
|
58
|
+
EndTime: now,
|
|
59
|
+
MetricName: 'ApproximateAgeOfOldestMessage',
|
|
60
|
+
Namespace: 'AWS/SQS',
|
|
61
|
+
Period: 300,
|
|
62
|
+
Dimensions: [{ Name: 'QueueName', Value: queueName }],
|
|
63
|
+
Statistics: ['Maximum']
|
|
64
|
+
};
|
|
65
|
+
const client = (0, cloudWatch_js_1.getCloudWatchClient)();
|
|
66
|
+
const cmd = new client_cloudwatch_1.GetMetricStatisticsCommand(params);
|
|
67
|
+
try {
|
|
68
|
+
const data = await client.send(cmd);
|
|
69
|
+
debug('getQueueAge', queueName, data);
|
|
70
|
+
if (!data.Datapoints || data.Datapoints.length === 0)
|
|
71
|
+
return 0;
|
|
72
|
+
return Math.max(...data.Datapoints.map(d => d.Maximum));
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
debug('getQueueAge error', queueName, e);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
48
79
|
/**
|
|
49
80
|
* Aggregates inmportant attributes across queues and reports a summary.
|
|
50
|
-
* Attributes:
|
|
81
|
+
* Attributes (from SQS GetQueueAttributes):
|
|
51
82
|
* - ApproximateNumberOfMessages: Sum
|
|
52
83
|
* - ApproximateNumberOfMessagesDelayed: Sum
|
|
53
84
|
* - ApproximateNumberOfMessagesNotVisible: Sum
|
|
85
|
+
* Metrics (from CloudWatch):
|
|
86
|
+
* - ApproximateAgeOfOldestMessage: Max
|
|
54
87
|
*/
|
|
55
88
|
async function getAggregateData(queueName) {
|
|
56
89
|
const { prefix, suffixRegex } = interpretWildcard(queueName);
|
|
@@ -70,11 +103,14 @@ async function getAggregateData(queueName) {
|
|
|
70
103
|
}
|
|
71
104
|
}
|
|
72
105
|
}
|
|
106
|
+
// Fetch ApproximateAgeOfOldestMessage from CloudWatch (not available via SQS API)
|
|
107
|
+
// Only query queues with messages to minimize CloudWatch API costs
|
|
108
|
+
const ageResults = await Promise.all([...total.contributingQueueNames].map(queue => getQueueAge(queue)));
|
|
109
|
+
total.ApproximateAgeOfOldestMessage = Math.max(0, ...ageResults);
|
|
73
110
|
// debug({ total })
|
|
74
111
|
// convert set to array
|
|
75
112
|
total.contributingQueueNames = [...total.contributingQueueNames.values()];
|
|
76
113
|
total.queueName = queueName;
|
|
77
114
|
return total;
|
|
78
115
|
}
|
|
79
|
-
exports.getAggregateData = getAggregateData;
|
|
80
116
|
debug('loaded');
|
package/commonjs/src/qrlCache.js
CHANGED
|
@@ -7,7 +7,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.
|
|
10
|
+
exports.fifoSuffix = void 0;
|
|
11
|
+
exports.normalizeQueueName = normalizeQueueName;
|
|
12
|
+
exports.normalizeFailQueueName = normalizeFailQueueName;
|
|
13
|
+
exports.normalizeDLQName = normalizeDLQName;
|
|
14
|
+
exports.qrlCacheClear = qrlCacheClear;
|
|
15
|
+
exports.qrlCacheGet = qrlCacheGet;
|
|
16
|
+
exports.qrlCacheSet = qrlCacheSet;
|
|
17
|
+
exports.qrlCacheInvalidate = qrlCacheInvalidate;
|
|
18
|
+
exports.ingestQRLs = ingestQRLs;
|
|
19
|
+
exports.getMatchingQueues = getMatchingQueues;
|
|
20
|
+
exports.getQnameUrlPairs = getQnameUrlPairs;
|
|
11
21
|
const url_1 = require("url");
|
|
12
22
|
const path_1 = require("path");
|
|
13
23
|
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
@@ -27,7 +37,6 @@ function normalizeQueueName(qname, opt) {
|
|
|
27
37
|
const base = hasFifo ? qname.slice(0, -exports.fifoSuffix.length) : qname;
|
|
28
38
|
return (needsPrefix ? opt.prefix : '') + base + (needsFifo ? exports.fifoSuffix : '');
|
|
29
39
|
}
|
|
30
|
-
exports.normalizeQueueName = normalizeQueueName;
|
|
31
40
|
//
|
|
32
41
|
// Normalizes fail queue name, appending both --fail-suffix and .fifo depending on opt
|
|
33
42
|
//
|
|
@@ -38,7 +47,6 @@ function normalizeFailQueueName(fqname, opt) {
|
|
|
38
47
|
const needsFail = !base.endsWith(opt.failSuffix);
|
|
39
48
|
return base + (needsFail ? opt.failSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
|
|
40
49
|
}
|
|
41
|
-
exports.normalizeFailQueueName = normalizeFailQueueName;
|
|
42
50
|
//
|
|
43
51
|
// Normalizes dlq queue name, appending both --dlq-suffix and .fifo depending on opt
|
|
44
52
|
//
|
|
@@ -48,14 +56,12 @@ function normalizeDLQName(dqname, opt) {
|
|
|
48
56
|
const needsDead = !base.endsWith(opt.dlqSuffix);
|
|
49
57
|
return base + (needsDead ? opt.dlqSuffix : '') + (opt.fifo ? exports.fifoSuffix : '');
|
|
50
58
|
}
|
|
51
|
-
exports.normalizeDLQName = normalizeDLQName;
|
|
52
59
|
//
|
|
53
60
|
// Clear cache
|
|
54
61
|
//
|
|
55
62
|
function qrlCacheClear() {
|
|
56
63
|
qcache.clear();
|
|
57
64
|
}
|
|
58
|
-
exports.qrlCacheClear = qrlCacheClear;
|
|
59
65
|
//
|
|
60
66
|
// Get QRL (Queue URL)
|
|
61
67
|
// Returns a promise for the queue name
|
|
@@ -80,7 +86,6 @@ async function qrlCacheGet(qname) {
|
|
|
80
86
|
// debug('qcache', Object.keys(qcache), 'get', qname, ' => ', qcache[qname])
|
|
81
87
|
return qrl;
|
|
82
88
|
}
|
|
83
|
-
exports.qrlCacheGet = qrlCacheGet;
|
|
84
89
|
//
|
|
85
90
|
// Set QRL (Queue URL)
|
|
86
91
|
// Immediately updates the cache
|
|
@@ -90,7 +95,6 @@ function qrlCacheSet(qname, qrl) {
|
|
|
90
95
|
qcache.set(qname, qrl);
|
|
91
96
|
// debug('qcache', Object.keys(qcache), 'set', qname, ' => ', qcache[qname])
|
|
92
97
|
}
|
|
93
|
-
exports.qrlCacheSet = qrlCacheSet;
|
|
94
98
|
//
|
|
95
99
|
// Invalidate cache for given qname
|
|
96
100
|
//
|
|
@@ -98,7 +102,6 @@ function qrlCacheInvalidate(qname) {
|
|
|
98
102
|
// debug('qcache', Object.keys(qcache), 'delete', qname, ' (was ', qcache[qname], ')')
|
|
99
103
|
qcache.delete(qname);
|
|
100
104
|
}
|
|
101
|
-
exports.qrlCacheInvalidate = qrlCacheInvalidate;
|
|
102
105
|
//
|
|
103
106
|
// Ingets multiple QRLs
|
|
104
107
|
// Extracts queue names from an array of QRLs and immediately updates the cache.
|
|
@@ -114,7 +117,6 @@ function ingestQRLs(qrls) {
|
|
|
114
117
|
}
|
|
115
118
|
return pairs;
|
|
116
119
|
}
|
|
117
|
-
exports.ingestQRLs = ingestQRLs;
|
|
118
120
|
/**
|
|
119
121
|
* Returns qrls for queues matching the given prefix and regex.
|
|
120
122
|
*/
|
|
@@ -133,7 +135,6 @@ async function getMatchingQueues(prefix, nextToken) {
|
|
|
133
135
|
(qrls || []).push(...await getMatchingQueues(prefix, keepGoing));
|
|
134
136
|
return qrls || [];
|
|
135
137
|
}
|
|
136
|
-
exports.getMatchingQueues = getMatchingQueues;
|
|
137
138
|
//
|
|
138
139
|
// Resolves into a flattened aray of {qname: ..., qrl: ...} objects.
|
|
139
140
|
//
|
|
@@ -168,5 +169,4 @@ async function getQnameUrlPairs(qnames, opt) {
|
|
|
168
169
|
const results = await Promise.all(promises);
|
|
169
170
|
return ([].concat.apply([], results)).filter(r => r);
|
|
170
171
|
}
|
|
171
|
-
exports.getQnameUrlPairs = getQnameUrlPairs;
|
|
172
172
|
debug('loaded');
|
|
@@ -311,7 +311,15 @@ class JobExecutor {
|
|
|
311
311
|
this.stats.runningJobs++;
|
|
312
312
|
this.stats.waitingJobs--;
|
|
313
313
|
const queue = job.qname.slice(this.opt.prefix.length);
|
|
314
|
-
const
|
|
314
|
+
const attributes = {
|
|
315
|
+
queueName: job.qname,
|
|
316
|
+
messageId: job.message.MessageId || '',
|
|
317
|
+
receiveCount: job.message.Attributes?.ApproximateReceiveCount || '1',
|
|
318
|
+
sentTimestamp: job.message.Attributes?.SentTimestamp || '',
|
|
319
|
+
firstReceiveTimestamp: job.message.Attributes?.ApproximateFirstReceiveTimestamp || '',
|
|
320
|
+
messageGroupId: job.message.Attributes?.MessageGroupId || ''
|
|
321
|
+
};
|
|
322
|
+
const result = await job.callback(queue, job.payload, attributes);
|
|
315
323
|
debug('executeJob callback finished', { payload: job.payload, result });
|
|
316
324
|
if (this.opt.verbose) {
|
|
317
325
|
console.error(chalk_1.default.green('SUCCESS'), job.payload);
|
|
@@ -369,7 +377,7 @@ class JobExecutor {
|
|
|
369
377
|
for (const [job, i] of jobs.map((job, i) => [job, i])) {
|
|
370
378
|
// Figure out if the next job needs to happen in serial, otherwise we can parallel execute
|
|
371
379
|
const nextJob = jobs[i + 1];
|
|
372
|
-
const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.
|
|
380
|
+
const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId;
|
|
373
381
|
// console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
|
|
374
382
|
// Execute serial or parallel
|
|
375
383
|
if (nextJobIsSerial)
|
package/commonjs/src/sqs.js
CHANGED
|
@@ -6,7 +6,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
9
|
+
exports.getSQSClient = getSQSClient;
|
|
10
|
+
exports.setSQSClient = setSQSClient;
|
|
11
|
+
exports.getMatchingQueues = getMatchingQueues;
|
|
12
|
+
exports.getQueueAttributes = getQueueAttributes;
|
|
10
13
|
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
11
14
|
const path_1 = require("path");
|
|
12
15
|
const debug_1 = __importDefault(require("debug"));
|
|
@@ -21,14 +24,12 @@ function getSQSClient() {
|
|
|
21
24
|
client = new client_sqs_1.SQSClient();
|
|
22
25
|
return client;
|
|
23
26
|
}
|
|
24
|
-
exports.getSQSClient = getSQSClient;
|
|
25
27
|
/**
|
|
26
28
|
* Utility function to set the client explicitly, used in testing.
|
|
27
29
|
*/
|
|
28
30
|
function setSQSClient(explicitClient) {
|
|
29
31
|
client = explicitClient;
|
|
30
32
|
}
|
|
31
|
-
exports.setSQSClient = setSQSClient;
|
|
32
33
|
/**
|
|
33
34
|
* Returns qrls for queues matching the given prefix and regex.
|
|
34
35
|
*/
|
|
@@ -48,7 +49,6 @@ async function getMatchingQueues(prefix, regex) {
|
|
|
48
49
|
}
|
|
49
50
|
return processQueues();
|
|
50
51
|
}
|
|
51
|
-
exports.getMatchingQueues = getMatchingQueues;
|
|
52
52
|
/**
|
|
53
53
|
* Gets attributes on every queue in parallel.
|
|
54
54
|
*/
|
|
@@ -93,5 +93,4 @@ async function getQueueAttributes(qrls) {
|
|
|
93
93
|
}
|
|
94
94
|
return Promise.all(promises);
|
|
95
95
|
}
|
|
96
|
-
exports.getQueueAttributes = getQueueAttributes;
|
|
97
96
|
debug('loaded');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A distributed scheduler for SQS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -14,15 +14,13 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@aws-sdk/client-cloudwatch": "3.465.0",
|
|
16
16
|
"@aws-sdk/client-sqs": "3.465.0",
|
|
17
|
-
"@sentry/node": "^7.
|
|
17
|
+
"@sentry/node": "^7.120.4",
|
|
18
18
|
"chalk": "^4.1.2",
|
|
19
19
|
"command-line-args": "^5.2.1",
|
|
20
20
|
"command-line-commands": "^3.0.2",
|
|
21
|
-
"command-line-usage": "^7.0.
|
|
22
|
-
"debug": "^4.3
|
|
23
|
-
"ioredis": "^5.3
|
|
24
|
-
"ioredis-mock": "^8.9.0",
|
|
25
|
-
"standard": "^17.1.0",
|
|
21
|
+
"command-line-usage": "^7.0.3",
|
|
22
|
+
"debug": "^4.4.3",
|
|
23
|
+
"ioredis": "^5.9.3",
|
|
26
24
|
"tree-kill": "^1.2.2",
|
|
27
25
|
"uuid": "^9.0.1"
|
|
28
26
|
},
|
|
@@ -36,10 +34,12 @@
|
|
|
36
34
|
}
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
|
-
"aws-sdk-client-mock": "^3.
|
|
40
|
-
"aws-sdk-client-mock-jest": "^3.
|
|
37
|
+
"aws-sdk-client-mock": "^3.1.0",
|
|
38
|
+
"aws-sdk-client-mock-jest": "^3.1.0",
|
|
39
|
+
"ioredis-mock": "^8.13.1",
|
|
41
40
|
"jest": "^29.7.0",
|
|
42
|
-
"
|
|
41
|
+
"standard": "^17.1.2",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
43
|
},
|
|
44
44
|
"preferGlobal": true,
|
|
45
45
|
"engines": {
|
package/src/cloudWatch.js
CHANGED
|
@@ -85,6 +85,16 @@ export async function putAggregateData (total, timestamp) {
|
|
|
85
85
|
Timestamp: now,
|
|
86
86
|
Value: total.ApproximateNumberOfMessagesNotVisible || 0,
|
|
87
87
|
Unit: 'Count'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
MetricName: 'ApproximateAgeOfOldestMessage',
|
|
91
|
+
Dimensions: [{
|
|
92
|
+
Name: 'queueName',
|
|
93
|
+
Value: total.queueName
|
|
94
|
+
}],
|
|
95
|
+
Timestamp: now,
|
|
96
|
+
Value: total.ApproximateAgeOfOldestMessage || 0,
|
|
97
|
+
Unit: 'Seconds'
|
|
88
98
|
}
|
|
89
99
|
]
|
|
90
100
|
}
|
package/src/defaults.js
CHANGED
|
@@ -66,7 +66,7 @@ export function validateQueueName (opt, name) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function validateMessageOptions (messageOptions) {
|
|
69
|
-
const validKeys = ['deduplicationId', 'groupId']
|
|
69
|
+
const validKeys = ['deduplicationId', 'groupId', 'delay']
|
|
70
70
|
if (typeof messageOptions === 'object' &&
|
|
71
71
|
!Array.isArray(messageOptions) &&
|
|
72
72
|
messageOptions !== null) {
|
|
@@ -142,7 +142,10 @@ export function getOptionsWithDefaults (options) {
|
|
|
142
142
|
|
|
143
143
|
// Check
|
|
144
144
|
create: options.create || process.env.QDONE_CREATE === 'true' || defaults.create,
|
|
145
|
-
overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || defaults.overwrite
|
|
145
|
+
overwrite: options.overwrite || process.env.QDONE_OVERWRITE === 'true' || defaults.overwrite,
|
|
146
|
+
|
|
147
|
+
// Dependency injection
|
|
148
|
+
Redis: options.Redis
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// Setting this env here means we don't have to in AWS SDK constructors
|
package/src/idleQueues.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getCloudWatchClient } from './cloudWatch.js'
|
|
|
7
7
|
import { getOptionsWithDefaults } from './defaults.js'
|
|
8
8
|
import { GetQueueAttributesCommand, DeleteQueueCommand, QueueDoesNotExist } from '@aws-sdk/client-sqs'
|
|
9
9
|
import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
|
|
10
|
-
import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix
|
|
10
|
+
import { normalizeFailQueueName, normalizeDLQName, getQnameUrlPairs, fifoSuffix } from './qrlCache.js'
|
|
11
11
|
import { getCache, setCache } from './cache.js'
|
|
12
12
|
// const AWS = require('aws-sdk')
|
|
13
13
|
|
package/src/monitor.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getMatchingQueues, getQueueAttributes } from './sqs.js'
|
|
6
|
-
import { putAggregateData } from './cloudWatch.js'
|
|
6
|
+
import { putAggregateData, getCloudWatchClient } from './cloudWatch.js'
|
|
7
|
+
import { GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
|
|
7
8
|
import { getOptionsWithDefaults } from './defaults.js'
|
|
8
9
|
import { normalizeQueueName } from './qrlCache.js'
|
|
9
10
|
import Debug from 'debug'
|
|
@@ -38,12 +39,42 @@ export function interpretWildcard (queueName) {
|
|
|
38
39
|
return { prefix, suffix, safeSuffix, suffixRegex }
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Gets ApproximateAgeOfOldestMessage for a single queue from CloudWatch.
|
|
44
|
+
* This metric is not available via the SQS GetQueueAttributes API.
|
|
45
|
+
*/
|
|
46
|
+
export async function getQueueAge (queueName) {
|
|
47
|
+
const now = new Date()
|
|
48
|
+
const params = {
|
|
49
|
+
StartTime: new Date(now.getTime() - 1000 * 60 * 5),
|
|
50
|
+
EndTime: now,
|
|
51
|
+
MetricName: 'ApproximateAgeOfOldestMessage',
|
|
52
|
+
Namespace: 'AWS/SQS',
|
|
53
|
+
Period: 300,
|
|
54
|
+
Dimensions: [{ Name: 'QueueName', Value: queueName }],
|
|
55
|
+
Statistics: ['Maximum']
|
|
56
|
+
}
|
|
57
|
+
const client = getCloudWatchClient()
|
|
58
|
+
const cmd = new GetMetricStatisticsCommand(params)
|
|
59
|
+
try {
|
|
60
|
+
const data = await client.send(cmd)
|
|
61
|
+
debug('getQueueAge', queueName, data)
|
|
62
|
+
if (!data.Datapoints || data.Datapoints.length === 0) return 0
|
|
63
|
+
return Math.max(...data.Datapoints.map(d => d.Maximum))
|
|
64
|
+
} catch (e) {
|
|
65
|
+
debug('getQueueAge error', queueName, e)
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
/**
|
|
42
71
|
* Aggregates inmportant attributes across queues and reports a summary.
|
|
43
|
-
* Attributes:
|
|
72
|
+
* Attributes (from SQS GetQueueAttributes):
|
|
44
73
|
* - ApproximateNumberOfMessages: Sum
|
|
45
74
|
* - ApproximateNumberOfMessagesDelayed: Sum
|
|
46
75
|
* - ApproximateNumberOfMessagesNotVisible: Sum
|
|
76
|
+
* Metrics (from CloudWatch):
|
|
77
|
+
* - ApproximateAgeOfOldestMessage: Max
|
|
47
78
|
*/
|
|
48
79
|
export async function getAggregateData (queueName) {
|
|
49
80
|
const { prefix, suffixRegex } = interpretWildcard(queueName)
|
|
@@ -63,6 +94,14 @@ export async function getAggregateData (queueName) {
|
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
96
|
}
|
|
97
|
+
|
|
98
|
+
// Fetch ApproximateAgeOfOldestMessage from CloudWatch (not available via SQS API)
|
|
99
|
+
// Only query queues with messages to minimize CloudWatch API costs
|
|
100
|
+
const ageResults = await Promise.all(
|
|
101
|
+
[...total.contributingQueueNames].map(queue => getQueueAge(queue))
|
|
102
|
+
)
|
|
103
|
+
total.ApproximateAgeOfOldestMessage = Math.max(0, ...ageResults)
|
|
104
|
+
|
|
66
105
|
// debug({ total })
|
|
67
106
|
// convert set to array
|
|
68
107
|
total.contributingQueueNames = [...total.contributingQueueNames.values()]
|
|
@@ -322,7 +322,15 @@ export class JobExecutor {
|
|
|
322
322
|
this.stats.runningJobs++
|
|
323
323
|
this.stats.waitingJobs--
|
|
324
324
|
const queue = job.qname.slice(this.opt.prefix.length)
|
|
325
|
-
const
|
|
325
|
+
const attributes = {
|
|
326
|
+
queueName: job.qname,
|
|
327
|
+
messageId: job.message.MessageId || '',
|
|
328
|
+
receiveCount: job.message.Attributes?.ApproximateReceiveCount || '1',
|
|
329
|
+
sentTimestamp: job.message.Attributes?.SentTimestamp || '',
|
|
330
|
+
firstReceiveTimestamp: job.message.Attributes?.ApproximateFirstReceiveTimestamp || '',
|
|
331
|
+
messageGroupId: job.message.Attributes?.MessageGroupId || ''
|
|
332
|
+
}
|
|
333
|
+
const result = await job.callback(queue, job.payload, attributes)
|
|
326
334
|
debug('executeJob callback finished', { payload: job.payload, result })
|
|
327
335
|
if (this.opt.verbose) {
|
|
328
336
|
console.error(chalk.green('SUCCESS'), job.payload)
|
|
@@ -381,7 +389,7 @@ export class JobExecutor {
|
|
|
381
389
|
for (const [job, i] of jobs.map((job, i) => [job, i])) {
|
|
382
390
|
// Figure out if the next job needs to happen in serial, otherwise we can parallel execute
|
|
383
391
|
const nextJob = jobs[i + 1]
|
|
384
|
-
const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.
|
|
392
|
+
const nextJobIsSerial = isFifo && nextJob && job.message?.Attributes?.MessageGroupId === nextJob.message?.Attributes?.MessageGroupId
|
|
385
393
|
|
|
386
394
|
// console.log({ i, nextJobAtt: nextJob?.message?.Attributes, nextJobIsSerial })
|
|
387
395
|
// Execute serial or parallel
|
package/src/worker.js
CHANGED
|
@@ -104,10 +104,21 @@ export async function executeJob (job, qname, qrl, opt) {
|
|
|
104
104
|
const treeKiller = setTimeout(killTree, opt.killAfter * 1000)
|
|
105
105
|
debug({ treeKiller: opt.killAfter * 1000, date: Date.now() })
|
|
106
106
|
|
|
107
|
+
// Build environment with SQS message attributes for child process
|
|
108
|
+
const env = {
|
|
109
|
+
...process.env,
|
|
110
|
+
QDONE_QUEUE_NAME: qname,
|
|
111
|
+
SQS_MESSAGE_ID: job.MessageId || '',
|
|
112
|
+
SQS_RECEIVE_COUNT: job.Attributes?.ApproximateReceiveCount || '1',
|
|
113
|
+
SQS_SENT_TIMESTAMP: job.Attributes?.SentTimestamp || '',
|
|
114
|
+
SQS_FIRST_RECEIVE_TIMESTAMP: job.Attributes?.ApproximateFirstReceiveTimestamp || '',
|
|
115
|
+
SQS_MESSAGE_GROUP_ID: job.Attributes?.MessageGroupId || ''
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
try {
|
|
108
119
|
// Success path for job execution
|
|
109
120
|
const { stdout, stderr } = await new Promise(function (resolve, reject) {
|
|
110
|
-
child = exec(cmd, function (err, stdout, stderr) {
|
|
121
|
+
child = exec(cmd, { env }, function (err, stdout, stderr) {
|
|
111
122
|
if (err) {
|
|
112
123
|
err.stdout = stdout
|
|
113
124
|
err.stderr = stderr
|