elasticio-sailor-nodejs 2.7.6 → 2.7.7
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 +5 -0
- package/lib/amqp.js +47 -7
- package/lib/messagesDB.js +37 -0
- package/lib/sailor.js +36 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2.7.7 (August 5, 2025)
|
|
2
|
+
|
|
3
|
+
* Improve handling of cases when connection to RabbitMQ is re-established.
|
|
4
|
+
[#7855](https://github.com/elasticio/elasticio/issues/7855)
|
|
5
|
+
|
|
1
6
|
## 2.7.6 (August 1, 2025)
|
|
2
7
|
|
|
3
8
|
* Updated `elasticio-rest-node` to version 2.0.0 to address a vulnerability
|
package/lib/amqp.js
CHANGED
|
@@ -6,6 +6,8 @@ const _ = require('lodash');
|
|
|
6
6
|
const eventToPromise = require('event-to-promise');
|
|
7
7
|
const uuid = require('uuid');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const messagesDB = require('./messagesDB.js');
|
|
10
|
+
const assert = require('assert');
|
|
9
11
|
|
|
10
12
|
const HEADER_ROUTING_KEY = 'x-eio-routing-key';
|
|
11
13
|
const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
|
|
@@ -215,14 +217,14 @@ class Amqp {
|
|
|
215
217
|
} catch (err) {
|
|
216
218
|
log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag },
|
|
217
219
|
'Error occurred while parsing message payload');
|
|
218
|
-
this.
|
|
220
|
+
this.rejectOriginal(amqpMessage);
|
|
219
221
|
return;
|
|
220
222
|
}
|
|
221
223
|
try {
|
|
222
224
|
await messageHandler(message, amqpMessage);
|
|
223
225
|
} catch (err) {
|
|
224
226
|
log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag }, 'Failed to process message, reject');
|
|
225
|
-
this.
|
|
227
|
+
this.rejectOriginal(amqpMessage);
|
|
226
228
|
}
|
|
227
229
|
});
|
|
228
230
|
log.debug({ queue }, 'Started listening for messages');
|
|
@@ -289,7 +291,10 @@ class Amqp {
|
|
|
289
291
|
'Error occurred while parsing message #%j payload',
|
|
290
292
|
message.fields.deliveryTag
|
|
291
293
|
);
|
|
292
|
-
|
|
294
|
+
// I didn't find a quick way to use reject that waits for new message when
|
|
295
|
+
// connection is re-established, so simply try to reject original message
|
|
296
|
+
// This will be fixed when we implement to Sailor Proxy
|
|
297
|
+
return this.rejectOriginal(message);
|
|
293
298
|
}
|
|
294
299
|
decryptedContent.headers = decryptedContent.headers || {};
|
|
295
300
|
if (message.properties.headers.reply_to) {
|
|
@@ -301,16 +306,51 @@ class Amqp {
|
|
|
301
306
|
callback(decryptedContent, message);
|
|
302
307
|
} catch (err) {
|
|
303
308
|
log.error(err, 'Failed to process message #%j, reject', message.fields.deliveryTag);
|
|
304
|
-
|
|
309
|
+
// I didn't find a quick way to use reject that waits for new message when
|
|
310
|
+
// connection is re-established, so simply try to reject original message
|
|
311
|
+
// This will be fixed when we implement to Sailor Proxy
|
|
312
|
+
return this.rejectOriginal(message);
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
|
|
309
|
-
|
|
316
|
+
async getMostRecentMessage(messageId) {
|
|
317
|
+
let message = messagesDB.getMessageById(messageId);
|
|
318
|
+
assert(message, `Message with ID ${messageId} not found in messagesDB`);
|
|
319
|
+
// If we stopped listening fo the new messages or the message was received
|
|
320
|
+
// from the same channel we can return it immediately
|
|
321
|
+
if (!this.consume || message.fields.consumerTag === this.consume.consumerTag) {
|
|
322
|
+
return message;
|
|
323
|
+
}
|
|
324
|
+
log.debug({ messageId }, 'Waiting for message from new channel');
|
|
325
|
+
message = await new Promise((resolve) => {
|
|
326
|
+
const onMessageUpdated = (updatedMessageId, updatedMessage) => {
|
|
327
|
+
if (updatedMessageId === messageId
|
|
328
|
+
&& updatedMessage.fields.consumerTag === this.consume.consumerTag) {
|
|
329
|
+
messagesDB.off('message-updated', onMessageUpdated);
|
|
330
|
+
resolve(updatedMessage);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
messagesDB.on('message-updated', onMessageUpdated);
|
|
334
|
+
});
|
|
335
|
+
log.debug({ messageId }, 'Message received from new channel');
|
|
336
|
+
return message;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async ack(messageId) {
|
|
340
|
+
const message = await this.getMostRecentMessage(messageId);
|
|
341
|
+
log.debug(message.fields, 'Message ack');
|
|
310
342
|
this.consumerChannel.ack(message);
|
|
343
|
+
messagesDB.deleteMessage(messageId);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async reject(messageId) {
|
|
347
|
+
const message = await this.getMostRecentMessage(messageId);
|
|
348
|
+
log.debug(message.fields, 'Message reject');
|
|
349
|
+
this.consumerChannel.reject(message, false);
|
|
350
|
+
messagesDB.deleteMessage(messageId);
|
|
311
351
|
}
|
|
312
352
|
|
|
313
|
-
|
|
353
|
+
rejectOriginal(message) {
|
|
314
354
|
log.debug('Message #%j reject', message.fields.deliveryTag);
|
|
315
355
|
return this.consumerChannel.reject(message, false);
|
|
316
356
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Simple map to store messages by their IDs
|
|
2
|
+
// This is useful when connection is re-established and we need to get the same
|
|
3
|
+
// message again, but now from the new connection
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const messagesDB = (() => {
|
|
7
|
+
const messagesById = new Map();
|
|
8
|
+
const emitter = new EventEmitter();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
getMessageById: function getMessageById(id) {
|
|
12
|
+
return messagesById.get(id);
|
|
13
|
+
},
|
|
14
|
+
addMessage: function addMessage(id, message) {
|
|
15
|
+
const existingMessage = messagesById.get(id);
|
|
16
|
+
messagesById.set(id, message);
|
|
17
|
+
if (existingMessage) {
|
|
18
|
+
emitter.emit('message-updated', id, message);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
deleteMessage: function deleteMessage(id) {
|
|
22
|
+
messagesById.delete(id);
|
|
23
|
+
},
|
|
24
|
+
on: function on(event, listener) {
|
|
25
|
+
emitter.on(event, listener);
|
|
26
|
+
},
|
|
27
|
+
off: function off(event, listener) {
|
|
28
|
+
emitter.off(event, listener);
|
|
29
|
+
},
|
|
30
|
+
__reset__: function reset() {
|
|
31
|
+
messagesById.clear();
|
|
32
|
+
emitter.removeAllListeners();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
module.exports = messagesDB;
|
package/lib/sailor.js
CHANGED
|
@@ -12,6 +12,7 @@ const co = require('co');
|
|
|
12
12
|
const pThrottle = require('p-throttle');
|
|
13
13
|
const { ObjectStorage } = require('@elastic.io/maester-client');
|
|
14
14
|
const { Readable } = require('stream');
|
|
15
|
+
const messagesDB = require('./messagesDB.js');
|
|
15
16
|
|
|
16
17
|
const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
|
|
17
18
|
const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
|
|
@@ -104,7 +105,7 @@ class Sailor {
|
|
|
104
105
|
return this.amqpConnection.disconnect();
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
reportError(err) {
|
|
108
|
+
async reportError(err) {
|
|
108
109
|
const headers = Object.assign({}, getAdditionalHeadersFromSettings(this.settings), {
|
|
109
110
|
execId: this.settings.EXEC_ID,
|
|
110
111
|
taskId: this.settings.FLOW_ID,
|
|
@@ -306,6 +307,7 @@ class Sailor {
|
|
|
306
307
|
async runExec(module, payload, message, outgoingMessageHeaders, stepData, timeStart, logger) {
|
|
307
308
|
const origPassthrough = _.cloneDeep(payload.passthrough) || {};
|
|
308
309
|
const incomingMessageHeaders = this.readIncomingMessageHeaders(message);
|
|
310
|
+
const messageId = incomingMessageHeaders.messageId;
|
|
309
311
|
const settings = this.settings;
|
|
310
312
|
const cfg = _.cloneDeep(stepData.config) || {};
|
|
311
313
|
const snapshot = _.cloneDeep(this.snapshot);
|
|
@@ -532,7 +534,7 @@ class Sailor {
|
|
|
532
534
|
}
|
|
533
535
|
}
|
|
534
536
|
|
|
535
|
-
function onEnd() {
|
|
537
|
+
async function onEnd() {
|
|
536
538
|
if (endWasEmitted) {
|
|
537
539
|
logger.warn({
|
|
538
540
|
messagesCount: that.messagesCount,
|
|
@@ -545,9 +547,9 @@ class Sailor {
|
|
|
545
547
|
endWasEmitted = true;
|
|
546
548
|
|
|
547
549
|
if (taskExec.errorCount > 0) {
|
|
548
|
-
that.amqpConnection.reject(
|
|
550
|
+
await that.amqpConnection.reject(messageId);
|
|
549
551
|
} else {
|
|
550
|
-
that.amqpConnection.ack(
|
|
552
|
+
await that.amqpConnection.ack(messageId);
|
|
551
553
|
}
|
|
552
554
|
that.messagesCount -= 1;
|
|
553
555
|
logger.trace({
|
|
@@ -586,15 +588,34 @@ class Sailor {
|
|
|
586
588
|
self.messagesCount += 1;
|
|
587
589
|
|
|
588
590
|
const timeStart = Date.now();
|
|
589
|
-
const { deliveryTag } = message.fields;
|
|
590
591
|
|
|
592
|
+
const messageId = incomingMessageHeaders.messageId;
|
|
591
593
|
const logger = log.child({
|
|
592
594
|
threadId: incomingMessageHeaders.threadId || 'unknown',
|
|
593
|
-
messageId:
|
|
595
|
+
messageId: messageId || 'unknown',
|
|
594
596
|
parentMessageId: incomingMessageHeaders.parentMessageId || 'unknown',
|
|
595
|
-
|
|
597
|
+
...message.fields
|
|
596
598
|
});
|
|
597
599
|
|
|
600
|
+
if (messageId) {
|
|
601
|
+
const alreadyExists = messagesDB.getMessageById(messageId);
|
|
602
|
+
// Add message to DB even if it already exists
|
|
603
|
+
messagesDB.addMessage(messageId, message);
|
|
604
|
+
if (alreadyExists) {
|
|
605
|
+
logger.warn({ messageId }, 'Duplicate message detected. This'
|
|
606
|
+
+ ' delivery will be ignored; the handler that first received'
|
|
607
|
+
+ ' this message will process it as part of deduplication.');
|
|
608
|
+
// If message was in messagesDB, it means that the connection was closed
|
|
609
|
+
// and this message was redelivered. In this case, the process for original
|
|
610
|
+
// message is waiting for this message to be added to DB and then ack or
|
|
611
|
+
// nack the new message, instead of the one that was delivered by closed
|
|
612
|
+
// channel
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
logger.warn('Message does not have messageId');
|
|
617
|
+
}
|
|
618
|
+
|
|
598
619
|
logger.trace({ messagesCount: this.messagesCount }, 'processMessage received');
|
|
599
620
|
|
|
600
621
|
const stepData = this.stepData;
|
|
@@ -623,8 +644,10 @@ class Sailor {
|
|
|
623
644
|
} catch (e) {
|
|
624
645
|
log.error(e);
|
|
625
646
|
outgoingMessageHeaders.end = new Date().getTime();
|
|
626
|
-
|
|
627
|
-
|
|
647
|
+
await Promise.all([
|
|
648
|
+
self.amqpConnection.sendError(e, outgoingMessageHeaders, message),
|
|
649
|
+
self.amqpConnection.reject(messageId)
|
|
650
|
+
]);
|
|
628
651
|
return;
|
|
629
652
|
}
|
|
630
653
|
|
|
@@ -652,8 +675,10 @@ class Sailor {
|
|
|
652
675
|
} catch (e) {
|
|
653
676
|
logger.error(e);
|
|
654
677
|
outgoingMessageHeaders.end = new Date().getTime();
|
|
655
|
-
|
|
656
|
-
|
|
678
|
+
await Promise.all([
|
|
679
|
+
self.amqpConnection.sendError(e, outgoingMessageHeaders, message),
|
|
680
|
+
self.amqpConnection.reject(messageId)
|
|
681
|
+
]);
|
|
657
682
|
return;
|
|
658
683
|
}
|
|
659
684
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elasticio-sailor-nodejs",
|
|
3
3
|
"description": "The official elastic.io library for bootstrapping and executing for Node.js connectors",
|
|
4
|
-
"version": "2.7.
|
|
4
|
+
"version": "2.7.7",
|
|
5
5
|
"main": "run.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"audit": "better-npm-audit audit --level high --production",
|