elasticio-sailor-nodejs 3.0.0-sailor-proxy-dev17 → 3.0.0-sailor-proxy-dev19

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/config/local.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "ELASTICIO_WORKSPACE_ID": "69b26526c4796609ca0da128",
8
8
  "ELASTICIO_USER_ID": "69b26526c4796609ca0da127",
9
9
  "ELASTICIO_COMP_ID": "69b26526c4796609ca0da126",
10
- "ELASTICIO_FUNCTION": "rebound_trigger",
10
+ "ELASTICIO_FUNCTION": "data_trigger",
11
11
  "ELASTICIO_API_URI": "http://localhost:9000",
12
12
  "ELASTICIO_API_USERNAME": "task-692ee23d4ab5d34bb7321559",
13
13
  "ELASTICIO_API_KEY": "976ffec8-455b-494e-9478-2d66761f4040",
@@ -17,7 +17,7 @@ const {
17
17
  NGHTTP2_NO_ERROR
18
18
  } = http2.constants;
19
19
 
20
- const MESSAGE_METADATA_QUERY_PARAM = 'message-metadata';
20
+ const MESSAGE_METADATA_HEADER = 'message-metadata';
21
21
  const HEADER_ROUTING_KEY = 'x-eio-routing-key';
22
22
  const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
23
23
  const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
