elasticio-sailor-nodejs 2.7.6-dev.2 → 2.7.7-dev1

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 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.reject(amqpMessage);
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.reject(amqpMessage);
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
- return this.reject(message);
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
- return this.reject(message);
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
- ack(message) {
309
- log.debug('Message #%j ack', message.fields.deliveryTag);
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
- reject(message) {
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(message);
550
+ await that.amqpConnection.reject(messageId);
549
551
  } else {
550
- that.amqpConnection.ack(message);
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: incomingMessageHeaders.messageId || 'unknown',
595
+ messageId: messageId || 'unknown',
594
596
  parentMessageId: incomingMessageHeaders.parentMessageId || 'unknown',
595
- deliveryTag
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
- self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
627
- self.amqpConnection.reject(message);
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
- self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
656
- self.amqpConnection.reject(message);
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.6-dev.2",
4
+ "version": "2.7.7-dev1",
5
5
  "main": "run.js",
6
6
  "scripts": {
7
7
  "audit": "better-npm-audit audit --level high --production",
@@ -21,7 +21,7 @@
21
21
  "bunyan": "1.8.10",
22
22
  "co": "4.6.0",
23
23
  "debug": "3.1.0",
24
- "elasticio-rest-node": "2.0.0-dev.3",
24
+ "elasticio-rest-node": "2.0.0",
25
25
  "event-to-promise": "0.8.0",
26
26
  "lodash": "4.17.21",
27
27
  "p-throttle": "2.1.0",