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

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() {
@@ -546,8 +587,6 @@ class ProxyClient {
546
587
  metadata,
547
588
  forceProtocolVersion
548
589
  }) {
549
- await this._ensureConnection();
550
-
551
590
  const throttledSend = this.throttles[type];
552
591
  if (throttledSend) {
553
592
  log.debug({ incomingMessageId, type, metadata }, 'Applying rate limiting for message send');
@@ -584,7 +623,7 @@ class ProxyClient {
584
623
  [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
585
624
  [HTTP2_HEADER_METHOD]: 'POST',
586
625
  [HTTP2_HEADER_AUTHORIZATION]: this.authHeader,
587
- [MESSAGE_METADATA_QUERY_PARAM]: JSON.stringify(preparedMetadata)
626
+ [MESSAGE_METADATA_HEADER]: JSON.stringify(preparedMetadata)
588
627
  });
589
628
  if (preparedData) {
590
629
  postMessageStream.write(preparedData);
@@ -652,8 +691,6 @@ class ProxyClient {
652
691
  }
653
692
 
654
693
  async finishProcessing(metadata, status) {
655
- await this._ensureConnection();
656
-
657
694
  if (Object.values(MESSAGE_PROCESSING_STATUS).indexOf(status) === -1) {
658
695
  throw new Error(`Invalid message processing status: ${status}`);
659
696
  }
@@ -703,8 +740,6 @@ class ProxyClient {
703
740
  }
704
741
 
705
742
  async sendError(err, outgoingMetadata, originalMessage, metadata) {
706
- await this._ensureConnection();
707
-
708
743
  const encryptedError = this._encryptor.encryptMessageContent({
709
744
  name: err.name,
710
745
  message: err.message,
@@ -742,8 +777,6 @@ class ProxyClient {
742
777
  }
743
778
 
744
779
  async sendRebound(reboundError, metadata, outgoingMetadata) {
745
- await this._ensureConnection();
746
-
747
780
  outgoingMetadata.end = new Date().getTime();
748
781
  outgoingMetadata.reboundReason = reboundError.message;
749
782
  return this.sendMessage({
@@ -754,8 +787,6 @@ class ProxyClient {
754
787
  }
755
788
 
756
789
  async sendSnapshot(data, metadata) {
757
- await this._ensureConnection();
758
-
759
790
  const payload = JSON.stringify(data);
760
791
  return this.sendMessage({
761
792
  type: 'snapshot',
@@ -766,20 +797,20 @@ class ProxyClient {
766
797
 
767
798
  _extractMessageMetadata(headers) {
768
799
  log.trace({ headers }, 'Extracting message metadata');
769
- const messageMetadataQuery = headers[MESSAGE_METADATA_QUERY_PARAM];
770
- if (!messageMetadataQuery) {
800
+ const messageMetadata = headers[MESSAGE_METADATA_HEADER];
801
+ if (!messageMetadata) {
771
802
  log.error({ headers }, 'Missing metadata in message stream response');
772
803
  throw new Error('Missing metadata in message stream response');
773
804
  }
774
805
 
775
806
  let parsedMessageMetadata;
776
807
  try {
777
- parsedMessageMetadata = JSON.parse(messageMetadataQuery);
808
+ parsedMessageMetadata = JSON.parse(messageMetadata);
778
809
  } catch (e) {
779
- log.error({ messageMetadataQuery }, 'Failed to parse metadata JSON');
810
+ log.error({ messageMetadata: messageMetadata }, 'Failed to parse metadata JSON');
780
811
  throw new Error('Failed to parse metadata JSON');
781
812
  }
782
- log.debug({ parsedMessageMetadata }, 'Parsed metadata from query parameter');
813
+ log.debug({ parsedMessageMetadata }, 'Parsed metadata from header');
783
814
 
784
815
  // Get meta headers
785
816
  const metaHeaderNames = Object.keys(parsedMessageMetadata)
package/lib/settings.js CHANGED
@@ -24,12 +24,6 @@ function getOptionalEnvVars(envVars) {
24
24
  ERROR_RATE_LIMIT: 2, // 2 errors every 100ms
25
25
  SNAPSHOT_RATE_LIMIT: 2, // 2 Snapshots every 100ms
26
26
  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
27
 
34
28
  OBJECT_STORAGE_SIZE_THRESHOLD: 1048576,
35
29
  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-dev18",
5
5
  "main": "run.js",
6
6
  "scripts": {
7
7
  "build": "tsc",