@celerispay/hazelcast-client 3.12.5-1 → 3.12.5-3

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.
@@ -46,7 +46,12 @@ export declare class ClientConnection {
46
46
  private readonly socket;
47
47
  private readonly writer;
48
48
  private readonly reader;
49
+ private readonly logger;
49
50
  constructor(client: HazelcastClient, address: Address, socket: net.Socket);
51
+ /**
52
+ * Sets up socket event handlers for proper connection state tracking
53
+ */
54
+ private setupSocketEventHandlers();
50
55
  /**
51
56
  * Returns the address of local port that is associated with this connection.
52
57
  * @returns
@@ -65,7 +70,16 @@ export declare class ClientConnection {
65
70
  * Closes this connection.
66
71
  */
67
72
  close(): void;
73
+ /**
74
+ * Checks if the connection is alive and healthy
75
+ * @returns true if connection is alive, false otherwise
76
+ */
68
77
  isAlive(): boolean;
78
+ /**
79
+ * Performs a more thorough health check of the connection
80
+ * @returns true if connection is healthy, false otherwise
81
+ */
82
+ isHealthy(): boolean;
69
83
  isHeartbeating(): boolean;
70
84
  setHeartbeating(heartbeating: boolean): void;
71
85
  isAuthenticatedAsOwner(): boolean;
@@ -217,7 +217,36 @@ var ClientConnection = /** @class */ (function () {
217
217
  _this.lastWriteTimeMillis = Date.now();
218
218
  });
219
219
  this.reader = new FrameReader();
220
+ this.logger = client.getLoggingService().getLogger();
221
+ // Add socket event handlers for proper connection state tracking
222
+ this.setupSocketEventHandlers();
220
223
  }
