ac-sqs 3.2.0 → 3.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/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/index.js +340 -85
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
## [3.2.1](https://github.com/admiralcloud/ac-sqs/compare/v3.2.0..v3.2.1) (2025-07-18 12:57:51)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fix
|
|
6
|
+
|
|
7
|
+
* **App:** Reduced code complexity | MP | [0313cc01d90d9e4d1ad3b7ef3136af7dfc72d717](https://github.com/admiralcloud/ac-sqs/commit/0313cc01d90d9e4d1ad3b7ef3136af7dfc72d717)
|
|
8
|
+
Create smaller functions
|
|
9
|
+
Related issues: [admiralcloud/ac-sqs#1](https://github.com/admiralcloud/ac-sqs/issues/1) [admiralcloud/ac-api-server#340](https://github.com/admiralcloud/ac-api-server/issues/340)
|
|
10
|
+
* **App:** Improved code quality | MP | [0d7e40518804f8c227cbc20c602510edada2e02e](https://github.com/admiralcloud/ac-sqs/commit/0d7e40518804f8c227cbc20c602510edada2e02e)
|
|
11
|
+
Separated huge function into smaller parts
|
|
12
|
+
Related issues: [admiralcloud/ac-sqs#1](https://github.com/admiralcloud/ac-sqs/issues/1) [admiralcloud/ac-api-server#340](https://github.com/admiralcloud/ac-api-server/issues/340)
|
|
13
|
+
* **App:** Improved code quality | MP | [cd9f5d3969d4aac9c3c87e7e84dd5788e9929cce](https://github.com/admiralcloud/ac-sqs/commit/cd9f5d3969d4aac9c3c87e7e84dd5788e9929cce)
|
|
14
|
+
Improved code quality
|
|
15
|
+
Related issues:
|
|
16
|
+
* **App:** Requested code changes | MP | [39aa6c33b9087fcb2c3cfa075716a5dd83e07550](https://github.com/admiralcloud/ac-sqs/commit/39aa6c33b9087fcb2c3cfa075716a5dd83e07550)
|
|
17
|
+
Improved code quality
|
|
18
|
+
Related issues:
|
|
19
|
+
* **App:** Improved visibility management | MP | [d45111846668fe3db268fd40987c726e82e1074c](https://github.com/admiralcloud/ac-sqs/commit/d45111846668fe3db268fd40987c726e82e1074c)
|
|
20
|
+
Add batch processing and throttling for visibility extension, better cleanup, graceful shutdown and stats
|
|
21
|
+
Related issues:
|
|
22
|
+
### Chores
|
|
23
|
+
|
|
24
|
+
* **App:** Updated packages | MP | [c6a2d43081047626aa60d5cb5f97427760143880](https://github.com/admiralcloud/ac-sqs/commit/c6a2d43081047626aa60d5cb5f97427760143880)
|
|
25
|
+
Updated packages
|
|
26
|
+
Related issues:
|
|
1
27
|
|
|
2
28
|
# [3.2.0](https://github.com/admiralcloud/ac-sqs/compare/v3.1.2..v3.2.0) (2025-05-11 11:56:57)
|
|
3
29
|
|
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ Preparations you have to make before running the tests:
|
|
|
120
120
|
+ export the AWS profile to use for tests (if it is not your default profile) using **export AWS_PROFILE=development**
|
|
121
121
|
+ export the AWS account id using **export awsaccount=12345**
|
|
122
122
|
+ create a SQS list named "test_acsqs"
|
|
123
|
-
+ create a bucket and export the name using **export bucket=
|
|
123
|
+
+ create a bucket and export the name using **export bucket=sqstest.admiralcloud.com**
|
|
124
124
|
+ export the node test environment using **export NODE_ENV=test**
|
|
125
125
|
|
|
126
126
|
**ATTENTION**: Tests may fail when checking the SQS length. This is a by-design failure:
|
package/index.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
const _ = require('lodash')
|
|
2
2
|
const { v4: uuidV4 } = require('uuid')
|
|
3
|
+
const { setTimeout: sleep } = require('timers/promises')
|
|
3
4
|
|
|
4
|
-
const { SQSClient, SendMessageCommand, SendMessageBatchCommand, ReceiveMessageCommand, DeleteMessageBatchCommand, GetQueueAttributesCommand,
|
|
5
|
+
const { SQSClient, SendMessageCommand, SendMessageBatchCommand, ReceiveMessageCommand, DeleteMessageBatchCommand, GetQueueAttributesCommand, ChangeMessageVisibilityBatchCommand } = require('@aws-sdk/client-sqs')
|
|
5
6
|
const { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectsCommand } = require("@aws-sdk/client-s3")
|
|
6
7
|
|
|
7
|
-
|
|
8
8
|
class ACSQS {
|
|
9
|
-
constructor({ region = 'eu-central-1', account, availableLists, useS3 = { enabled: true, bucket: undefined }, messageThreshold = 250e3, debug, logger=console, throwError = false }) {
|
|
9
|
+
constructor({ region = 'eu-central-1', account, availableLists, useS3 = { enabled: true, bucket: undefined }, messageThreshold = 250e3, debug, logger = console, throwError = false, maxConcurrentMessages = 3000 }) {
|
|
10
10
|
this.region = region
|
|
11
11
|
this.account = account
|
|
12
12
|
this.availableLists = availableLists
|
|
13
13
|
this.logger = logger
|
|
14
14
|
this.throwError = throwError
|
|
15
|
-
this.
|
|
15
|
+
this.maxConcurrentMessages = maxConcurrentMessages
|
|
16
|
+
|
|
17
|
+
// Improved visibility management
|
|
18
|
+
this.visibilityManagement = new Map()
|
|
19
|
+
this.batchExtendRunning = false
|
|
20
|
+
this.stopBatchExtend = false
|
|
21
|
+
this.batchExtendInterval = 5000 // Check every 5 seconds
|
|
16
22
|
|
|
17
23
|
const awsConfig = {
|
|
18
24
|
region,
|
|
@@ -26,6 +32,281 @@ class ACSQS {
|
|
|
26
32
|
this.bucket = _.get(useS3, 'bucket')
|
|
27
33
|
this.s3 = new S3Client(awsConfig)
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
// Start batch extend loop
|
|
37
|
+
this.startBatchExtendTimer()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async startBatchExtendTimer() {
|
|
41
|
+
if (this.batchExtendRunning) return
|
|
42
|
+
|
|
43
|
+
this.batchExtendRunning = true
|
|
44
|
+
this.stopBatchExtend = false
|
|
45
|
+
|
|
46
|
+
while (true) {
|
|
47
|
+
try {
|
|
48
|
+
if (this.stopBatchExtend) break
|
|
49
|
+
|
|
50
|
+
await sleep(this.batchExtendInterval)
|
|
51
|
+
|
|
52
|
+
if (this.stopBatchExtend) break
|
|
53
|
+
|
|
54
|
+
await this.processBatchExtensions()
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger.error('ACSQS | startBatchExtendTimer | Error in batch extend loop | %s', error?.message)
|
|
58
|
+
// Don't break the loop on errors, just log and continue
|
|
59
|
+
await sleep(1000) // Wait 1s on error before retrying
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.batchExtendRunning = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stopBatchExtendTimer() {
|
|
67
|
+
this.stopBatchExtend = true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async processBatchExtensions() {
|
|
71
|
+
if (this.visibilityManagement.size === 0) return
|
|
72
|
+
|
|
73
|
+
// Group messages by queue for batch processing
|
|
74
|
+
const queueGroups = new Map()
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
|
|
77
|
+
for (const [messageId, messageData] of this.visibilityManagement) {
|
|
78
|
+
// Check if message still exists in tracking (might have been deleted)
|
|
79
|
+
if (!this.visibilityManagement.has(messageId)) continue
|
|
80
|
+
|
|
81
|
+
// Check if message needs extension
|
|
82
|
+
if (now >= messageData.nextExtendTime) {
|
|
83
|
+
// Check max extensions
|
|
84
|
+
if (messageData.extensionCount >= messageData.maxExtensions) {
|
|
85
|
+
this.logger.warn('ACSQS | processBatchExtensions | Max extensions reached | %s | %s', messageData.queueName, messageId)
|
|
86
|
+
this.removeVisibilityTracking(messageId)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!queueGroups.has(messageData.queueName)) {
|
|
91
|
+
queueGroups.set(messageData.queueName, [])
|
|
92
|
+
}
|
|
93
|
+
queueGroups.get(messageData.queueName).push(messageData)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Process each queue's extensions in batch
|
|
98
|
+
for (const [queueName, messages] of queueGroups) {
|
|
99
|
+
await this.extendVisibilityBatch(queueName, messages)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async extendVisibilityBatch(queueName, messages) {
|
|
104
|
+
if (messages.length === 0) return
|
|
105
|
+
|
|
106
|
+
const config = _.find(this.availableLists, { name: queueName })
|
|
107
|
+
if (!config) return
|
|
108
|
+
|
|
109
|
+
const chunks = this.chunkMessages(messages)
|
|
110
|
+
await this.processAllChunks(queueName, chunks, config)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
chunkMessages(messages) {
|
|
114
|
+
return _.chunk(messages, 10)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async processAllChunks(queueName, chunks, config) {
|
|
118
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
119
|
+
const chunk = chunks[i]
|
|
120
|
+
|
|
121
|
+
// Add delay between chunks to avoid throttling (except for first chunk)
|
|
122
|
+
if (i > 0) {
|
|
123
|
+
await sleep(100)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.processChunk(queueName, chunk, config, i + 1, chunks.length)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async processChunk(queueName, chunk, config, chunkNumber, totalChunks) {
|
|
131
|
+
const validChunk = this.getValidChunk(chunk)
|
|
132
|
+
if (validChunk.length === 0) return
|
|
133
|
+
|
|
134
|
+
const sqsParams = await this.buildSQSParams(validChunk, config)
|
|
135
|
+
await this.executeChunkWithRetry(queueName, validChunk, sqsParams, config, chunkNumber, totalChunks)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getValidChunk(chunk) {
|
|
139
|
+
return chunk.filter(messageData => this.visibilityManagement.has(messageData.messageId))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async buildSQSParams(validChunk, config) {
|
|
143
|
+
const visibilityTimeout = _.get(config, 'visibilityTimeout', 30)
|
|
144
|
+
const entries = validChunk.map(messageData => ({
|
|
145
|
+
Id: messageData.messageId,
|
|
146
|
+
ReceiptHandle: messageData.receiptHandle,
|
|
147
|
+
VisibilityTimeout: visibilityTimeout
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
QueueUrl: await this.getQueueUrl(config),
|
|
152
|
+
Entries: entries
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async executeChunkWithRetry(queueName, validChunk, sqsParams, config, chunkNumber, totalChunks) {
|
|
157
|
+
const maxRetries = 2
|
|
158
|
+
let lastError = null
|
|
159
|
+
|
|
160
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
161
|
+
if (attempt > 1) {
|
|
162
|
+
await this.handleRetryDelay(lastError, attempt, maxRetries, queueName, chunkNumber, totalChunks)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const command = new ChangeMessageVisibilityBatchCommand(sqsParams)
|
|
167
|
+
const response = await this.sqs.send(command)
|
|
168
|
+
|
|
169
|
+
const visibilityTimeout = _.get(config, 'visibilityTimeout', 30)
|
|
170
|
+
this.handleChunkResponse(queueName, validChunk, response, config, visibilityTimeout)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
lastError = error
|
|
175
|
+
|
|
176
|
+
if (this.isPermanentError(error)) {
|
|
177
|
+
this.logger.warn('ACSQS | processChunk | Permanent error, not retrying | %s | %s', queueName, error.message)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (attempt < maxRetries) {
|
|
182
|
+
this.logger.warn('ACSQS | processChunk | Attempt %s failed, will retry | %s | %s',
|
|
183
|
+
attempt, queueName, error.message)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.handleAllRetriesFailed(queueName, validChunk, lastError)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async handleRetryDelay(lastError, attempt, maxRetries, queueName, chunkNumber, totalChunks) {
|
|
192
|
+
const isThrottled = lastError?.message?.includes('throttled')
|
|
193
|
+
const delayMs = isThrottled ? 500 : 200
|
|
194
|
+
|
|
195
|
+
this.logger.warn('ACSQS | processChunk | Retry attempt %s/%s | %s | Chunk %s/%s | Waiting %sms',
|
|
196
|
+
attempt, maxRetries, queueName, chunkNumber, totalChunks, delayMs)
|
|
197
|
+
|
|
198
|
+
await sleep(delayMs)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
isPermanentError(error) {
|
|
202
|
+
return error.message?.includes('ReceiptHandleIsInvalid') ||
|
|
203
|
+
error.message?.includes('MessageNotInflight')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
handleAllRetriesFailed(queueName, validChunk, lastError) {
|
|
207
|
+
this.logger.error('ACSQS | processChunk | All retries failed | %s | Chunk size: %s | %s',
|
|
208
|
+
queueName, validChunk.length, lastError?.message)
|
|
209
|
+
|
|
210
|
+
for (const messageData of validChunk) {
|
|
211
|
+
this.removeVisibilityTracking(messageData.messageId)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
handleChunkResponse(queueName, validChunk, response, config, visibilityTimeout) {
|
|
216
|
+
const successful = new Set((response.Successful || []).map(s => s.Id))
|
|
217
|
+
const failed = new Set((response.Failed || []).map(f => f.Id))
|
|
218
|
+
|
|
219
|
+
for (const messageData of validChunk) {
|
|
220
|
+
// Check again if message still exists (might have been deleted during AWS call)
|
|
221
|
+
if (!this.visibilityManagement.has(messageData.messageId)) continue
|
|
222
|
+
|
|
223
|
+
if (successful.has(messageData.messageId)) {
|
|
224
|
+
this.updateSuccessfulMessage(messageData, visibilityTimeout, queueName, config)
|
|
225
|
+
}
|
|
226
|
+
else if (failed.has(messageData.messageId)) {
|
|
227
|
+
this.handleFailedMessage(messageData, response.Failed, queueName, config)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
updateSuccessfulMessage(messageData, visibilityTimeout, queueName, config) {
|
|
233
|
+
messageData.extensionCount++
|
|
234
|
+
messageData.nextExtendTime = Date.now() + (visibilityTimeout * 0.8 * 1000)
|
|
235
|
+
|
|
236
|
+
if (config.debug) {
|
|
237
|
+
this.logger.debug('ACSQS | extendVisibilityBatch | Success | %s | M %s | %ss | Count: %s',
|
|
238
|
+
queueName, messageData.messageId, visibilityTimeout, messageData.extensionCount)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
handleFailedMessage(messageData, failedItems, queueName, config) {
|
|
243
|
+
const failedItem = failedItems.find(f => f.Id === messageData.messageId)
|
|
244
|
+
|
|
245
|
+
// Log only if it's not an expired receipt handle (which is normal)
|
|
246
|
+
if (failedItem?.Code === 'ReceiptHandleIsInvalid') {
|
|
247
|
+
if (config.debug) {
|
|
248
|
+
this.logger.debug('ACSQS | extendVisibilityBatch | Receipt handle expired (normal) | %s | M %s',
|
|
249
|
+
queueName, messageData.messageId)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
this.logger.warn('ACSQS | extendVisibilityBatch | Failed | %s | M %s | %s',
|
|
254
|
+
queueName, messageData.messageId, failedItem?.Message || 'Unknown error')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Remove from tracking if receipt handle expired or other permanent error
|
|
258
|
+
if (failedItem?.Code === 'ReceiptHandleIsInvalid' || failedItem?.Code === 'MessageNotInflight') {
|
|
259
|
+
this.removeVisibilityTracking(messageData.messageId)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
addVisibilityTracking(messageId, queueName, receiptHandle, config) {
|
|
264
|
+
// Check if we're at max capacity
|
|
265
|
+
if (this.visibilityManagement.size >= this.maxConcurrentMessages) {
|
|
266
|
+
this.logger.warn('ACSQS | addVisibilityTracking | Max concurrent messages reached | %s', this.maxConcurrentMessages)
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const visibilityTimeout = _.get(config, 'visibilityTimeout', 30)
|
|
271
|
+
const maxExtensions = _.get(config, 'maxVisibilityExtensions', 12)
|
|
272
|
+
|
|
273
|
+
this.visibilityManagement.set(messageId, {
|
|
274
|
+
messageId,
|
|
275
|
+
queueName,
|
|
276
|
+
receiptHandle,
|
|
277
|
+
extensionCount: 0,
|
|
278
|
+
maxExtensions,
|
|
279
|
+
nextExtendTime: Date.now() + (visibilityTimeout * 0.8 * 1000),
|
|
280
|
+
createdAt: Date.now()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
removeVisibilityTracking(messageId) {
|
|
287
|
+
this.visibilityManagement.delete(messageId)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Legacy method for backwards compatibility - now uses batch processing
|
|
291
|
+
async extendVisibility({ name, message, throwError }) {
|
|
292
|
+
const config = _.find(this.availableLists, { name })
|
|
293
|
+
if (!config) {
|
|
294
|
+
this.logger.error('AWS | extendVisibility | configurationMissing | %s', name)
|
|
295
|
+
throw new Error('configurationForListMissing')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { MessageId: messageId } = message
|
|
299
|
+
|
|
300
|
+
// Check if message is already being tracked
|
|
301
|
+
if (this.visibilityManagement.has(messageId)) {
|
|
302
|
+
// Force immediate extension by setting nextExtendTime to now
|
|
303
|
+
const messageData = this.visibilityManagement.get(messageId)
|
|
304
|
+
messageData.nextExtendTime = Date.now()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Add to tracking if not already present
|
|
309
|
+
this.addVisibilityTracking(messageId, name, message.ReceiptHandle, config)
|
|
29
310
|
}
|
|
30
311
|
|
|
31
312
|
async getAllLists({ throwError = false } = {}) {
|
|
@@ -68,7 +349,6 @@ class ACSQS {
|
|
|
68
349
|
}
|
|
69
350
|
}
|
|
70
351
|
|
|
71
|
-
|
|
72
352
|
async sendSQSMessage({ name, message, messageGroupId, deDuplicationId, delay, throwError, debug }) {
|
|
73
353
|
const config = _.find(this.availableLists, { name })
|
|
74
354
|
if (!config) {
|
|
@@ -165,62 +445,6 @@ class ACSQS {
|
|
|
165
445
|
}
|
|
166
446
|
}
|
|
167
447
|
|
|
168
|
-
async extendVisibility({ name, message, throwError }) {
|
|
169
|
-
const config = _.find(this.availableLists, { name })
|
|
170
|
-
if (!config) {
|
|
171
|
-
this.logger.error('AWS | extendVisibility | configurationMissing | %s', name)
|
|
172
|
-
throw new Error('configurationForListMissing')
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const visibilityTimeout = _.get(config, 'visibilityTimeout', 15)
|
|
176
|
-
const maxVisibilityExtensions = _.get(config, 'maxVisibilityExtensions', 12) // max number of times the extension can me made (12 x 15s = 3min)
|
|
177
|
-
|
|
178
|
-
const { MessageId: messageId, ReceiptHandle: receiptHandle } = message
|
|
179
|
-
|
|
180
|
-
// Check if we've reached maximum extensions
|
|
181
|
-
if (this.visibilityTimer[messageId] && this.visibilityTimer[messageId].visibilityExtensionCount >= maxVisibilityExtensions) {
|
|
182
|
-
this.logger.warn('ACSQS | extendVisibility | %s | M %s | Max extensions reached | %s | %j', name, messageId, maxVisibilityExtensions, message)
|
|
183
|
-
this.deleteVisibilityTimer({ messageId })
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Track extension count
|
|
188
|
-
if (this.visibilityTimer[messageId]) {
|
|
189
|
-
this.visibilityTimer[messageId].visibilityExtensionCount++
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const sqsParams = {
|
|
193
|
-
QueueUrl: await this.getQueueUrl(config),
|
|
194
|
-
ReceiptHandle: receiptHandle,
|
|
195
|
-
VisibilityTimeout: visibilityTimeout
|
|
196
|
-
}
|
|
197
|
-
const command = new ChangeMessageVisibilityCommand(sqsParams)
|
|
198
|
-
try {
|
|
199
|
-
const response = await this.sqs.send(command)
|
|
200
|
-
if (config.debug) {
|
|
201
|
-
const visibilityExtensionCount = this.visibilityTimer[messageId] ? this.visibilityTimer[messageId].visibilityExtensionCount : 0
|
|
202
|
-
this.logger.debug('ACSQS | extendVisibility | %s | M %s | %ss | %s | %j', name, messageId, visibilityTimeout, visibilityExtensionCount, message)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return response
|
|
206
|
-
}
|
|
207
|
-
catch(e) {
|
|
208
|
-
this.logger.error('ACSQS | extendVisibility | %s | %s', name, e?.message)
|
|
209
|
-
this.deleteVisibilityTimer({ messageId })
|
|
210
|
-
if (this.throwError || throwError) throw e
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
deleteVisibilityTimer({ messageId }) {
|
|
215
|
-
if (this.visibilityTimer[messageId]) {
|
|
216
|
-
clearTimeout(this.visibilityTimer[messageId].timer)
|
|
217
|
-
const self = this
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
delete self.visibilityTimer[messageId]
|
|
220
|
-
}, 1000)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
448
|
async receiveSQSMessages({ name, throwError, debug }) {
|
|
225
449
|
const config = _.find(this.availableLists, { name })
|
|
226
450
|
if (!config) {
|
|
@@ -241,7 +465,6 @@ class ACSQS {
|
|
|
241
465
|
const result = await this.sqs.send(command)
|
|
242
466
|
if (!_.size(result.Messages)) return
|
|
243
467
|
|
|
244
|
-
// Benutze Arrow-Funktion, um `this` beizubehalten
|
|
245
468
|
const messages = await Promise.all(result.Messages.map(async (message) => {
|
|
246
469
|
if (message.Body.startsWith('s3:')) {
|
|
247
470
|
const key = message.Body.replace('s3:', '')
|
|
@@ -251,27 +474,14 @@ class ACSQS {
|
|
|
251
474
|
message.s3key = key
|
|
252
475
|
}
|
|
253
476
|
catch(e) {
|
|
254
|
-
this.logger.error('ACSQS | receiveSQSMessages | s3KeyInvalid | %s', name, key)
|
|
477
|
+
this.logger.error('ACSQS | receiveSQSMessages | s3KeyInvalid | %s | %s', name, key)
|
|
255
478
|
}
|
|
256
479
|
}
|
|
257
480
|
|
|
258
481
|
if (visibilityTimeout > 0) {
|
|
259
|
-
//
|
|
260
|
-
const { MessageId: messageId } = message
|
|
261
|
-
|
|
262
|
-
const self = this // `this` als lokale Variable speichern
|
|
263
|
-
|
|
264
|
-
this.visibilityTimer[messageId] = {
|
|
265
|
-
// Arrow-Funktion für setInterval damit `this` erhalten bleibt,
|
|
266
|
-
// oder verwende die lokale Variable `self`
|
|
267
|
-
timer: setInterval(() => {
|
|
268
|
-
this.extendVisibility({ name, message })
|
|
269
|
-
.catch(e => {
|
|
270
|
-
this.logger.error('ACSQS | AutoExtendVisibility | Failed %s', e.message)
|
|
271
|
-
})
|
|
272
|
-
}, timeoutMs),
|
|
273
|
-
visibilityExtensionCount: 0
|
|
274
|
-
}
|
|
482
|
+
// Add to visibility tracking instead of individual timers
|
|
483
|
+
const { MessageId: messageId, ReceiptHandle: receiptHandle } = message
|
|
484
|
+
this.addVisibilityTracking(messageId, name, receiptHandle, config)
|
|
275
485
|
}
|
|
276
486
|
|
|
277
487
|
return message
|
|
@@ -305,12 +515,10 @@ class ACSQS {
|
|
|
305
515
|
if (item.s3key) {
|
|
306
516
|
s3keys.push({ Key: item.s3key })
|
|
307
517
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
518
|
+
// Remove from visibility tracking
|
|
519
|
+
this.removeVisibilityTracking(messageId)
|
|
311
520
|
}
|
|
312
521
|
|
|
313
|
-
|
|
314
522
|
let sqsParams = {
|
|
315
523
|
QueueUrl: await this.getQueueUrl(config),
|
|
316
524
|
Entries: entries
|
|
@@ -327,7 +535,7 @@ class ACSQS {
|
|
|
327
535
|
Objects: s3keys,
|
|
328
536
|
}
|
|
329
537
|
}
|
|
330
|
-
const command = new DeleteObjectsCommand(input)
|
|
538
|
+
const command = new DeleteObjectsCommand(input)
|
|
331
539
|
this.s3.send(command)
|
|
332
540
|
}
|
|
333
541
|
return response
|
|
@@ -338,6 +546,54 @@ class ACSQS {
|
|
|
338
546
|
}
|
|
339
547
|
}
|
|
340
548
|
|
|
549
|
+
// Cleanup method for graceful shutdown
|
|
550
|
+
async shutdown() {
|
|
551
|
+
this.stopBatchExtendTimer()
|
|
552
|
+
|
|
553
|
+
// Wait for batch extend loop to finish
|
|
554
|
+
while (this.batchExtendRunning) {
|
|
555
|
+
await sleep(100)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.visibilityManagement.clear()
|
|
559
|
+
this.logger.info('ACSQS | shutdown | Visibility management stopped and cleared')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Get visibility tracking stats for monitoring
|
|
563
|
+
getVisibilityStats() {
|
|
564
|
+
const stats = {
|
|
565
|
+
totalTracked: this.visibilityManagement.size,
|
|
566
|
+
queueBreakdown: {},
|
|
567
|
+
oldestMessage: null,
|
|
568
|
+
avgExtensions: 0
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let totalExtensions = 0
|
|
572
|
+
let oldestTime = Date.now()
|
|
573
|
+
|
|
574
|
+
for (const [messageId, data] of this.visibilityManagement) {
|
|
575
|
+
if (!stats.queueBreakdown[data.queueName]) {
|
|
576
|
+
stats.queueBreakdown[data.queueName] = 0
|
|
577
|
+
}
|
|
578
|
+
stats.queueBreakdown[data.queueName]++
|
|
579
|
+
totalExtensions += data.extensionCount
|
|
580
|
+
|
|
581
|
+
if (data.createdAt < oldestTime) {
|
|
582
|
+
oldestTime = data.createdAt
|
|
583
|
+
stats.oldestMessage = {
|
|
584
|
+
messageId,
|
|
585
|
+
age: Date.now() - data.createdAt,
|
|
586
|
+
extensions: data.extensionCount
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (this.visibilityManagement.size > 0) {
|
|
592
|
+
stats.avgExtensions = totalExtensions / this.visibilityManagement.size
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return stats
|
|
596
|
+
}
|
|
341
597
|
|
|
342
598
|
// helpers
|
|
343
599
|
async fetchS3Object({ key }) {
|
|
@@ -356,7 +612,6 @@ class ACSQS {
|
|
|
356
612
|
throw e
|
|
357
613
|
}
|
|
358
614
|
}
|
|
359
|
-
|
|
360
615
|
}
|
|
361
616
|
|
|
362
617
|
module.exports = ACSQS
|
package/package.json
CHANGED
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
"author": "Mark Poepping (https://www.admiralcloud.com)",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": "admiralcloud/ac-sqs",
|
|
6
|
-
"version": "3.2.
|
|
6
|
+
"version": "3.2.1",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@aws-sdk/client-s3": "^3.
|
|
9
|
-
"@aws-sdk/client-sqs": "^3.
|
|
8
|
+
"@aws-sdk/client-s3": "^3.846.0",
|
|
9
|
+
"@aws-sdk/client-sqs": "^3.846.0",
|
|
10
10
|
"lodash": "^4.17.21",
|
|
11
11
|
"uuid": "^11.1.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"ac-semantic-release": "^0.4.6",
|
|
15
15
|
"chai": "^4.5.0",
|
|
16
|
-
"eslint": "^9.
|
|
17
|
-
"mocha": "^11.
|
|
16
|
+
"eslint": "^9.31.0",
|
|
17
|
+
"mocha": "^11.7.1"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "mocha --reporter spec"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=20.0.0"
|
|
24
24
|
},
|
|
25
25
|
"resolutions": {
|
|
26
26
|
"mocha/chokidar/braces": "^3.0.3"
|