qdone 2.0.34-alpha → 2.0.36-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.
@@ -16,17 +16,18 @@ let client;
16
16
  * how to connect.
17
17
  */
18
18
  function getCacheClient(opt) {
19
+ const RedisClass = opt.Redis || ioredis_1.default;
19
20
  if (client) {
20
21
  return client;
21
22
  }
22
23
  else if (opt.cacheUri) {
23
24
  const url = new url_1.URL(opt.cacheUri);
24
25
  if (url.protocol === 'redis:') {
25
- client = new ioredis_1.default(url.toString());
26
+ client = new RedisClass(url.toString());
26
27
  }
27
28
  else if (url.protocol === 'redis-cluster:') {
28
29
  url.protocol = 'redis:';
29
- client = new ioredis_1.default.Cluster([url.toString()], { slotsRefreshInterval: 60 * 1000 });
30
+ client = new RedisClass.Cluster([url.toString()], { slotsRefreshInterval: 60 * 1000 });
30
31
  }
31
32
  else {
32
33
  throw new UsageError(`Only redis:// or redis-cluster:// URLs are currently supported. Got: ${url.protocol}`);
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dedupSuccessfullyProcessedMulti = exports.dedupSuccessfullyProcessed = exports.dedupShouldEnqueueMulti = exports.dedupShouldEnqueue = exports.statMaintenance = exports.updateStats = exports.addDedupParamsToMessage = exports.getCacheKey = exports.getDeduplicationId = void 0;
7
+ const crypto_1 = require("crypto");
8
+ const uuid_1 = require("uuid");
9
+ const cache_js_1 = require("./cache.js");
10
+ const debug_1 = __importDefault(require("debug"));
11
+ const debug = (0, debug_1.default)('qdone:dedup');
12
+ /**
13
+ * Returns a MessageDeduplicationId key appropriate for using with Amazon SQS
14
+ * for the given message. The passed dedupContent will be returned untouched
15
+ * if it meets all the requirements for SQS's MessageDeduplicationId,
16
+ * otherwise disallowed characters will be replaced by `_` and content longer
17
+ * than 128 characters will be truncated and a hash of the content appended.
18
+ * @param {String} dedupContent - Content used to construct the deduplication id.
19
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
20
+ * @returns {String} the cache key
21
+ */
22
+ function getDeduplicationId(dedupContent, opt) {
23
+ debug({ getDeduplicationId: { dedupContent } });
24
+ // Don't transmit long keys to redis
25
+ dedupContent = dedupContent.trim().replace(/[^a-zA-Z0-9!"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]/g, '_');
26
+ const max = 128;
27
+ const sep = '...sha1:';
28
+ if (dedupContent.length > max) {
29
+ dedupContent = dedupContent.slice(0, max - sep.length - 40) + '...sha1:' + (0, crypto_1.createHash)('sha1').update(dedupContent).digest('hex');
30
+ }
31
+ return dedupContent;
32
+ }
33
+ exports.getDeduplicationId = getDeduplicationId;
34
+ /**
35
+ * Returns the cache key given a deduplication id.
36
+ * @param {String} dedupId - a deduplication id returned from getDeduplicationId
37
+ * @param opt - Opt object from getOptionsWithDefaults()
38
+ * @returns the cache key
39
+ */
40
+ function getCacheKey(dedupId, opt) {
41
+ const cacheKey = opt.cachePrefix + 'dedup:' + dedupId;
42
+ debug({ getCacheKey: { cacheKey } });
43
+ return cacheKey;
44
+ }
45
+ exports.getCacheKey = getCacheKey;
46
+ /**
47
+ * Modifies a message (parameters to SendMessageCommand) to add the parameters
48
+ * for whatever deduplication options the caller has set.
49
+ * @param {String} message - parameters to SendMessageCommand
50
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
51
+ * @param {Object} [messageOptions] - optional per message options. We only care about the key deduplicationId.
52
+ * @returns {Object} the modified parameters/message object
53
+ */
54
+ function addDedupParamsToMessage(message, opt, messageOptions) {
55
+ // Either of these means we need to calculate an id
56
+ if (opt.fifo || opt.externalDedup) {
57
+ const uuidFunction = opt.uuidFunction || uuid_1.v1;
58
+ if (opt.deduplicationId)
59
+ message.MessageDeduplicationId = opt.deduplicationId;
60
+ if (opt.dedupIdPerMessage)
61
+ message.MessageDeduplicationId = uuidFunction();
62
+ if (messageOptions?.deduplicationId)
63
+ message.MessageDeduplicationId = messageOptions.deduplicationId;
64
+ // Fallback to using the message body
65
+ if (!message.MessageDeduplicationId) {
66
+ message.MessageDeduplicationId = getDeduplicationId(message.MessageBody, opt);
67
+ }
68
+ // Track our own dedup id so we can look it up upon ReceiveMessage
69
+ if (opt.externalDedup) {
70
+ message.MessageAttributes = {
71
+ QdoneDeduplicationId: {
72
+ StringValue: message.MessageDeduplicationId,
73
+ DataType: 'String'
74
+ }
75
+ };
76
+ // If we are using our own dedup, then we must disable the SQS dedup by
77
+ // providing a different unique ID. Otherwise SQS will interact with us.
78
+ if (opt.fifo)
79
+ message.MessageDeduplicationId = uuidFunction();
80
+ }
81
+ // Non fifo can't have this parameter
82
+ if (!opt.fifo)
83
+ delete message.MessageDeduplicationId;
84
+ }
85
+ return message;
86
+ }
87
+ exports.addDedupParamsToMessage = addDedupParamsToMessage;
88
+ /**
89
+ * Updates statistics in redis, of which there are two:
90
+ * 1. duplicateSet - a set who's members are cache keys and scores are the number of duplicate
91
+ * runs prevented by dedup.
92
+ * 2. expirationSet - a set who's members are cache keys and scores are when the cache key expires
93
+ * @param {String} cacheKey
94
+ * @param {Number} duplicates - the number of duplicates, must be at least 1 to gather stats
95
+ * @param {Number} expireAt - timestamp for when this key's dedupPeriod expires
96
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
97
+ * @param {Object} pipeline - (Optional) redis pipeline you will exec() yourself
98
+ */
99
+ async function updateStats(cacheKey, duplicates, expireAt, opt, pipeline) {
100
+ if (duplicates >= 1) {
101
+ const duplicateSet = opt.cachePrefix + 'dedup-stats:duplicateSet';
102
+ const expirationSet = opt.cachePrefix + 'dedup-stats:expirationSet';
103
+ const hadPipeline = !!pipeline;
104
+ if (!hadPipeline)
105
+ pipeline = (0, cache_js_1.getCacheClient)(opt).multi();
106
+ pipeline.zadd(duplicateSet, 'GT', duplicates, cacheKey);
107
+ pipeline.zadd(expirationSet, 'GT', expireAt, cacheKey);
108
+ if (!hadPipeline)
109
+ await pipeline.exec();
110
+ }
111
+ }
112
+ exports.updateStats = updateStats;
113
+ /**
114
+ * Removes expired items from stats.
115
+ */
116
+ async function statMaintenance(opt) {
117
+ const duplicateSet = opt.cachePrefix + 'dedup-stats:duplicateSet';
118
+ const expirationSet = opt.cachePrefix + 'dedup-stats:expirationSet';
119
+ const client = (0, cache_js_1.getCacheClient)(opt);
120
+ const now = new Date().getTime();
121
+ // Grab a batch of expired keys
122
+ debug({ statMaintenance: { aboutToGo: true, expirationSet } });
123
+ const expiredStats = await client.zrange(expirationSet, '-inf', now, 'BYSCORE');
124
+ debug({ statMaintenance: { expiredStats } });
125
+ // And remove them from indexes, main storage
126
+ if (expiredStats.length) {
127
+ const result = await client.multi()
128
+ .zrem(expirationSet, expiredStats)
129
+ .zrem(duplicateSet, expiredStats)
130
+ .exec();
131
+ debug({ statMaintenance: { result } });
132
+ }
133
+ }
134
+ exports.statMaintenance = statMaintenance;
135
+ /**
136
+ * Determines whether we should enqueue this message or whether it is a duplicate.
137
+ * Returns true if enqueuing the message would not result in a duplicate.
138
+ * @param {Object} message - Parameters to SendMessageCommand
139
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
140
+ * @returns {Boolean} true if the message can be enqueued without duplicate, else false
141
+ */
142
+ async function dedupShouldEnqueue(message, opt) {
143
+ const client = (0, cache_js_1.getCacheClient)(opt);
144
+ const dedupId = message?.MessageAttributes?.QdoneDeduplicationId?.StringValue;
145
+ const cacheKey = getCacheKey(dedupId, opt);
146
+ const expireAt = new Date().getTime() + opt.dedupPeriod;
147
+ const copies = await client.incr(cacheKey);
148
+ debug({ action: 'shouldEnqueue', cacheKey, copies });
149
+ if (copies === 1) {
150
+ await client.expireat(cacheKey, expireAt);
151
+ return true;
152
+ }
153
+ if (opt.dedupStats) {
154
+ const duplicates = copies - 1;
155
+ await updateStats(cacheKey, duplicates, expireAt, opt);
156
+ }
157
+ return false;
158
+ }
159
+ exports.dedupShouldEnqueue = dedupShouldEnqueue;
160
+ /**
161
+ * Determines which messages we should enqueue, returning only those that
162
+ * would not be duplicates.
163
+ * @param {Array[Object]} messages - Entries array for the SendMessageBatchCommand
164
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
165
+ * @returns {Array[Object]} an array of messages that can be safely enqueued. Could be empty.
166
+ */
167
+ async function dedupShouldEnqueueMulti(messages, opt) {
168
+ debug({ dedupShouldEnqueueMulti: { messages, opt } });
169
+ const expireAt = new Date().getTime() + opt.dedupPeriod;
170
+ // Increment all
171
+ const incrPipeline = (0, cache_js_1.getCacheClient)(opt).pipeline();
172
+ for (const message of messages) {
173
+ const dedupId = message?.MessageAttributes?.QdoneDeduplicationId?.StringValue;
174
+ const cacheKey = getCacheKey(dedupId, opt);
175
+ incrPipeline.incr(cacheKey);
176
+ }
177
+ const responses = await incrPipeline.exec();
178
+ debug({ dedupShouldEnqueueMulti: { messages, responses } });
179
+ // Figure out dedup period
180
+ const minDedupPeriod = 6 * 60;
181
+ const dedupPeriod = Math.min(opt.dedupPeriod, minDedupPeriod);
182
+ // Interpret responses and expire keys for races we won
183
+ const expirePipeline = (0, cache_js_1.getCacheClient)(opt).pipeline();
184
+ const statsPipeline = opt.dedupStats ? (0, cache_js_1.getCacheClient)(opt).pipeline() : undefined;
185
+ const messagesToEnqueue = [];
186
+ for (let i = 0; i < messages.length; i++) {
187
+ const message = messages[i];
188
+ const [, copies] = responses[i];
189
+ const dedupId = message?.MessageAttributes?.QdoneDeduplicationId?.StringValue;
190
+ const cacheKey = getCacheKey(dedupId, opt);
191
+ if (copies === 1) {
192
+ messagesToEnqueue.push(message);
193
+ expirePipeline.expireat(cacheKey, expireAt);
194
+ }
195
+ else if (opt.dedupStats) {
196
+ const duplicates = copies - 1;
197
+ updateStats(cacheKey, duplicates, expireAt, opt, statsPipeline);
198
+ }
199
+ }
200
+ await expirePipeline.exec();
201
+ if (opt.dedupStats)
202
+ await statsPipeline.exec();
203
+ return messagesToEnqueue;
204
+ }
205
+ exports.dedupShouldEnqueueMulti = dedupShouldEnqueueMulti;
206
+ /**
207
+ * Marks a message as processed so that subsequent calls to dedupShouldEnqueue
208
+ * and dedupShouldEnqueueMulti will allow a message to be enqueued again
209
+ * without waiting for dedupPeriod to expire.
210
+ * @param {Object} message - Return value from RecieveMessageCommand
211
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
212
+ * @returns {Number} 1 if a cache key was deleted, otherwise 0
213
+ */
214
+ async function dedupSuccessfullyProcessed(message, opt) {
215
+ const client = (0, cache_js_1.getCacheClient)(opt);
216
+ const dedupId = message?.MessageAttributes?.QdoneDeduplicationId?.StringValue;
217
+ if (dedupId) {
218
+ const cacheKey = getCacheKey(dedupId, opt);
219
+ const count = await client.del(cacheKey);
220
+ // Probabalistic stat maintenance
221
+ if (opt.dedupStats) {
222
+ const chance = 1 / 100.0;
223
+ if (Math.random() < chance)
224
+ await statMaintenance(opt);
225
+ }
226
+ return count;
227
+ }
228
+ return 0;
229
+ }
230
+ exports.dedupSuccessfullyProcessed = dedupSuccessfullyProcessed;
231
+ /**
232
+ * Marks an array of messages as processed so that subsequent calls to
233
+ * dedupShouldEnqueue and dedupShouldEnqueueMulti will allow a message to be
234
+ * enqueued again without waiting for dedupPeriod to expire.
235
+ * @param {Array[Object]} messages - Return values from RecieveMessageCommand
236
+ * @param {Object} opt - Opt object from getOptionsWithDefaults()
237
+ * @returns {Number} number of deleted keys
238
+ */
239
+ async function dedupSuccessfullyProcessedMulti(messages, opt) {
240
+ debug({ messages, dedupSuccessfullyProcessedMulti: { messages, opt } });
241
+ const cacheKeys = [];
242
+ for (const message of messages) {
243
+ const dedupId = message?.MessageAttributes?.QdoneDeduplicationId?.StringValue;
244
+ if (dedupId) {
245
+ const cacheKey = getCacheKey(dedupId, opt);
246
+ cacheKeys.push(cacheKey);
247
+ }
248
+ }
249
+ debug({ dedupSuccessfullyProcessedMulti: { cacheKeys } });
250
+ if (cacheKeys.length) {
251
+ const numDeleted = await (0, cache_js_1.getCacheClient)(opt).del(cacheKeys);
252
+ // const numDeleted = results.map(([, val]) => val).reduce((a, b) => a + b, 0)
253
+ debug({ dedupSuccessfullyProcessedMulti: { cacheKeys, numDeleted } });
254
+ // Probabalistic stat maintenance
255
+ if (opt.dedupStats) {
256
+ const chance = numDeleted / 100.0;
257
+ if (Math.random() < chance)
258
+ await statMaintenance(opt);
259
+ }
260
+ return numDeleted;
261
+ }
262
+ return 0;
263
+ }
264
+ exports.dedupSuccessfullyProcessedMulti = dedupSuccessfullyProcessedMulti;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupVerbose = exports.setupAWS = exports.getOptionsWithDefaults = exports.defaults = void 0;
3
+ exports.setupVerbose = exports.setupAWS = exports.getOptionsWithDefaults = exports.validateMessageOptions = exports.defaults = void 0;
4
4
  /**
5
5
  * Default options for qdone. Accepts a command line options object and
6
6
  * returns nicely-named options.
@@ -20,10 +20,15 @@ exports.defaults = Object.freeze({
20
20
  fifo: false,
21
21
  disableLog: false,
22
22
  includeFailed: false,
23
+ includeDead: false,
24
+ externalDedup: false,
25
+ dedupPeriod: 60 * 5,
26
+ dedupStats: false,
23
27
  // Enqueue
24
28
  groupId: (0, uuid_1.v1)(),
25
29
  groupIdPerMessage: false,
26
30
  deduplicationId: undefined,
31
+ dedupIdPerMessage: false,
27
32
  messageRetentionPeriod: 1209600,
28
33
  delay: 0,
29
34
  sendRetries: 6,
@@ -41,7 +46,9 @@ exports.defaults = Object.freeze({
41
46
  // Idle Queues
42
47
  idleFor: 60,
43
48
  delete: false,
44
- unpair: false
49
+ unpair: false,
50
+ // Check
51
+ create: false
45
52
  });
46
53
  function validateInteger(opt, name) {
47
54
  const parsed = parseInt(opt[name], 10);
@@ -49,6 +56,20 @@ function validateInteger(opt, name) {
49
56
  throw new Error(`${name} needs to be an integer.`);
50
57
  return parsed;
51
58
  }
59
+ function validateMessageOptions(messageOptions) {
60
+ const validKeys = ['deduplicaitonId', 'groupId'];
61
+ if (typeof messageOptions === 'object' &&
62
+ !Array.isArray(messageOptions) &&
63
+ messageOptions !== null) {
64
+ for (const key in messageOptions) {
65
+ if (!validKeys.includes(key))
66
+ throw new Error(`Invalid message option ${key}`);
67
+ }
68
+ return messageOptions;
69
+ }
70
+ return {};
71
+ }
72
+ exports.validateMessageOptions = validateMessageOptions;
52
73
  /**
53
74
  * This function should be called by each exposed API entry point on the
54
75
  * options passed in from the caller. It supports options named in camelCase
@@ -62,7 +83,7 @@ function getOptionsWithDefaults(options) {
62
83
  if (!options)
63
84
  options = {};
64
85
  // Activate DLQ if any option is set
65
- const dlq = options.dlq || !!(options['dlq-suffix'] || options['dlq-after'] || options['dlq-name']);
86
+ const dlq = options.dlq || !!(options['dlq-suffix'] || options['dlq-after'] || options['dlq-name'] || options.dlqSuffix || options.dlqAfter || options.dlqName);
66
87
  const opt = {
67
88
  // Shared
68
89
  prefix: options.prefix === '' ? options.prefix : (options.prefix || exports.defaults.prefix),
@@ -73,6 +94,11 @@ function getOptionsWithDefaults(options) {
73
94
  fifo: options.fifo || exports.defaults.fifo,
74
95
  sentryDsn: options.sentryDsn || options['sentry-dsn'],
75
96
  disableLog: options.disableLog || options['disable-log'] || exports.defaults.disableLog,
97
+ includeFailed: options.includeFailed || options['include-failed'] || exports.defaults.includeFailed,
98
+ includeDead: options.includeDead || options['include-dead'] || exports.defaults.includeDead,
99
+ externalDedup: options.externalDedup || options['external-dedup'] || exports.defaults.externalDedup,
100
+ dedupPeriod: options.dedupPeriod || options['dedup-period'] || exports.defaults.dedupPeriod,
101
+ dedupStats: options.dedupStats || options['dedup-stats'] || exports.defaults.dedupStats,
76
102
  // Cache
77
103
  cacheUri: options.cacheUri || options['cache-uri'] || exports.defaults.cacheUri,
78
104
  cachePrefix: options.cachePrefix || options['cache-prefix'] || exports.defaults.cachePrefix,
@@ -81,6 +107,7 @@ function getOptionsWithDefaults(options) {
81
107
  groupId: options.groupId || options['group-id'] || exports.defaults.groupId,
82
108
  groupIdPerMessage: false,
83
109
  deduplicationId: options.deduplicationId || options['deduplication-id'] || exports.defaults.deduplicationId,
110
+ dedupIdPerMessage: options.dedupIdPerMessage || options['dedup-id-per-message'] || exports.defaults.dedupIdPerMessage,
84
111
  messageRetentionPeriod: options.messageRetentionPeriod || options['message-retention-period'] || exports.defaults.messageRetentionPeriod,
85
112
  delay: options.delay || exports.defaults.delay,
86
113
  sendRetries: options['send-retries'] || exports.defaults.sendRetries,
@@ -94,13 +121,15 @@ function getOptionsWithDefaults(options) {
94
121
  killAfter: options.killAfter || options['kill-after'] || exports.defaults.killAfter,
95
122
  archive: options.archive || exports.defaults.archive,
96
123
  activeOnly: options.activeOnly || options['active-only'] || exports.defaults.activeOnly,
97
- includeFailed: options.includeFailed || options['include-failed'] || exports.defaults.includeFailed,
98
124
  maxConcurrentJobs: options.maxConcurrentJobs || exports.defaults.maxConcurrentJobs,
99
125
  maxMemoryPercent: options.maxMemoryPercent || exports.defaults.maxMemoryPercent,
100
126
  // Idle Queues
101
127
  idleFor: options.idleFor || options['idle-for'] || exports.defaults.idleFor,
102
128
  delete: options.delete || exports.defaults.delete,
103
- unpair: options.delete || exports.defaults.unpair
129
+ unpair: options.delete || exports.defaults.unpair,
130
+ // Check
131
+ create: options.create || exports.defaults.create,
132
+ overwrite: options.overwrite || exports.defaults.overwrite
104
133
  };
105
134
  // Setting this env here means we don't have to in AWS SDK constructors
106
135
  process.env.AWS_REGION = opt.region;
@@ -109,6 +138,7 @@ function getOptionsWithDefaults(options) {
109
138
  opt.messageRetentionPeriod = validateInteger(opt, 'messageRetentionPeriod');
110
139
  opt.delay = validateInteger(opt, 'delay');
111
140
  opt.sendRetries = validateInteger(opt, 'sendRetries');
141
+ opt.dedupPeriod = validateInteger(opt, 'dedupPeriod');
112
142
  opt.failDelay = validateInteger(opt, 'failDelay');
113
143
  opt.dlqAfter = validateInteger(opt, 'dlqAfter');
114
144
  opt.waitTime = validateInteger(opt, 'waitTime');
@@ -116,6 +146,13 @@ function getOptionsWithDefaults(options) {
116
146
  opt.maxConcurrentJobs = validateInteger(opt, 'maxConcurrentJobs');
117
147
  opt.maxMemoryPercent = validateInteger(opt, 'maxMemoryPercent');
118
148
  opt.idleFor = validateInteger(opt, 'idleFor');
149
+ // Validate dedup args
150
+ if (opt.externalDedup && !opt.cacheUri)
151
+ throw new Error('--external-dedup requires the --cache-uri argument');
152
+ if (opt.externalDedup && (!opt.dedupPeriod || opt.dedupPeriod < 1))
153
+ throw new Error('--external-dedup of redis requires a --dedup-period > 1 second');
154
+ if (opt.dedupIdPerMessage && opt.deduplicationId)
155
+ throw new Error('Use either --deduplication-id or --dedup-id-per-message but not both');
119
156
  return opt;
120
157
  }
121
158
  exports.getOptionsWithDefaults = getOptionsWithDefaults;