224
+ /**
225
+ * Sets up socket event handlers for proper connection state tracking
226
+ */
227
+ ClientConnection.prototype.setupSocketEventHandlers = function () {
228
+ var _this = this;
229
+ // Handle socket close events
230
+ this.socket.on('close', function () {
231
+ _this.closedTime = Date.now();
232
+ _this.logger.debug('ClientConnection', "Socket closed for " + _this.address.toString());
233
+ });
234
+ // Handle socket end events
235
+ this.socket.on('end', function () {
236
+ _this.closedTime = Date.now();
237
+ _this.logger.debug('ClientConnection', "Socket ended by remote for " + _this.address.toString());
238
+ });
239
+ // Handle socket errors
240
+ this.socket.on('error', function (error) {
241
+ _this.logger.warn('ClientConnection', "Socket error for " + _this.address.toString() + ":", error);
242
+ // Don't set closedTime here as the socket might still be usable
243
+ });
244
+ // Handle socket timeout
245
+ this.socket.on('timeout', function () {
246
+ _this.logger.warn('ClientConnection', "Socket timeout for " + _this.address.toString());
247
+ // Don't set closedTime here as the socket might still be usable
248
+ });
249
+ };
221
250
  /**
222
251
  * Returns the address of local port that is associated with this connection.
223
252
  * @returns
@@ -254,8 +283,43 @@ var ClientConnection = /** @class */ (function () {
254
283
  this.socket.end();
255
284
  this.closedTime = Date.now();
256
285
  };
286
+ /**
287
+ * Checks if the connection is alive and healthy
288
+ * @returns true if connection is alive, false otherwise
289
+ */
257
290
  ClientConnection.prototype.isAlive = function () {
258
- return this.closedTime === 0;
291
+ // Check if we've explicitly closed the connection
292
+ if (this.closedTime !== 0) {
293
+ return false;
294
+ }
295
+ // Check if the underlying socket is still connected
296
+ if (!this.socket || this.socket.destroyed) {
297
+ return false;
298
+ }
299
+ // Check if the socket is writable (indicates it's still connected)
300
+ if (!this.socket.writable) {
301
+ return false;
302
+ }
303
+ // Check if we've received data recently (within last 30 seconds)
304
+ var now = Date.now();
305
+ var lastReadThreshold = 30000; // 30 seconds
306
+ if (this.lastReadTimeMillis > 0 && (now - this.lastReadTimeMillis) > lastReadThreshold) {
307
+ // If we haven't received data for a while, the connection might be stale
308
+ return false;
309
+ }
310
+ return true;
311
+ };
312
+ /**
313
+ * Performs a more thorough health check of the connection
314
+ * @returns true if connection is healthy, false otherwise
315
+ */
316
+ ClientConnection.prototype.isHealthy = function () {
317
+ if (!this.isAlive()) {
318
+ return false;
319
+ }
320
+ // Additional health checks can be added here
321
+ // For now, just check if we're still heartbeating
322
+ return this.isHeartbeating();
259
323
  };
260
324
  ClientConnection.prototype.isHeartbeating = function () {
261
325
  return this.heartbeating;
@@ -36,6 +36,37 @@ export declare class ClientConnectionManager extends EventEmitter {
36
36
  getActiveConnections(): {
37
37
  [address: string]: ClientConnection;
38
38
  };
39
+ /**
40
+ * Checks if we already have a connection to the given address
41
+ * @param address The address to check
42
+ * @returns true if connection exists and is healthy, false otherwise
43
+ */
44
+ hasConnection(address: Address): boolean;
45
+ /**
46
+ * Forces cleanup of all dead connections
47
+ * This is useful during failover to prevent connection leakage
48
+ */
49
+ forceCleanupDeadConnections(): void;
50
+ /**
51
+ * Gets an existing connection to a specific address if it exists
52
+ * @param address The address to check for existing connection
53
+ * @returns The existing connection or undefined if none exists
54
+ */
55
+ getConnection(address: Address): ClientConnection | undefined;
56
+ /**
57
+ * Gets all established connections
58
+ * @returns Object containing all established connections
59
+ */
60
+ getEstablishedConnections(): {
61
+ [address: string]: ClientConnection;
62
+ };
63
+ /**
64
+ * Gets all pending connections
65
+ * @returns Object containing all pending connections
66
+ */
67
+ getPendingConnections(): {
68
+ [address: string]: Promise.Resolver<ClientConnection>;
69
+ };
39
70
  /**
40
71
  * Returns the {@link ClientConnection} with given {@link Address}. If there is no such connection established,
41
72
  * it first connects to the address and then return the {@link ClientConnection}.
@@ -98,6 +98,17 @@ var ClientConnectionManager = /** @class */ (function (_super) {
98
98
  _this.destroyConnection(connection.getAddress());
99
99
  }
100
100
  });
101
+ // Log current connection state
102
+ var activeConnections = Object.keys(this.establishedConnections).length;
103
+ var pendingConnections = Object.keys(this.pendingConnections).length;
104
+ var failedConnections = this.failedConnections.size;
105
+ this.logger.debug('ClientConnectionManager', "Connection State - Active: " + activeConnections + ", Pending: " + pendingConnections + ", Failed: " + failedConnections);
106
+ if (activeConnections > 0) {
107
+ Object.keys(this.establishedConnections).forEach(function (addressStr) {
108
+ var connection = _this.establishedConnections[addressStr];
109
+ _this.logger.debug('ClientConnectionManager', "Connection to " + addressStr + ": Alive=" + connection.isAlive() + ", Owner=" + connection.isAuthenticatedAsOwner());
110
+ });
111
+ }
101
112
  };
102
113
  ClientConnectionManager.prototype.checkConnectionHealth = function () {
103
114
  // Use Object.keys() instead of Object.values() for compatibility
@@ -112,8 +123,8 @@ var ClientConnectionManager = /** @class */ (function (_super) {
112
123
  }
113
124
  };
114
125
  ClientConnectionManager.prototype.isConnectionHealthy = function (connection) {
115
- // Only check if connection is alive - isAuthenticated() method doesn't exist
116
- return connection.isAlive();
126
+ // Use the improved health check method
127
+ return connection.isHealthy();
117
128
  };
118
129
  ClientConnectionManager.prototype.retryConnection = function (address, asOwner, retryCount) {
119
130
  var _this = this;
@@ -160,6 +171,73 @@ var ClientConnectionManager = /** @class */ (function (_super) {
160
171
  ClientConnectionManager.prototype.getActiveConnections = function () {
161
172
  return this.establishedConnections;
162
173
  };
174
+ /**
175
+ * Checks if we already have a connection to the given address
176
+ * @param address The address to check
177
+ * @returns true if connection exists and is healthy, false otherwise
178
+ */
179
+ ClientConnectionManager.prototype.hasConnection = function (address) {
180
+ var addressStr = address.toString();
181
+ var connection = this.establishedConnections[addressStr];
182
+ return connection && connection.isAlive();
183
+ };
184
+ /**
185
+ * Forces cleanup of all dead connections
186
+ * This is useful during failover to prevent connection leakage
187
+ */
188
+ ClientConnectionManager.prototype.forceCleanupDeadConnections = function () {
189
+ var _this = this;
190
+ this.logger.info('ClientConnectionManager', 'Forcing cleanup of all dead connections');
191
+ var addressesToRemove = [];
192
+ // Check all established connections
193
+ Object.keys(this.establishedConnections).forEach(function (addressStr) {
194
+ var connection = _this.establishedConnections[addressStr];
195
+ if (connection && !connection.isHealthy()) {
196
+ _this.logger.warn('ClientConnectionManager', "Found dead connection to " + addressStr + ", marking for cleanup");
197
+ addressesToRemove.push(addressStr);
198
+ }
199
+ });
200
+ // Remove dead connections
201
+ addressesToRemove.forEach(function (addressStr) {
202
+ var connection = _this.establishedConnections[addressStr];
203
+ if (connection) {
204
+ _this.destroyConnection(connection.getAddress());
205
+ }
206
+ });
207
+ // Also clean up any pending connections that might be stuck
208
+ Object.keys(this.pendingConnections).forEach(function (addressStr) {
209
+ var pendingConnection = _this.pendingConnections[addressStr];
210
+ if (pendingConnection) {
211
+ _this.logger.warn('ClientConnectionManager', "Cleaning up stuck pending connection to " + addressStr);
212
+ pendingConnection.reject(new Error('Connection cleanup forced'));
213
+ delete _this.pendingConnections[addressStr];
214
+ }
215
+ });
216
+ this.logger.info('ClientConnectionManager', "Cleanup completed. Removed " + addressesToRemove.length + " dead connections");
217
+ };
218
+ /**
219
+ * Gets an existing connection to a specific address if it exists
220
+ * @param address The address to check for existing connection
221
+ * @returns The existing connection or undefined if none exists
222
+ */
223
+ ClientConnectionManager.prototype.getConnection = function (address) {
224
+ var addressStr = address.toString();
225
+ return this.establishedConnections[addressStr];
226
+ };
227
+ /**
228
+ * Gets all established connections
229
+ * @returns Object containing all established connections
230
+ */
231
+ ClientConnectionManager.prototype.getEstablishedConnections = function () {
232
+ return this.establishedConnections;
233
+ };
234
+ /**
235
+ * Gets all pending connections
236
+ * @returns Object containing all pending connections
237
+ */
238
+ ClientConnectionManager.prototype.getPendingConnections = function () {
239
+ return this.pendingConnections;
240
+ };
163
241
  /**
164
242
  * Returns the {@link ClientConnection} with given {@link Address}. If there is no such connection established,
165
243
  * it first connects to the address and then return the {@link ClientConnection}.
@@ -98,7 +98,15 @@ export declare class ClusterService {
98
98
  private handleMemberAttributeChange(uuid, key, operationType, value);
99
99
  private memberAdded(member);
100
100
  private memberRemoved(member);
101
+ /**
102
+ * Logs the current state for debugging purposes
103
+ */
104
+ private logCurrentState();
101
105
  private startReconnectionTask();
106
+ /**
107
+ * Starts a periodic task to log the current state for debugging
108
+ */
109
+ private startStateLoggingTask();
102
110
  private attemptReconnectionToFailedNodes();
103
111
  /**
104
112
  * Attempts to establish a connection to a specific address
@@ -57,6 +57,7 @@ var ClusterService = /** @class */ (function () {
57
57
  this.logger = this.client.getLoggingService().getLogger();
58
58
  this.members = [];
59
59
  this.startReconnectionTask();
60
+ this.startStateLoggingTask();
60
61
  }
61
62
  /**
62
63
  * Starts cluster service.
@@ -227,15 +228,21 @@ var ClusterService = /** @class */ (function () {
227
228
  this.failoverInProgress = true;
228
229
  this.lastFailoverAttempt = now;
229
230
  this.logger.info('ClusterService', 'Starting failover process...');
231
+ // Log state before failover
232
+ this.logCurrentState();
233
+ // Force cleanup of all dead connections to prevent leakage
234
+ this.client.getConnectionManager().forceCleanupDeadConnections();
230
235
  // Clear any stale partition information
231
236
  this.client.getPartitionService().clearPartitionTable();
232
237
  // Attempt to reconnect to cluster
233
238
  this.connectToCluster()
234
239
  .then(function () {
235
240
  _this.logger.info('ClusterService', 'Failover completed successfully');
241
+ _this.logCurrentState(); // Log state after successful failover
236
242
  })
237
243
  .catch(function (error) {
238
244
  _this.logger.error('ClusterService', 'Failover failed', error);
245
+ _this.logCurrentState(); // Log state after failed failover
239
246
  // If failover fails, shutdown the client to prevent further issues
240
247
  _this.client.shutdown();
241
248
  })
@@ -349,6 +356,8 @@ var ClusterService = /** @class */ (function () {
349
356
  this.members = members;
350
357
  this.client.getPartitionService().refresh();
351
358
  this.logger.info('ClusterService', 'Members received.', this.members);
359
+ // Log current state after member list update
360
+ this.logCurrentState();
352
361
  var events = this.detectMembershipEvents(prevMembers);
353
362
  for (var _i = 0, events_1 = events; _i < events_1.length; _i++) {
354
363
  var event = events_1[_i];
@@ -416,10 +425,55 @@ var ClusterService = /** @class */ (function () {
416
425
  var removedMemberList = this.members.splice(memberIndex, 1);
417
426
  assert(removedMemberList.length === 1);
418
427
  }
419
- this.client.getConnectionManager().destroyConnection(member.address);
428
+ // Check if we have a healthy connection to this member
429
+ var connectionManager = this.client.getConnectionManager();
430
+ var existingConnection = connectionManager.getConnection(member.address);
431
+ if (existingConnection && existingConnection.isHealthy()) {
432
+ // If the connection is healthy, don't destroy it immediately
433
+ // This prevents unnecessary disconnections during temporary network issues
434
+ this.logger.info('ClusterService', "Member removed but connection is healthy: " + member.address.toString() + ", preserving connection");
435
+ // Only destroy if we're not in failover mode
436
+ if (!this.failoverInProgress) {
437
+ this.logger.debug('ClusterService', "Destroying healthy connection to removed member: " + member.address.toString());
438
+ connectionManager.destroyConnection(member.address);
439
+ }
440
+ else {
441
+ this.logger.debug('ClusterService', "Preserving healthy connection during failover: " + member.address.toString());
442
+ }
443
+ }
444
+ else {
445
+ // If connection is unhealthy, destroy it
446
+ this.logger.info('ClusterService', "Member removed with unhealthy connection: " + member.address.toString() + ", destroying connection");
447
+ connectionManager.destroyConnection(member.address);
448
+ }
420
449
  var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.REMOVED, this.members);
421
450
  this.fireMembershipEvent(membershipEvent);
422
451
  };
452
+ /**
453
+ * Logs the current state for debugging purposes
454
+ */
455
+ ClusterService.prototype.logCurrentState = function () {
456
+ var _this = this;
457
+ var activeConnections = Object.keys(this.client.getConnectionManager().getEstablishedConnections()).length;
458
+ var memberCount = this.members.length;
459
+ var downAddressesCount = this.downAddresses.size;
460
+ var hasOwner = !!this.ownerConnection;
461
+ this.logger.info('ClusterService', "Current State - Members: " + memberCount + ", Active Connections: " + activeConnections + ", Down Addresses: " + downAddressesCount + ", Has Owner: " + hasOwner);
462
+ if (this.ownerConnection) {
463
+ this.logger.info('ClusterService', "Owner Connection: " + this.ownerConnection.getAddress().toString() + ", Alive: " + this.ownerConnection.isAlive());
464
+ }
465
+ // Log all active connections
466
+ var connections = this.client.getConnectionManager().getEstablishedConnections();
467
+ Object.keys(connections).forEach(function (addressStr) {
468
+ var connection = connections[addressStr];
469
+ _this.logger.debug('ClusterService', "Connection to " + addressStr + ": Alive=" + connection.isAlive() + ", Owner=" + connection.isAuthenticatedAsOwner());
470
+ });
471
+ // Log down addresses
472
+ if (downAddressesCount > 0) {
473
+ var downAddresses = Array.from(this.downAddresses.keys());
474
+ this.logger.debug('ClusterService', "Down Addresses: " + downAddresses.join(', '));
475
+ }
476
+ };
423
477
  ClusterService.prototype.startReconnectionTask = function () {
424
478
  var _this = this;
425
479
  // Periodically attempt to reconnect to previously failed addresses
@@ -427,15 +481,29 @@ var ClusterService = /** @class */ (function () {
427
481
  _this.attemptReconnectionToFailedNodes();
428
482
  }, this.reconnectionInterval);
429
483
  };
484
+ /**
485
+ * Starts a periodic task to log the current state for debugging
486
+ */
487
+ ClusterService.prototype.startStateLoggingTask = function () {
488
+ var _this = this;
489
+ // Log state every 30 seconds for debugging
490
+ setInterval(function () {
491
+ if (_this.client.getLifecycleService().isRunning()) {
492
+ _this.logCurrentState();
493
+ }
494
+ }, 30000);
495
+ };
430
496
  ClusterService.prototype.attemptReconnectionToFailedNodes = function () {
431
497
  var _this = this;
432
- if (this.failoverInProgress || !this.ownerConnection) {
498
+ // Allow reconnection even during failover, but be more careful
499
+ if (this.failoverInProgress) {
500
+ this.logger.debug('ClusterService', 'Skipping reconnection attempt during failover');
433
501
  return;
434
502
  }
435
503
  var now = Date.now();
436
504
  var addressesToReconnect = [];
437
505
  var totalDownAddresses = this.downAddresses.size;
438
- // If we have no down addresses, we can increase the reconnection interval
506
+ // If we have no down addresses, we can skip
439
507
  if (totalDownAddresses === 0) {
440
508
  return;
441
509
  }
@@ -449,6 +517,12 @@ var ClusterService = /** @class */ (function () {
449
517
  var port = parseInt(portStr, 10);
450
518
  if (host && !isNaN(port)) {
451
519
  var address = new Address(host, port);
520
+ // Check if we already have a connection to this address
521
+ if (_this.client.getConnectionManager().hasConnection(address)) {
522
+ _this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", removing from down addresses");
523
+ _this.downAddresses.delete(addressStr);
524
+ return;
525
+ }
452
526
  addressesToReconnect.push(address);
453
527
  }
454
528
  }
@@ -474,6 +548,8 @@ var ClusterService = /** @class */ (function () {
474
548
  });
475
549
  this.logger.debug('ClusterService', "Still waiting for " + totalDownAddresses + " addresses to unblock: " + remainingBlocked.join(', '));
476
550
  }
551
+ // Log current state after reconnection attempts
552
+ this.logCurrentState();
477
553
  };
478
554
  /**
479
555
  * Attempts to establish a connection to a specific address
@@ -482,6 +558,20 @@ var ClusterService = /** @class */ (function () {
482
558
  ClusterService.prototype.attemptReconnectionToAddress = function (address) {
483
559
  var _this = this;
484
560
  var addressStr = address.toString();
561
+ // Check if we already have a connection to this address
562
+ if (this.client.getConnectionManager().hasConnection(address)) {
563
+ this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", skipping reconnection");
564
+ this.downAddresses.delete(addressStr);
565
+ return;
566
+ }
567
+ // Check if we're already trying to connect to this address
568
+ var connectionManager = this.client.getConnectionManager();
569
+ var establishedConnections = connectionManager.getEstablishedConnections();
570
+ var pendingConnections = Object.keys(connectionManager.getPendingConnections || {}).length;
571
+ if (pendingConnections > 0) {
572
+ this.logger.debug('ClusterService', "Already have pending connections, skipping reconnection to " + addressStr);
573
+ return;
574
+ }
485
575
  // Remove from down addresses to allow connection attempt
486
576
  this.downAddresses.delete(addressStr);
487
577
  this.logger.debug('ClusterService', "Attempting reconnection to " + addressStr);
@@ -489,8 +579,14 @@ var ClusterService = /** @class */ (function () {
489
579
  this.client.getConnectionManager().getOrConnect(address, false)
490
580
  .then(function (connection) {
491
581
  _this.logger.info('ClusterService', "Successfully reconnected to " + addressStr);
492
- // Check if this reconnected node should become the new owner
493
- _this.evaluateOwnershipChange(address, connection);
582
+ // Only evaluate ownership change if we don't have an owner or current owner is unhealthy
583
+ if (!_this.ownerConnection || !_this.ownerConnection.isHealthy()) {
584
+ _this.logger.info('ClusterService', "Evaluating ownership change for " + addressStr);
585
+ _this.evaluateOwnershipChange(address, connection);
586
+ }
587
+ else {
588
+ _this.logger.debug('ClusterService', "Keeping " + addressStr + " as member connection, current owner is healthy");
589
+ }
494
590
  // Trigger partition service refresh to update routing information
495
591
  _this.client.getPartitionService().refresh();
496
592
  }).catch(function (error) {
@@ -518,12 +614,8 @@ var ClusterService = /** @class */ (function () {
518
614
  this.promoteToOwner(connection, address);
519
615
  return;
520
616
  }
521
- // Check if this reconnected node was previously our owner (by checking if it's in our known addresses)
522
- var wasPreviousOwner = this.knownAddresses.some(function (knownAddr) { return knownAddr.equals(address); });
523
- if (wasPreviousOwner) {
524
- this.logger.debug('ClusterService', "Reconnected node " + address.toString() + " was previously known, monitoring for ownership opportunity");
525
- // Don't switch ownership immediately, but keep the connection for potential future use
526
- }
617
+ // Don't switch ownership if current owner is healthy
618
+ this.logger.debug('ClusterService', "Current owner is healthy, keeping " + address.toString() + " as member connection");
527
619
  };
528
620
  /**
529
621
  * Promotes a connection to owner status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celerispay/hazelcast-client",
3
- "version": "3.12.5-1",
3
+ "version": "3.12.5-3",
4
4
  "description": "Hazelcast - open source In-Memory Data Grid - client for NodeJS with critical connection failover fixes",
5
5
  "main": "./lib/index.js",
6
6
  "scripts": {