@@ -155,6 +155,9 @@ class ProxyClient {
155
155
  log.warn({ reason, details }, 'Connection lost, initiating reconnection');
156
156
  this.reconnecting = true;
157
157
 
158
+ // Clean up existing message streams
159
+ this._cleanupMessageStreams();
160
+
158
161
  // Clean up the current session
159
162
  if (this.clientSession && !this.clientSession.destroyed) {
160
163
  this.clientSession.destroy();
@@ -228,7 +231,6 @@ class ProxyClient {
228
231
  return resolve();
229
232
  }
230
233
 
231
- // TODO: what if incoming message is received during graceful shutdown?
232
234
  this.clientSession.close(() => {
233
235
  log.debug('Successfully closed HTTP2 connection');
234
236
  resolve();
@@ -237,21 +239,24 @@ class ProxyClient {
237
239
  }
238
240
 
239
241
  async _ensureConnection() {
240
- if (this.isConnected()) {
242
+ // Check if we have a valid connection (not just "isConnected" which includes reconnecting state)
243
+ if (!this.closed && this.clientSession && !this.clientSession.destroyed) {
241
244
  return;
242
245
  }
243
246
 
244
247
  if (this.reconnecting) {
245
- // Wait for reconnection to complete
248
+ // Wait for reconnection to complete and ensure we have a valid session
246
249
  log.info('Waiting for reconnection to complete');
247
250
  const maxWait = 30000; // 30 seconds
248
251
  const startTime = Date.now();
249
- while (this.reconnecting && (Date.now() - startTime) < maxWait) {
252
+ while ((this.reconnecting || !this.clientSession || this.clientSession.destroyed) &&
253
+ !this.closed && (Date.now() - startTime) < maxWait) {
250
254
  await new Promise(resolve => setTimeout(resolve, 100));
251
255
  }
252
256
 
253
- if (!this.isConnected()) {
254
- throw new Error('Failed to reconnect within timeout period');
257
+ // Double-check we have a valid session after waiting
258
+ if (this.closed || !this.clientSession || this.clientSession.destroyed) {
259
+ throw new Error('Failed to establish valid connection within timeout period');
255
260
  }
256
261
  return;
257
262
  }
@@ -269,6 +274,7 @@ class ProxyClient {
269
274
  const retryDelay = this.settings.PROXY_OBJECT_REQUEST_RETRY_DELAY;
270
275
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
271
276
  try {
277
+ await this._ensureConnection();
272
278
  const result = await requestFn();
273
279
  if (attempt > 0) {
274
280
  log.info({ attempt, maxRetries }, `${operationName} succeeded after retry`);
@@ -286,8 +292,9 @@ class ProxyClient {
286
292
  attempt,
287
293
  maxRetries,
288
294
  error: error.message,
289
- isRetryable
290
- }, `${operationName} failed, no more retries`);
295
+ errorCode: error.code,
296
+ errorStatusCode: error.statusCode
297
+ }, `${operationName} failed and will not be retried`);
291
298
  throw error;
292
299
  }
293
300
 
@@ -313,8 +320,6 @@ class ProxyClient {
313
320
  }
314
321
 
315
322
  async fetchMessageBody(message, logger) {
316
- await this._ensureConnection();
317
-
318
323
  const { body, headers } = message;
319
324
 
320
325
  logger.info('Checking if incoming messages is lightweight...');
@@ -363,8 +368,6 @@ class ProxyClient {
363
368
  }
364
369
 
365
370
  async uploadMessageBody(bodyBuf) {
366
- await this._ensureConnection();
367
-
368
371
  return this._proxyRequestWithRetries('Upload message body', () => new Promise((resolve, reject) => {
369
372
  const postMessageStream = this.clientSession.request({
370
373
  [HTTP2_HEADER_PATH]: '/object',
@@ -427,7 +430,6 @@ class ProxyClient {
427
430
  prefetch
428
431
  }).toString();
429
432
  log.info({ prefetch }, 'Requesting message from proxy');
430
- // TODO: what timeout is here? e.g. when flow is realtime - we might need to wait a long time...
431
433
  const getMessageStream = this.clientSession.request({
432
434
  [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
433
435
  [HTTP2_HEADER_METHOD]: 'GET',
@@ -435,61 +437,100 @@ class ProxyClient {
435
437
  });
436
438
  this.getMessageStreams.add(getMessageStream);
437
439
 
438
- const { headers, body } = await new Promise((resolve, reject) => {
439
- getMessageStream.on('response', (headers, flags) => {
440
- log.info({ headers, flags }, 'Connected to message stream');
441
- if (headers[HTTP2_HEADER_STATUS] !== 200) {
442
- return reject(new Error(`Failed to get message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
443
- }
444
- const messageId = headers['x-message-id'];
445
- const chunks = [];
446
- getMessageStream.on('data', chunk => {
447
- chunks.push(chunk);
448
- });
449
- getMessageStream.on('end', () => {
450
- log.info('Message stream ended by server');
451
- const body = Buffer.concat(chunks);
452
- log.info({
453
- messageId,
454
- messageSize: body.length
455
- }, 'Received complete message from server');
456
- log.trace({ body: body.toString() }, 'Message body as string');
457
- resolve({ headers, body });
440
+ try {
441
+ const { headers, body } = await new Promise((resolve, reject) => {
442
+ getMessageStream.on('response', (headers, flags) => {
443
+ log.info({ headers, flags }, 'Connected to message stream');
444
+ if (headers[HTTP2_HEADER_STATUS] === 204) {
445
+ log.info('Received empty message stream, closing stream and going to request again');
446
+ getMessageStream.close(NGHTTP2_NO_ERROR);
447
+ return;
448
+ } else if (headers[HTTP2_HEADER_STATUS] !== 200) {
449
+ return reject(new Error(`Failed to get message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
450
+ }
451
+ const chunks = [];
452
+ getMessageStream.on('data', chunk => {
453
+ chunks.push(chunk);
454
+ });
455
+ getMessageStream.on('end', () => {
456
+ log.info('Message stream ended by server');
457
+ const body = Buffer.concat(chunks);
458
+ log.info({
459
+ messageId: headers.messageId,
460
+ messageSize: body.length
461
+ }, 'Received complete message from server');
462
+ log.trace({ body: body.toString() }, 'Message body as string');
463
+ resolve({ headers, body });
464
+ });
458
465
  });
459
- });
460
466
 
461
- getMessageStream.on('close', () => {
462
- log.warn('Message stream closed by server');
463
- reject(new Error('Message stream closed by server'));
464
- });
465
- getMessageStream.on('error', (err) => {
466
- log.error(err, 'Error on message stream');
467
- reject(err);
467
+ getMessageStream.on('close', () => {
468
+ log.warn('Message stream closed by server');
469
+ if (getMessageStream.rstCode !== NGHTTP2_NO_ERROR) {
470
+ reject(new Error('Message stream closed by server'));
471
+ } else {
472
+ resolve({ headers: null, body: null });
473
+ }
474
+ });
475
+ getMessageStream.on('error', (err) => {
476
+ log.error(err, 'Error on message stream');
477
+ reject(err);
478
+ });
468
479
  });
469
- });
470
-
471
- const messageMetadata = this._extractMessageMetadata(headers);
472
- const message = this._decodeMessage(body, messageMetadata);
473
- log.debug({ messageMetadata, message }, 'Processing received message');
474
- await messageHandler(messageMetadata, message);
480
+ if (headers === null && body === null) {
481
+ // Stream was closed without data (e.g. 204 No Content), just return to listen for the next messageMetadata
482
+ return;
483
+ }
484
+
485
+ const messageMetadata = this._extractMessageMetadata(headers);
486
+ const message = this._decodeMessage(body, messageMetadata);
487
+ log.debug({ messageMetadata, message }, 'Processing received message');
488
+ await messageHandler(messageMetadata, message);
489
+ } finally {
490
+ // Remove this specific stream from the tracking set
491
+ this.getMessageStreams.delete(getMessageStream);
492
+ // Ensure the stream is properly closed
493
+ if (!getMessageStream.closed && !getMessageStream.destroyed) {
494
+ try {
495
+ getMessageStream.destroy();
496
+ } catch (err) {
497
+ log.debug({ err }, 'Error closing message stream');
498
+ }
499
+ }
500
+ }
475
501
  }));
476
502
  } catch (err) {
503
+ log.error(err, 'Error while listening for messages');
477
504
  if (this.closed) {
478
505
  log.info('Connection closed, stopping message listener');
479
506
  break;
480
507
  }
508
+ // Clean up any invalid streams from the error
509
+ this._cleanupMessageStreams();
510
+ } finally {
511
+ // Note: _cleanupMessageStreams() is called in the catch block for errors
512
+ // and in _handleDisconnection() for disconnections, so we only clear here
513
+ // for normal completion (which shouldn't happen in the infinite loop)
514
+ if (this.closed || !this.listeningForMessages) {
515
+ log.debug('Cleaning up message streams on exit');
516
+ this._cleanupMessageStreams();
517
+ }
518
+ }
519
+ }
520
+ }
481
521
 
482
- if (!this.reconnecting) {
483
- log.error(err, 'Error in listenForMessages, waiting for reconnection');
484
- } else {
485
- log.debug('Currently reconnecting, will retry listening for messages after reconnection');
522
+ _cleanupMessageStreams() {
523
+ log.debug({ streamCount: this.getMessageStreams.size }, 'Cleaning up message streams due to disconnection');
524
+ for (const stream of this.getMessageStreams) {
525
+ if (stream && !stream.closed && !stream.destroyed) {
526
+ try {
527
+ stream.destroy();
528
+ } catch (err) {
529
+ log.debug({ err }, 'Error destroying message stream');
486
530
  }
487
- await new Promise(resolve => setTimeout(resolve, 1000));
488
- } finally {
489
- log.debug('Cleaning up message streams');
490
- this.getMessageStreams.clear();
491
531
  }
492
532
  }
533
+ this.getMessageStreams.clear();
493
534
  }
494
535
 
495
536
  async stopListeningForMessages() {
@@ -522,6 +563,7 @@ class ProxyClient {
522
563
  ...metadata,
523
564
  messageId: metadata.messageId || uuid.v4()
524
565
  };
566
+ delete preparedMetadata.reboundIteration;
525
567
  let preparedData = data;
526
568
  if (preparedData && preparedData.headers) {
527
569
  preparedData.headers = _.omitBy(
@@ -546,8 +588,6 @@ class ProxyClient {
546
588
  metadata,
547
589
  forceProtocolVersion
548
590
  }) {
549
- await this._ensureConnection();
550
-
551
591
  const throttledSend = this.throttles[type];
552
592
  if (throttledSend) {
553
593
  log.debug({ incomingMessageId, type, metadata }, 'Applying rate limiting for message send');
@@ -584,7 +624,7 @@ class ProxyClient {
584
624
  [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
585
625
  [HTTP2_HEADER_METHOD]: 'POST',
586
626
  [HTTP2_HEADER_AUTHORIZATION]: this.authHeader,
587
- [MESSAGE_METADATA_QUERY_PARAM]: JSON.stringify(preparedMetadata)
627
+ [MESSAGE_METADATA_HEADER]: JSON.stringify(preparedMetadata)
588
628
  });
589
629
  if (preparedData) {
590
630
  postMessageStream.write(preparedData);
@@ -652,16 +692,15 @@ class ProxyClient {
652
692
  }
653
693
 
654
694
  async finishProcessing(metadata, status) {
655
- await this._ensureConnection();
656
-
657
695
  if (Object.values(MESSAGE_PROCESSING_STATUS).indexOf(status) === -1) {
658
696
  throw new Error(`Invalid message processing status: ${status}`);
659
697
  }
660
- const incomingMessageId = metadata.messageId;
661
- log.debug({ incomingMessageId, status }, 'Finishing processing of message');
698
+ const { messageId: incomingMessageId, reboundIteration } = metadata;
699
+ log.debug({ incomingMessageId, status, reboundIteration }, 'Finishing processing of message');
662
700
  const queryParams = new URLSearchParams({
663
701
  incomingMessageId,
664
- status
702
+ status,
703
+ ...(typeof reboundIteration !== 'undefined' ? { reboundIteration } : {})
665
704
  }).toString();
666
705
 
667
706
  await this._proxyRequestWithRetries('Finish processing', () => new Promise((resolve, reject) => {
@@ -703,8 +742,6 @@ class ProxyClient {
703
742
  }
704
743
 
705
744
  async sendError(err, outgoingMetadata, originalMessage, metadata) {
706
- await this._ensureConnection();
707
-
708
745
  const encryptedError = this._encryptor.encryptMessageContent({
709
746
  name: err.name,
710
747
  message: err.message,
@@ -742,8 +779,6 @@ class ProxyClient {
742
779
  }
743
780
 
744
781
  async sendRebound(reboundError, metadata, outgoingMetadata) {
745
- await this._ensureConnection();
746
-
747
782
  outgoingMetadata.end = new Date().getTime();
748
783
  outgoingMetadata.reboundReason = reboundError.message;
749
784
  return this.sendMessage({
@@ -754,8 +789,6 @@ class ProxyClient {
754
789
  }
755
790
 
756
791
  async sendSnapshot(data, metadata) {
757
- await this._ensureConnection();
758
-
759
792
  const payload = JSON.stringify(data);
760
793
  return this.sendMessage({
761
794
  type: 'snapshot',
@@ -766,20 +799,20 @@ class ProxyClient {
766
799
 
767
800
  _extractMessageMetadata(headers) {
768
801
  log.trace({ headers }, 'Extracting message metadata');
769
- const messageMetadataQuery = headers[MESSAGE_METADATA_QUERY_PARAM];
770
- if (!messageMetadataQuery) {
802
+ const messageMetadata = headers[MESSAGE_METADATA_HEADER];
803
+ if (!messageMetadata) {
771
804
  log.error({ headers }, 'Missing metadata in message stream response');
772
805
  throw new Error('Missing metadata in message stream response');
773
806
  }
774
807
 
775
808
  let parsedMessageMetadata;
776
809
  try {
777
- parsedMessageMetadata = JSON.parse(messageMetadataQuery);
810
+ parsedMessageMetadata = JSON.parse(messageMetadata);
778
811
  } catch (e) {
779
- log.error({ messageMetadataQuery }, 'Failed to parse metadata JSON');
812
+ log.error({ messageMetadata: messageMetadata }, 'Failed to parse metadata JSON');
780
813
  throw new Error('Failed to parse metadata JSON');
781
814
  }
782
- log.debug({ parsedMessageMetadata }, 'Parsed metadata from query parameter');
815
+ log.debug({ parsedMessageMetadata }, 'Parsed metadata from header');
783
816
 
784
817
  // Get meta headers
785
818
  const metaHeaderNames = Object.keys(parsedMessageMetadata)
@@ -804,6 +837,9 @@ class ProxyClient {
804
837
  if (parsedMessageMetadata.reply_to) {
805
838
  result.reply_to = parsedMessageMetadata.reply_to;
806
839
  }
840
+ if (parsedMessageMetadata.reboundIteration) {
841
+ result.reboundIteration = parsedMessageMetadata.reboundIteration;
842
+ }
807
843
  log.debug({ result }, 'Extracted message metadata');
808
844
  return result;
809
845
  }
package/lib/settings.js CHANGED
@@ -19,17 +19,10 @@ function getOptionalEnvVars(envVars) {
19
19
  PROXY_OBJECT_REQUEST_RETRY_DELAY: 100,
20
20
  PROXY_OBJECT_REQUEST_MAX_RETRY_DELAY: 5 * 60 * 1000, // 5 mins
21
21
 
22
- // TODO: Move to proxy?
23
22
  DATA_RATE_LIMIT: 10, // 10 data events every 100ms
24
23
  ERROR_RATE_LIMIT: 2, // 2 errors every 100ms
25
24
  SNAPSHOT_RATE_LIMIT: 2, // 2 Snapshots every 100ms
26
25
  RATE_INTERVAL: 100, // 100ms
27
- PROCESS_AMQP_DRAIN: true,
28
- AMQP_PUBLISH_RETRY_DELAY: 100, // 100ms
29
- AMQP_PUBLISH_RETRY_ATTEMPTS: Infinity,
30
- AMQP_PUBLISH_MAX_RETRY_DELAY: 5 * 60 * 1000, // 5 mins
31
- // Should be defaulted to true and moved to proxy
32
- AMQP_PERSISTENT_MESSAGES: false,
33
26
 
34
27
  OBJECT_STORAGE_SIZE_THRESHOLD: 1048576,
35
28
  OUTGOING_MESSAGE_SIZE_LIMIT: 10485760,
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": "3.0.0-sailor-proxy-dev17",
4
+ "version": "3.0.0-sailor-proxy-dev19",
5
5
  "main": "run.js",
6
6
  "scripts": {
7
7
  "build": "tsc",