elasticio-sailor-nodejs 3.0.0-sailor-proxy-dev16 → 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';
@@ -58,6 +58,7 @@ class ProxyClient {
58
58
  compId: settings.COMP_ID,
59
59
  function: settings.FUNCTION
60
60
  }, proxySecret);
61
+ this.authHeader = `Bearer ${this.proxyJWT}`;
61
62
 
62
63
  this.throttles = {
63
64
  // 100 Messages per Second
@@ -154,6 +155,9 @@ class ProxyClient {
154
155
  log.warn({ reason, details }, 'Connection lost, initiating reconnection');
155
156
  this.reconnecting = true;
156
157
 
158
+ // Clean up existing message streams
159
+ this._cleanupMessageStreams();
160
+
157
161
  // Clean up the current session
158
162
  if (this.clientSession && !this.clientSession.destroyed) {
159
163
  this.clientSession.destroy();
@@ -227,7 +231,6 @@ class ProxyClient {
227
231
  return resolve();
228
232
  }
229
233
 
230
- // TODO: what if incoming message is received during graceful shutdown?
231
234
  this.clientSession.close(() => {
232
235
  log.debug('Successfully closed HTTP2 connection');
233
236
  resolve();
@@ -236,21 +239,24 @@ class ProxyClient {
236
239
  }
237
240
 
238
241
  async _ensureConnection() {
239
- 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) {
240
244
  return;
241
245
  }
242
246
 
243
247
  if (this.reconnecting) {
244
- // Wait for reconnection to complete
248
+ // Wait for reconnection to complete and ensure we have a valid session
245
249
  log.info('Waiting for reconnection to complete');
246
250
  const maxWait = 30000; // 30 seconds
247
251
  const startTime = Date.now();
248
- while (this.reconnecting && (Date.now() - startTime) < maxWait) {
252
+ while ((this.reconnecting || !this.clientSession || this.clientSession.destroyed) &&
253
+ !this.closed && (Date.now() - startTime) < maxWait) {
249
254
  await new Promise(resolve => setTimeout(resolve, 100));
250
255
  }
251
256
 
252
- if (!this.isConnected()) {
253
- 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');
254
260
  }
255
261
  return;
256
262
  }
@@ -268,6 +274,7 @@ class ProxyClient {
268
274
  const retryDelay = this.settings.PROXY_OBJECT_REQUEST_RETRY_DELAY;
269
275
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
270
276
  try {
277
+ await this._ensureConnection();
271
278
  const result = await requestFn();
272
279
  if (attempt > 0) {
273
280
  log.info({ attempt, maxRetries }, `${operationName} succeeded after retry`);
@@ -285,8 +292,9 @@ class ProxyClient {
285
292
  attempt,
286
293
  maxRetries,
287
294
  error: error.message,
288
- isRetryable
289
- }, `${operationName} failed, no more retries`);
295
+ errorCode: error.code,
296
+ errorStatusCode: error.statusCode
297
+ }, `${operationName} failed and will not be retried`);
290
298
  throw error;
291
299
  }
292
300
 
@@ -312,8 +320,6 @@ class ProxyClient {
312
320
  }
313
321
 
314
322
  async fetchMessageBody(message, logger) {
315
- await this._ensureConnection();
316
-
317
323
  const { body, headers } = message;
318
324
 
319
325
  logger.info('Checking if incoming messages is lightweight...');
@@ -336,7 +342,7 @@ class ProxyClient {
336
342
  const getObjectStream = this.clientSession.request({
337
343
  [HTTP2_HEADER_PATH]: `/object/${objectId}`,
338
344
  [HTTP2_HEADER_METHOD]: 'GET',
339
- [HTTP2_HEADER_AUTHORIZATION]: this.proxyJWT
345
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
340
346
  }).pipe(this._encryptor.createDecipher());
341
347
 
342
348
  const chunks = [];
@@ -362,13 +368,11 @@ class ProxyClient {
362
368
  }
363
369
 
364
370
  async uploadMessageBody(bodyBuf) {
365
- await this._ensureConnection();
366
-
367
371
  return this._proxyRequestWithRetries('Upload message body', () => new Promise((resolve, reject) => {
368
372
  const postMessageStream = this.clientSession.request({
369
373
  [HTTP2_HEADER_PATH]: '/object',
370
374
  [HTTP2_HEADER_METHOD]: 'POST',
371
- [HTTP2_HEADER_AUTHORIZATION]: this.proxyJWT
375
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
372
376
  });
373
377
 
374
378
  let responseData = '';
@@ -426,69 +430,107 @@ class ProxyClient {
426
430
  prefetch
427
431
  }).toString();
428
432
  log.info({ prefetch }, 'Requesting message from proxy');
429
- // TODO: what timeout is here? e.g. when flow is realtime - we might need to wait a long time...
430
433
  const getMessageStream = this.clientSession.request({
431
434
  [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
432
435
  [HTTP2_HEADER_METHOD]: 'GET',
433
- [HTTP2_HEADER_AUTHORIZATION]: this.proxyJWT
436
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
434
437
  });
435
438
  this.getMessageStreams.add(getMessageStream);
436
439
 
437
- const { headers, body } = await new Promise((resolve, reject) => {
438
- getMessageStream.on('response', (headers, flags) => {
439
- log.info({ headers, flags }, 'Connected to message stream');
440
- if (headers[HTTP2_HEADER_STATUS] !== 200) {
441
- return reject(new Error(`Failed to get message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
442
- }
443
- const messageId = headers['x-message-id'];
444
- const chunks = [];
445
- getMessageStream.on('data', chunk => {
446
- chunks.push(chunk);
447
- });
448
- getMessageStream.on('end', () => {
449
- log.info('Message stream ended by server');
450
- const body = Buffer.concat(chunks);
451
- log.info({
452
- messageId,
453
- messageSize: body.length
454
- }, 'Received complete message from server');
455
- log.trace({ body: body.toString() }, 'Message body as string');
456
- 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
+ });
457
465
  });
458
- });
459
466
 
460
- getMessageStream.on('close', () => {
461
- log.warn('Message stream closed by server');
462
- reject(new Error('Message stream closed by server'));
463
- });
464
- getMessageStream.on('error', (err) => {
465
- log.error(err, 'Error on message stream');
466
- 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
+ });
467
479
  });
468
- });
469
-
470
- const messageMetadata = this._extractMessageMetadata(headers);
471
- const message = this._decodeMessage(body, messageMetadata);
472
- log.debug({ messageMetadata, message }, 'Processing received message');
473
- 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
+ }
474
501
  }));
475
502
  } catch (err) {
503
+ log.error(err, 'Error while listening for messages');
476
504
  if (this.closed) {
477
505
  log.info('Connection closed, stopping message listener');
478
506
  break;
479
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
+ }
480
521
 
481
- if (!this.reconnecting) {
482
- log.error(err, 'Error in listenForMessages, waiting for reconnection');
483
- } else {
484
- 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');
485
530
  }
486
- await new Promise(resolve => setTimeout(resolve, 1000));
487
- } finally {
488
- log.debug('Cleaning up message streams');
489
- this.getMessageStreams.clear();
490
531
  }
491
532
  }
533
+ this.getMessageStreams.clear();
492
534
  }
493
535
 
494
536
  async stopListeningForMessages() {
@@ -545,8 +587,6 @@ class ProxyClient {
545
587
  metadata,
546
588
  forceProtocolVersion
547
589
  }) {
548
- await this._ensureConnection();
549
-
550
590
  const throttledSend = this.throttles[type];
551
591
  if (throttledSend) {
552
592
  log.debug({ incomingMessageId, type, metadata }, 'Applying rate limiting for message send');
@@ -582,8 +622,8 @@ class ProxyClient {
582
622
  const postMessageStream = this.clientSession.request({
583
623
  [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
584
624
  [HTTP2_HEADER_METHOD]: 'POST',
585
- [HTTP2_HEADER_AUTHORIZATION]: this.proxyJWT,
586
- [MESSAGE_METADATA_QUERY_PARAM]: JSON.stringify(preparedMetadata)
625
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader,
626
+ [MESSAGE_METADATA_HEADER]: JSON.stringify(preparedMetadata)
587
627
  });
588
628
  if (preparedData) {
589
629
  postMessageStream.write(preparedData);
@@ -651,8 +691,6 @@ class ProxyClient {
651
691
  }
652
692
 
653
693
  async finishProcessing(metadata, status) {
654
- await this._ensureConnection();
655
-
656
694
  if (Object.values(MESSAGE_PROCESSING_STATUS).indexOf(status) === -1) {
657
695
  throw new Error(`Invalid message processing status: ${status}`);
658
696
  }
@@ -667,7 +705,7 @@ class ProxyClient {
667
705
  const postMessageStream = this.clientSession.request({
668
706
  [HTTP2_HEADER_PATH]: `/finish-processing?${queryParams}`,
669
707
  [HTTP2_HEADER_METHOD]: 'POST',
670
- [HTTP2_HEADER_AUTHORIZATION]: this.proxyJWT
708
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
671
709
  });
672
710
  postMessageStream.end();
673
711
 
@@ -702,8 +740,6 @@ class ProxyClient {
702
740
  }
703
741
 
704
742
  async sendError(err, outgoingMetadata, originalMessage, metadata) {
705
- await this._ensureConnection();
706
-
707
743
  const encryptedError = this._encryptor.encryptMessageContent({
708
744
  name: err.name,
709
745
  message: err.message,
@@ -741,8 +777,6 @@ class ProxyClient {
741
777
  }
742
778
 
743
779
  async sendRebound(reboundError, metadata, outgoingMetadata) {
744
- await this._ensureConnection();
745
-
746
780
  outgoingMetadata.end = new Date().getTime();
747
781
  outgoingMetadata.reboundReason = reboundError.message;
748
782
  return this.sendMessage({
@@ -753,8 +787,6 @@ class ProxyClient {
753
787
  }
754
788
 
755
789
  async sendSnapshot(data, metadata) {
756
- await this._ensureConnection();
757
-
758
790
  const payload = JSON.stringify(data);
759
791
  return this.sendMessage({
760
792
  type: 'snapshot',
@@ -765,20 +797,20 @@ class ProxyClient {
765
797
 
766
798
  _extractMessageMetadata(headers) {
767
799
  log.trace({ headers }, 'Extracting message metadata');
768
- const messageMetadataQuery = headers[MESSAGE_METADATA_QUERY_PARAM];
769
- if (!messageMetadataQuery) {
800
+ const messageMetadata = headers[MESSAGE_METADATA_HEADER];
801
+ if (!messageMetadata) {
770
802
  log.error({ headers }, 'Missing metadata in message stream response');
771
803
  throw new Error('Missing metadata in message stream response');
772
804
  }
773
805
 
774
806
  let parsedMessageMetadata;
775
807
  try {
776
- parsedMessageMetadata = JSON.parse(messageMetadataQuery);
808
+ parsedMessageMetadata = JSON.parse(messageMetadata);
777
809
  } catch (e) {
778
- log.error({ messageMetadataQuery }, 'Failed to parse metadata JSON');
810
+ log.error({ messageMetadata: messageMetadata }, 'Failed to parse metadata JSON');
779
811
  throw new Error('Failed to parse metadata JSON');
780
812
  }
781
- log.debug({ parsedMessageMetadata }, 'Parsed metadata from query parameter');
813
+ log.debug({ parsedMessageMetadata }, 'Parsed metadata from header');
782
814
 
783
815
  // Get meta headers
784
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-dev16",
4
+ "version": "3.0.0-sailor-proxy-dev18",
5
5
  "main": "run.js",
6
6
  "scripts": {
7
7
  "build": "tsc",