elasticio-sailor-nodejs 3.0.0-dev3 → 3.0.0-dev5

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.
@@ -202,6 +202,7 @@ class ProxyClient {
202
202
  return resolve();
203
203
  }
204
204
 
205
+ // TODO: what if incoming message is received during graceful shutdown?
205
206
  this.clientSession.close(() => {
206
207
  log.debug('Successfully closed HTTP2 connection');
207
208
  resolve();
@@ -262,34 +263,74 @@ class ProxyClient {
262
263
 
263
264
  logger.info('Going to fetch message body.', { objectId });
264
265
 
265
- try {
266
- const getObjectStream = this.clientSession.request({
267
- [HTTP2_HEADER_PATH]: `/object/${objectId}`,
268
- [HTTP2_HEADER_METHOD]: 'GET',
269
- [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
270
- }).pipe(this._encryptor.createDecipher());
271
- // TODO: Add retries
272
- object = await new Promise((resolve, reject) => {
273
- const chunks = [];
274
- getObjectStream.on('data', chunk => {
275
- chunks.push(chunk);
276
- });
277
- getObjectStream.on('error', (err) => {
278
- logger.error(err, 'Error during fetching message body');
279
- reject(err);
280
- });
281
- getObjectStream.on('end', () => {
282
- logger.info('Message stream ended by server');
283
- const buffer = Buffer.concat(chunks);
284
- logger.info({ messageSize: buffer.length }, 'Received complete message from server');
285
- resolve({ data: JSON.parse(buffer.toString()) });
266
+ const maxRetries = this.settings.PROXY_OBJECT_REQUEST_RETRY_ATTEMPTS;
267
+ const retryDelay = this.settings.PROXY_OBJECT_REQUEST_RETRY_DELAY;
268
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
269
+ try {
270
+ object = await new Promise((resolve, reject) => {
271
+ const getObjectStream = this.clientSession.request({
272
+ [HTTP2_HEADER_PATH]: `/object/${objectId}`,
273
+ [HTTP2_HEADER_METHOD]: 'GET',
274
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
275
+ }).pipe(this._encryptor.createDecipher());
276
+
277
+ const chunks = [];
278
+ getObjectStream.on('data', chunk => {
279
+ chunks.push(chunk);
280
+ });
281
+ getObjectStream.on('error', (err) => {
282
+ logger.error(err, 'Error during fetching message body');
283
+ reject(err);
284
+ });
285
+ getObjectStream.on('end', () => {
286
+ logger.info('Message stream ended by server');
287
+ const buffer = Buffer.concat(chunks);
288
+ logger.info({ messageSize: buffer.length }, 'Received complete message from server');
289
+ resolve({ data: JSON.parse(buffer.toString()) });
290
+ });
286
291
  });
287
- });
288
- } catch (e) {
289
- log.error(e);
290
- throw new Error(`Failed to get message body with id=${objectId}`);
291
- }
292
292
 
293
+ if (attempt > 0) {
294
+ log.info({ attempt, maxRetries }, 'Fetch message body succeeded after retry');
295
+ }
296
+ return objectId;
297
+ } catch (error) {
298
+ const isLastAttempt = attempt === maxRetries;
299
+ const isRetryable = error.isNetworkError ||
300
+ (error.statusCode && error.statusCode >= 500) ||
301
+ error.code === 'ECONNRESET' ||
302
+ error.code === 'ETIMEDOUT';
303
+
304
+ if (!isRetryable || isLastAttempt) {
305
+ log.error({
306
+ attempt,
307
+ maxRetries,
308
+ error: error.message,
309
+ isRetryable
310
+ }, 'Fetch message body failed, no more retries');
311
+ throw error;
312
+ }
313
+
314
+ const delay = Math.min(
315
+ retryDelay * Math.pow(2, attempt),
316
+ this.settings.PROXY_OBJECT_REQUEST_MAX_RETRY_DELAY
317
+ );
318
+ log.warn({
319
+ attempt,
320
+ maxRetries,
321
+ error: error.message,
322
+ nextRetryIn: delay
323
+ }, 'Fetch message body failed, retrying...');
324
+
325
+ await new Promise(resolve => setTimeout(resolve, delay));
326
+
327
+ // Ensure connection is still valid before retry
328
+ if (!this.isConnected()) {
329
+ log.info('Reconnecting before retry...');
330
+ await this.connect();
331
+ }
332
+ }
333
+ }
293
334
  logger.info('Successfully obtained message body.', { objectId });
294
335
  logger.trace('Message body object received');
295
336
 
@@ -299,43 +340,101 @@ class ProxyClient {
299
340
  async uploadMessageBody(bodyBuf) {
300
341
  await this._ensureConnection();
301
342
 
302
- return new Promise((resolve, reject) => {
303
- // TODO: Add retries
304
- const postMessageStream = this.clientSession.request({
305
- [HTTP2_HEADER_PATH]: '/object',
306
- [HTTP2_HEADER_METHOD]: 'POST',
307
- [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
308
- });
343
+ const maxRetries = this.settings.PROXY_OBJECT_REQUEST_RETRY_ATTEMPTS;
344
+ const retryDelay = this.settings.PROXY_OBJECT_REQUEST_RETRY_DELAY;
345
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
346
+ try {
347
+ const objectId = await new Promise((resolve, reject) => {
348
+ const postMessageStream = this.clientSession.request({
349
+ [HTTP2_HEADER_PATH]: '/object',
350
+ [HTTP2_HEADER_METHOD]: 'POST',
351
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
352
+ });
309
353
 
310
- postMessageStream.on('response', (headers, flags) => {
311
- const status = headers[http2.constants.HTTP2_HEADER_STATUS];
312
- if (status !== 200) {
313
- return reject(new Error(`Failed to upload message body, status code: ${status}`));
354
+ let responseData = '';
355
+ let statusCode = null;
356
+
357
+ postMessageStream.on('response', (headers, flags) => {
358
+ statusCode = headers[http2.constants.HTTP2_HEADER_STATUS];
359
+ if (statusCode !== 200) {
360
+ const error = new Error(`Failed to upload message body, status code: ${statusCode}`);
361
+ error.statusCode = statusCode;
362
+ return reject(error);
363
+ }
364
+ });
365
+
366
+ postMessageStream.on('data', chunk => {
367
+ responseData += chunk;
368
+ });
369
+
370
+ postMessageStream.on('error', (err) => {
371
+ log.error(err, 'Error during upload message body');
372
+ err.isNetworkError = true;
373
+ reject(err);
374
+ });
375
+
376
+ postMessageStream.on('end', () => {
377
+ if (!responseData) {
378
+ return
379
+ }
380
+ try {
381
+ const responseJson = JSON.parse(responseData);
382
+ resolve(responseJson.objectId);
383
+ } catch (e) {
384
+ log.error(e, 'Failed to parse upload message body response');
385
+ reject(e);
386
+ }
387
+ });
388
+
389
+ const cipher = this._encryptor.createCipher();
390
+ cipher.pipe(postMessageStream);
391
+ cipher.write(bodyBuf);
392
+ cipher.end();
393
+ });
394
+
395
+ // Success - return the objectId
396
+ if (attempt > 0) {
397
+ log.info({ attempt, maxRetries }, 'Upload message body succeeded after retry');
314
398
  }
315
- });
316
- let responseData = '';
317
- postMessageStream.on('data', chunk => {
318
- responseData += chunk;
319
- });
320
- postMessageStream.on('error', (err) => {
321
- log.error(err, 'Error during upload message body');
322
- reject(err);
323
- });
324
- postMessageStream.on('end', () => {
325
- try {
326
- const responseJson = JSON.parse(responseData);
327
- resolve(responseJson.objectId);
328
- } catch (e) {
329
- log.error(e, 'Failed to parse upload message body response');
330
- reject(e);
399
+ return objectId;
400
+
401
+ } catch (error) {
402
+ const isLastAttempt = attempt === maxRetries;
403
+ const isRetryable = error.isNetworkError ||
404
+ (error.statusCode && error.statusCode >= 500) ||
405
+ error.code === 'ECONNRESET' ||
406
+ error.code === 'ETIMEDOUT';
407
+
408
+ if (!isRetryable || isLastAttempt) {
409
+ log.error({
410
+ attempt,
411
+ maxRetries,
412
+ error: error.message,
413
+ isRetryable
414
+ }, 'Upload message body failed, no more retries');
415
+ throw error;
331
416
  }
332
- });
333
417
 
334
- const cipher = this._encryptor.createCipher();
335
- cipher.pipe(postMessageStream);
336
- cipher.write(bodyBuf);
337
- cipher.end();
338
- });
418
+ const delay = Math.min(
419
+ retryDelay * Math.pow(2, attempt),
420
+ this.settings.PROXY_OBJECT_REQUEST_MAX_RETRY_DELAY
421
+ );
422
+ log.warn({
423
+ attempt,
424
+ maxRetries,
425
+ error: error.message,
426
+ nextRetryIn: delay
427
+ }, 'Upload message body failed, retrying...');
428
+
429
+ await new Promise(resolve => setTimeout(resolve, delay));
430
+
431
+ // Ensure connection is still valid before retry
432
+ if (!this.isConnected()) {
433
+ log.info('Reconnecting before retry...');
434
+ await this.connect();
435
+ }
436
+ }
437
+ }
339
438
  }
340
439
 
341
440
  async listenForMessages(messageHandler) {
package/lib/sailor.js CHANGED
@@ -35,6 +35,8 @@ class Sailor {
35
35
  //eslint-disable-next-line new-cap
36
36
  this.apiClient = RestApiClient(
37
37
  settings.API_USERNAME,
38
+ // TODO find a way to make username and key consistent (without running api tests and looking up
39
+ // correct values in MongoDB)
38
40
  settings.API_KEY,
39
41
  {
40
42
  retryCount: settings.API_REQUEST_RETRY_ATTEMPTS,
package/lib/settings.js CHANGED
@@ -15,6 +15,9 @@ function getOptionalEnvVars(envVars) {
15
15
  PROXY_RECONNECT_MAX_DELAY: 30 * 1000, // 30 seconds
16
16
  PROXY_RECONNECT_BACKOFF_MULTIPLIER: 2,
17
17
  PROXY_RECONNECT_JITTER_FACTOR: 0.3,
18
+ PROXY_OBJECT_REQUEST_RETRY_ATTEMPTS: Infinity,
19
+ PROXY_OBJECT_REQUEST_RETRY_DELAY: 100,
20
+ PROXY_OBJECT_REQUEST_MAX_RETRY_DELAY: 5 * 60 * 1000, // 5 mins
18
21
 
19
22
  // TODO: Move to proxy?
20
23
  DATA_RATE_LIMIT: 10, // 10 data events every 100ms
package/mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ node = "18.16.1"
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-dev3",
4
+ "version": "3.0.0-dev5",
5
5
  "main": "run.js",
6
6
  "scripts": {
7
7
  "build": "tsc",
package/run.js CHANGED
@@ -57,23 +57,6 @@ async function putOutToSea(settings, ipc) {
57
57
  ipc.send('init:ended');
58
58
  }
59
59
 
60
- async function disconnectAndExit() {
61
- if (!disconnectRequired) {
62
- return;
63
- }
64
- disconnectRequired = false;
65
-
66
- try {
67
- logger.info('Disconnecting...');
68
- await sailor.disconnect();
69
- logger.info('Successfully disconnected');
70
- process.exit();
71
- } catch (err) {
72
- logger.error(err, 'Unable to disconnect');
73
- process.exit(-1);
74
- }
75
- }
76
-
77
60
  async function gracefulShutdown() {
78
61
  if (!disconnectRequired) {
79
62
  return;
@@ -90,8 +73,15 @@ async function gracefulShutdown() {
90
73
  await sailorInit;
91
74
  logger.trace('Waited an init before graceful shutdown');
92
75
 
93
- await sailor.scheduleShutdown();
94
- await disconnectAndExit();
76
+ try {
77
+ logger.info('Disconnecting...');
78
+ await sailor.scheduleShutdown();
79
+ logger.info('Successfully disconnected');
80
+ process.exit();
81
+ } catch (err) {
82
+ logger.error(err, 'Unable to disconnect');
83
+ process.exit(-1);
84
+ }
95
85
  }
96
86
 
97
87
  async function run(settings, ipc) {