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

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.
@@ -11,9 +11,6 @@ export declare class PartitionService {
11
11
  private logger;
12
12
  private lastRefreshTime;
13
13
  private readonly minRefreshInterval;
14
- private refreshInProgress;
15
- private readonly maxRefreshRetries;
16
- private refreshRetryCount;
17
14
  constructor(client: HazelcastClient);
18
15
  initialize(): Promise<void>;
19
16
  shutdown(): void;
@@ -23,9 +23,6 @@ var PartitionService = /** @class */ (function () {
23
23
  this.partitionMap = {};
24
24
  this.lastRefreshTime = 0;
25
25
  this.minRefreshInterval = 2000; // Minimum 2 seconds between refreshes
26
- this.refreshInProgress = false;
27
- this.maxRefreshRetries = 3;
28
- this.refreshRetryCount = 0;
29
26
  this.client = client;
30
27
  this.logger = client.getLoggingService().getLogger();
31
28
  this.isShutdown = false;
@@ -37,7 +34,6 @@ var PartitionService = /** @class */ (function () {
37
34
  PartitionService.prototype.shutdown = function () {
38
35
  clearInterval(this.partitionRefreshTask);
39
36
  this.isShutdown = true;
40
- this.refreshInProgress = false;
41
37
  };
42
38
  /**
43
39
  * Clears the partition table, forcing a refresh on next operation
@@ -47,7 +43,6 @@ var PartitionService = /** @class */ (function () {
47
43
  this.partitionMap = {};
48
44
  this.partitionCount = 0;
49
45
  this.lastRefreshTime = 0;
50
- this.refreshRetryCount = 0;
51
46
  };
52
47
  /**
53
48
  * Refreshes the internal partition table.
@@ -62,16 +57,11 @@ var PartitionService = /** @class */ (function () {
62
57
  this.logger.debug('PartitionService', 'Skipping refresh, too soon since last refresh');
63
58
  return Promise.resolve();
64
59
  }
65
- if (this.refreshInProgress) {
66
- this.logger.debug('PartitionService', 'Refresh already in progress, skipping');
67
- return Promise.resolve();
68
- }
69
60
  var ownerConnection = this.client.getClusterService().getOwnerConnection();
70
61
  if (ownerConnection == null) {
71
62
  this.logger.warn('PartitionService', 'Cannot refresh partitions, no owner connection available');
72
63
  return Promise.resolve();
73
64
  }
74
- this.refreshInProgress = true;
75
65
  var clientMessage = GetPartitionsCodec.encodeRequest();
76
66
  return this.client.getInvocationService()
77
67
  .invokeOnConnection(ownerConnection, clientMessage)
@@ -80,21 +70,12 @@ var PartitionService = /** @class */ (function () {
80
70
  _this.partitionMap = receivedPartitionMap;
81
71
  _this.partitionCount = Object.keys(_this.partitionMap).length;
82
72
  _this.lastRefreshTime = now;
83
- _this.refreshRetryCount = 0; // Reset retry count on success
84
73
  _this.logger.debug('PartitionService', "Refreshed partition table with " + _this.partitionCount + " partitions");
85
74
  }).catch(function (e) {
86
75
  if (_this.client.getLifecycleService().isRunning()) {
87
76
  _this.logger.warn('PartitionService', 'Error while fetching cluster partition table from '
88
- + _this.client.getClusterService().ownerUuid, e);
89
- // Increment retry count and clear table if too many failures
90
- _this.refreshRetryCount++;
91
- if (_this.refreshRetryCount >= _this.maxRefreshRetries) {
92
- _this.logger.error('PartitionService', "Max refresh retries (" + _this.maxRefreshRetries + ") exceeded, clearing partition table");
93
- _this.clearPartitionTable();
94
- }
77
+ + _this.client.getClusterService().ownerUuid || 'unknown', e);
95
78
  }
96
- }).finally(function () {
97
- _this.refreshInProgress = false;
98
79
  });
99
80
  };
100
81
  /**
@@ -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}.
@@ -51,9 +82,14 @@ export declare class ClientConnectionManager extends EventEmitter {
51
82
  destroyConnection(address: Address): void;
52
83
  /**
53
84
  * Cleans up all connections to a specific address during failover
54
- * @param address
85
+ * @param address The address to cleanup
55
86
  */
56
87
  cleanupConnectionsForFailover(address: Address): void;
88
+ /**
89
+ * Handles authentication errors by clearing failed connections and allowing retry
90
+ * @param address The address that had authentication issues
91
+ */
92
+ handleAuthenticationError(address: Address): void;
57
93
  shutdown(): void;
58
94
  private triggerConnect(address, asOwner);
59
95
  private connectTLSSocket(address, configOpts);
@@ -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;
@@ -122,11 +133,21 @@ var ClientConnectionManager = /** @class */ (function (_super) {
122
133
  _this.failedConnections.delete(address.toString());
123
134
  return connection;
124
135
  }).catch(function (error) {
136
+ // Check if it's an authentication error
137
+ var isAuthError = error.message && (error.message.includes('Invalid Credentials') ||
138
+ error.message.includes('authentication') ||
139
+ error.message.includes('credentials'));
140
+ if (isAuthError) {
141
+ _this.logger.error('ClientConnectionManager', "Authentication failed for " + address.toString() + ", not retrying: " + error.message);
142
+ // Handle authentication errors specially
143
+ _this.handleAuthenticationError(address);
144
+ throw error;
145
+ }
125
146
  if (retryCount < _this.maxConnectionRetries) {
126
147
  _this.logger.warn('ClientConnectionManager', "Connection attempt " + (retryCount + 1) + " failed for " + address.toString() + ", retrying in " + _this.connectionRetryDelay + "ms");
127
- return new Promise(function (resolve) {
148
+ return new Promise(function (resolve, reject) {
128
149
  setTimeout(function () {
129
- _this.retryConnection(address, asOwner, retryCount + 1).then(resolve).catch(resolve);
150
+ _this.retryConnection(address, asOwner, retryCount + 1).then(resolve).catch(reject);
130
151
  }, _this.connectionRetryDelay);
131
152
  });
132
153
  }
@@ -160,6 +181,73 @@ var ClientConnectionManager = /** @class */ (function (_super) {
160
181
  ClientConnectionManager.prototype.getActiveConnections = function () {
161
182
  return this.establishedConnections;
162
183
  };
184
+ /**
185
+ * Checks if we already have a connection to the given address
186
+ * @param address The address to check
187
+ * @returns true if connection exists and is healthy, false otherwise
188
+ */
189
+ ClientConnectionManager.prototype.hasConnection = function (address) {
190
+ var addressStr = address.toString();
191
+ var connection = this.establishedConnections[addressStr];
192
+ return connection && connection.isAlive();
193
+ };
194
+ /**
195
+ * Forces cleanup of all dead connections
196
+ * This is useful during failover to prevent connection leakage
197
+ */
198
+ ClientConnectionManager.prototype.forceCleanupDeadConnections = function () {
199
+ var _this = this;
200
+ this.logger.info('ClientConnectionManager', 'Forcing cleanup of all dead connections');
201
+ var addressesToRemove = [];
202
+ // Check all established connections
203
+ Object.keys(this.establishedConnections).forEach(function (addressStr) {
204
+ var connection = _this.establishedConnections[addressStr];
205
+ if (connection && !connection.isHealthy()) {
206
+ _this.logger.warn('ClientConnectionManager', "Found dead connection to " + addressStr + ", marking for cleanup");
207
+ addressesToRemove.push(addressStr);
208
+ }
209
+ });
210
+ // Remove dead connections
211
+ addressesToRemove.forEach(function (addressStr) {
212
+ var connection = _this.establishedConnections[addressStr];
213
+ if (connection) {
214
+ _this.destroyConnection(connection.getAddress());
215
+ }
216
+ });
217
+ // Also clean up any pending connections that might be stuck
218
+ Object.keys(this.pendingConnections).forEach(function (addressStr) {
219
+ var pendingConnection = _this.pendingConnections[addressStr];
220
+ if (pendingConnection) {
221
+ _this.logger.warn('ClientConnectionManager', "Cleaning up stuck pending connection to " + addressStr);
222
+ pendingConnection.reject(new Error('Connection cleanup forced'));
223
+ delete _this.pendingConnections[addressStr];
224
+ }
225
+ });
226
+ this.logger.info('ClientConnectionManager', "Cleanup completed. Removed " + addressesToRemove.length + " dead connections");
227
+ };
228
+ /**
229
+ * Gets an existing connection to a specific address if it exists
230
+ * @param address The address to check for existing connection
231
+ * @returns The existing connection or undefined if none exists
232
+ */
233
+ ClientConnectionManager.prototype.getConnection = function (address) {
234
+ var addressStr = address.toString();
235
+ return this.establishedConnections[addressStr];
236
+ };
237
+ /**
238
+ * Gets all established connections
239
+ * @returns Object containing all established connections
240
+ */
241
+ ClientConnectionManager.prototype.getEstablishedConnections = function () {
242
+ return this.establishedConnections;
243
+ };
244
+ /**
245
+ * Gets all pending connections
246
+ * @returns Object containing all pending connections
247
+ */
248
+ ClientConnectionManager.prototype.getPendingConnections = function () {
249
+ return this.pendingConnections;
250
+ };
163
251
  /**
164
252
  * Returns the {@link ClientConnection} with given {@link Address}. If there is no such connection established,
165
253
  * it first connects to the address and then return the {@link ClientConnection}.
@@ -195,6 +283,13 @@ var ClientConnectionManager = /** @class */ (function (_super) {
195
283
  this.pendingConnections[addressIndex] = connectionResolver;
196
284
  this.retryConnection(address, asOwner)
197
285
  .then(function (clientConnection) {
286
+ // Safety check: ensure we got a proper ClientConnection
287
+ if (!clientConnection || typeof clientConnection.getAddress !== 'function') {
288
+ var error = new Error("Invalid connection object returned: " + typeof clientConnection);
289
+ _this.logger.error('ClientConnectionManager', error.message);
290
+ connectionResolver.reject(error);
291
+ return;
292
+ }
198
293
  _this.establishedConnections[clientConnection.getAddress().toString()] = clientConnection;
199
294
  _this.onConnectionOpened(clientConnection);
200
295
  connectionResolver.resolve(clientConnection);
@@ -243,7 +338,7 @@ var ClientConnectionManager = /** @class */ (function (_super) {
243
338
  };
244
339
  /**
245
340
  * Cleans up all connections to a specific address during failover
246
- * @param address
341
+ * @param address The address to cleanup
247
342
  */
248
343
  ClientConnectionManager.prototype.cleanupConnectionsForFailover = function (address) {
249
344
  var addressStr = address.toString();
@@ -253,6 +348,27 @@ var ClientConnectionManager = /** @class */ (function (_super) {
253
348
  // Remove from failed connections to allow reconnection after failover
254
349
  this.failedConnections.delete(addressStr);
255
350
  };
351
+ /**
352
+ * Handles authentication errors by clearing failed connections and allowing retry
353
+ * @param address The address that had authentication issues
354
+ */
355
+ ClientConnectionManager.prototype.handleAuthenticationError = function (address) {
356
+ var addressStr = address.toString();
357
+ this.logger.warn('ClientConnectionManager', "Handling authentication error for " + addressStr);
358
+ // Clear from failed connections to allow retry with fresh credentials
359
+ this.failedConnections.delete(addressStr);
360
+ // Also clear any established connections to this address
361
+ if (this.establishedConnections.hasOwnProperty(addressStr)) {
362
+ this.logger.info('ClientConnectionManager', "Clearing established connection to " + addressStr + " due to auth error");
363
+ this.destroyConnection(address);
364
+ }
365
+ // Clear any pending connections
366
+ if (this.pendingConnections.hasOwnProperty(addressStr)) {
367
+ this.logger.info('ClientConnectionManager', "Clearing pending connection to " + addressStr + " due to auth error");
368
+ this.pendingConnections[addressStr].reject(new Error('Authentication error, connection cleared'));
369
+ delete this.pendingConnections[addressStr];
370
+ }
371
+ };
256
372
  ClientConnectionManager.prototype.shutdown = function () {
257
373
  if (this.connectionHealthCheckInterval) {
258
374
  clearInterval(this.connectionHealthCheckInterval);
@@ -47,6 +47,10 @@ export declare class ClusterService {
47
47
  */
48
48
  connectToCluster(): Promise<void>;
49
49
  getPossibleMemberAddresses(): Promise<string[]>;
50
+ /**
51
+ * Returns the owner connection if available
52
+ */
53
+ getOwnerConnection(): ClientConnection | null;
50
54
  /**
51
55
  * Returns the list of members in the cluster.
52
56
  * @returns
@@ -63,11 +67,6 @@ export declare class ClusterService {
63
67
  * @returns {ClientInfo}
64
68
  */
65
69
  getClientInfo(): ClientInfo;
66
- /**
67
- * Returns the connection associated with owner node of this client.
68
- * @returns {ClientConnection}
69
- */
70
- getOwnerConnection(): ClientConnection;
71
70
  /**
72
71
  * Adds MembershipListener to listen for membership updates. There is no check for duplicate registrations,
73
72
  * so if you register the listener twice, it will get events twice.
@@ -87,6 +86,10 @@ export declare class ClusterService {
87
86
  private onConnectionClosed(connection);
88
87
  private onHeartbeatStopped(connection);
89
88
  private triggerFailover();
89
+ /**
90
+ * Attempts emergency recovery when failover fails
91
+ */
92
+ private attemptEmergencyRecovery();
90
93
  private isAddressKnownDown(address);
91
94
  private markAddressAsDown(address);
92
95
  private getDownAddressesInfo();
@@ -98,7 +101,15 @@ export declare class ClusterService {
98
101
  private handleMemberAttributeChange(uuid, key, operationType, value);
99
102
  private memberAdded(member);
100
103
  private memberRemoved(member);
104
+ /**
105
+ * Logs the current state for debugging purposes
106
+ */
107
+ private logCurrentState();
101
108
  private startReconnectionTask();
109
+ /**
110
+ * Starts a periodic task to log the current state for debugging
111
+ */
112
+ private startStateLoggingTask();
102
113
  private attemptReconnectionToFailedNodes();
103
114
  /**
104
115
  * Attempts to establish a connection to a specific address
@@ -50,13 +50,14 @@ var ClusterService = /** @class */ (function () {
50
50
  this.lastFailoverAttempt = 0;
51
51
  this.failoverCooldown = 5000; // 5 seconds cooldown between failover attempts
52
52
  this.downAddresses = new Map(); // address -> timestamp when marked down
53
- this.addressBlockDuration = 30000; // 30 seconds block duration for down addresses
53
+ this.addressBlockDuration = 15000; // Reduced from 30000ms to 15000ms
54
54
  this.reconnectionTask = null;
55
55
  this.reconnectionInterval = 10000; // 10 seconds between reconnection attempts
56
56
  this.client = client;
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.
@@ -102,6 +103,12 @@ var ClusterService = /** @class */ (function () {
102
103
  return Array.from(new Set(Array.from(addresses).concat(Array.from(providerAddresses))));
103
104
  });
104
105
  };
106
+ /**
107
+ * Returns the owner connection if available
108
+ */
109
+ ClusterService.prototype.getOwnerConnection = function () {
110
+ return this.ownerConnection;
111
+ };
105
112
  /**
106
113
  * Returns the list of members in the cluster.
107
114
  * @returns
@@ -143,16 +150,15 @@ var ClusterService = /** @class */ (function () {
143
150
  ClusterService.prototype.getClientInfo = function () {
144
151
  var info = new ClientInfo_1.ClientInfo();
145
152
  info.uuid = this.uuid;
146
- info.localAddress = this.getOwnerConnection().getLocalAddress();
153
+ var ownerConnection = this.getOwnerConnection();
154
+ if (ownerConnection) {
155
+ info.localAddress = ownerConnection.getLocalAddress();
156
+ }
157
+ else {
158
+ info.localAddress = null;
159
+ }
147
160
  return info;
148
161
  };
149
- /**
150
- * Returns the connection associated with owner node of this client.
151
- * @returns {ClientConnection}
152
- */
153
- ClusterService.prototype.getOwnerConnection = function () {
154
- return this.ownerConnection;
155
- };
156
162
  /**
157
163
  * Adds MembershipListener to listen for membership updates. There is no check for duplicate registrations,
158
164
  * so if you register the listener twice, it will get events twice.
@@ -184,7 +190,11 @@ var ClusterService = /** @class */ (function () {
184
190
  var handleAttributeChange = _this.handleMemberAttributeChange.bind(_this);
185
191
  ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.handle(m, handleMember, handleMemberList, handleAttributeChange, null);
186
192
  };
187
- return this.client.getInvocationService().invokeOnConnection(this.getOwnerConnection(), request, handler)
193
+ var ownerConnection = this.getOwnerConnection();
194
+ if (!ownerConnection) {
195
+ return Promise.reject(new Error('Cannot initialize membership listener: no owner connection available'));
196
+ }
197
+ return this.client.getInvocationService().invokeOnConnection(ownerConnection, request, handler)
188
198
  .then(function (resp) {
189
199
  _this.logger.trace('ClusterService', 'Registered listener with id '
190
200
  + ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.decodeResponse(resp).response);
@@ -227,22 +237,64 @@ var ClusterService = /** @class */ (function () {
227
237
  this.failoverInProgress = true;
228
238
  this.lastFailoverAttempt = now;
229
239
  this.logger.info('ClusterService', 'Starting failover process...');
240
+ // Log state before failover
241
+ this.logCurrentState();
242
+ // Force cleanup of all dead connections to prevent leakage
243
+ this.client.getConnectionManager().forceCleanupDeadConnections();
230
244
  // Clear any stale partition information
231
245
  this.client.getPartitionService().clearPartitionTable();
232
246
  // Attempt to reconnect to cluster
233
247
  this.connectToCluster()
234
248
  .then(function () {
235
249
  _this.logger.info('ClusterService', 'Failover completed successfully');
250
+ _this.logCurrentState(); // Log state after successful failover
236
251
  })
237
252
  .catch(function (error) {
238
253
  _this.logger.error('ClusterService', 'Failover failed', error);
239
- // If failover fails, shutdown the client to prevent further issues
240
- _this.client.shutdown();
254
+ // If failover fails, try to unblock at least one address to allow recovery
255
+ _this.attemptEmergencyRecovery();
256
+ _this.logCurrentState(); // Log state after failed failover
257
+ // Don't shutdown immediately, give recovery a chance
241
258
  })
242
259
  .finally(function () {
243
260
  _this.failoverInProgress = false;
244
261
  });
245
262
  };
263
+ /**
264
+ * Attempts emergency recovery when failover fails
265
+ */
266
+ ClusterService.prototype.attemptEmergencyRecovery = function () {
267
+ var _this = this;
268
+ this.logger.warn('ClusterService', 'Attempting emergency recovery...');
269
+ // Unblock at least one address to allow recovery
270
+ if (this.downAddresses.size > 0) {
271
+ var firstBlockedAddress_1 = Array.from(this.downAddresses.keys())[0];
272
+ this.logger.info('ClusterService', "Emergency unblocking address " + firstBlockedAddress_1);
273
+ this.downAddresses.delete(firstBlockedAddress_1);
274
+ // Try to connect to the unblocked address
275
+ try {
276
+ var _a = firstBlockedAddress_1.split(':'), host = _a[0], portStr = _a[1];
277
+ var port = parseInt(portStr, 10);
278
+ if (host && !isNaN(port)) {
279
+ var address_1 = new Address(host, port);
280
+ this.logger.info('ClusterService', "Attempting emergency connection to " + firstBlockedAddress_1);
281
+ // Try to connect without blocking
282
+ this.client.getConnectionManager().getOrConnect(address_1, false)
283
+ .then(function (connection) {
284
+ _this.logger.info('ClusterService', "Emergency connection successful to " + firstBlockedAddress_1);
285
+ _this.evaluateOwnershipChange(address_1, connection);
286
+ _this.client.getPartitionService().refresh();
287
+ })
288
+ .catch(function (error) {
289
+ _this.logger.warn('ClusterService', "Emergency connection failed to " + firstBlockedAddress_1 + ":", error);
290
+ });
291
+ }
292
+ }
293
+ catch (error) {
294
+ this.logger.error('ClusterService', 'Error during emergency recovery:', error);
295
+ }
296
+ }
297
+ };
246
298
  ClusterService.prototype.isAddressKnownDown = function (address) {
247
299
  var addressStr = address.toString();
248
300
  var downTime = this.downAddresses.get(addressStr);
@@ -349,6 +401,8 @@ var ClusterService = /** @class */ (function () {
349
401
  this.members = members;
350
402
  this.client.getPartitionService().refresh();
351
403
  this.logger.info('ClusterService', 'Members received.', this.members);
404
+ // Log current state after member list update
405
+ this.logCurrentState();
352
406
  var events = this.detectMembershipEvents(prevMembers);
353
407
  for (var _i = 0, events_1 = events; _i < events_1.length; _i++) {
354
408
  var event = events_1[_i];
@@ -416,10 +470,55 @@ var ClusterService = /** @class */ (function () {
416
470
  var removedMemberList = this.members.splice(memberIndex, 1);
417
471
  assert(removedMemberList.length === 1);
418
472
  }
419
- this.client.getConnectionManager().destroyConnection(member.address);
473
+ // Check if we have a healthy connection to this member
474
+ var connectionManager = this.client.getConnectionManager();
475
+ var existingConnection = connectionManager.getConnection(member.address);
476
+ if (existingConnection && existingConnection.isHealthy()) {
477
+ // If the connection is healthy, don't destroy it immediately
478
+ // This prevents unnecessary disconnections during temporary network issues
479
+ this.logger.info('ClusterService', "Member removed but connection is healthy: " + member.address.toString() + ", preserving connection");
480
+ // Only destroy if we're not in failover mode
481
+ if (!this.failoverInProgress) {
482
+ this.logger.debug('ClusterService', "Destroying healthy connection to removed member: " + member.address.toString());
483
+ connectionManager.destroyConnection(member.address);
484
+ }
485
+ else {
486
+ this.logger.debug('ClusterService', "Preserving healthy connection during failover: " + member.address.toString());
487
+ }
488
+ }
489
+ else {
490
+ // If connection is unhealthy, destroy it
491
+ this.logger.info('ClusterService', "Member removed with unhealthy connection: " + member.address.toString() + ", destroying connection");
492
+ connectionManager.destroyConnection(member.address);
493
+ }
420
494
  var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.REMOVED, this.members);
421
495
  this.fireMembershipEvent(membershipEvent);
422
496
  };
497
+ /**
498
+ * Logs the current state for debugging purposes
499
+ */
500
+ ClusterService.prototype.logCurrentState = function () {
501
+ var _this = this;
502
+ var activeConnections = Object.keys(this.client.getConnectionManager().getEstablishedConnections()).length;
503
+ var memberCount = this.members.length;
504
+ var downAddressesCount = this.downAddresses.size;
505
+ var hasOwner = !!this.ownerConnection;
506
+ this.logger.info('ClusterService', "Current State - Members: " + memberCount + ", Active Connections: " + activeConnections + ", Down Addresses: " + downAddressesCount + ", Has Owner: " + hasOwner);
507
+ if (this.ownerConnection) {
508
+ this.logger.info('ClusterService', "Owner Connection: " + this.ownerConnection.getAddress().toString() + ", Alive: " + this.ownerConnection.isAlive());
509
+ }
510
+ // Log all active connections
511
+ var connections = this.client.getConnectionManager().getEstablishedConnections();
512
+ Object.keys(connections).forEach(function (addressStr) {
513
+ var connection = connections[addressStr];
514
+ _this.logger.debug('ClusterService', "Connection to " + addressStr + ": Alive=" + connection.isAlive() + ", Owner=" + connection.isAuthenticatedAsOwner());
515
+ });
516
+ // Log down addresses
517
+ if (downAddressesCount > 0) {
518
+ var downAddresses = Array.from(this.downAddresses.keys());
519
+ this.logger.debug('ClusterService', "Down Addresses: " + downAddresses.join(', '));
520
+ }
521
+ };
423
522
  ClusterService.prototype.startReconnectionTask = function () {
424
523
  var _this = this;
425
524
  // Periodically attempt to reconnect to previously failed addresses
@@ -427,15 +526,29 @@ var ClusterService = /** @class */ (function () {
427
526
  _this.attemptReconnectionToFailedNodes();
428
527
  }, this.reconnectionInterval);
429
528
  };
529
+ /**
530
+ * Starts a periodic task to log the current state for debugging
531
+ */
532
+ ClusterService.prototype.startStateLoggingTask = function () {
533
+ var _this = this;
534
+ // Log state every 30 seconds for debugging
535
+ setInterval(function () {
536
+ if (_this.client.getLifecycleService().isRunning()) {
537
+ _this.logCurrentState();
538
+ }
539
+ }, 30000);
540
+ };
430
541
  ClusterService.prototype.attemptReconnectionToFailedNodes = function () {
431
542
  var _this = this;
432
- if (this.failoverInProgress || !this.ownerConnection) {
543
+ // Allow reconnection even during failover, but be more careful
544
+ if (this.failoverInProgress) {
545
+ this.logger.debug('ClusterService', 'Skipping reconnection attempt during failover');
433
546
  return;
434
547
  }
435
548
  var now = Date.now();
436
549
  var addressesToReconnect = [];
437
550
  var totalDownAddresses = this.downAddresses.size;
438
- // If we have no down addresses, we can increase the reconnection interval
551
+ // If we have no down addresses, we can skip
439
552
  if (totalDownAddresses === 0) {
440
553
  return;
441
554
  }
@@ -449,6 +562,12 @@ var ClusterService = /** @class */ (function () {
449
562
  var port = parseInt(portStr, 10);
450
563
  if (host && !isNaN(port)) {
451
564
  var address = new Address(host, port);
565
+ // Check if we already have a connection to this address
566
+ if (_this.client.getConnectionManager().hasConnection(address)) {
567
+ _this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", removing from down addresses");
568
+ _this.downAddresses.delete(addressStr);
569
+ return;
570
+ }
452
571
  addressesToReconnect.push(address);
453
572
  }
454
573
  }
@@ -474,6 +593,8 @@ var ClusterService = /** @class */ (function () {
474
593
  });
475
594
  this.logger.debug('ClusterService', "Still waiting for " + totalDownAddresses + " addresses to unblock: " + remainingBlocked.join(', '));
476
595
  }
596
+ // Log current state after reconnection attempts
597
+ this.logCurrentState();
477
598
  };
478
599
  /**
479
600
  * Attempts to establish a connection to a specific address
@@ -482,6 +603,20 @@ var ClusterService = /** @class */ (function () {
482
603
  ClusterService.prototype.attemptReconnectionToAddress = function (address) {
483
604
  var _this = this;
484
605
  var addressStr = address.toString();
606
+ // Check if we already have a connection to this address
607
+ if (this.client.getConnectionManager().hasConnection(address)) {
608
+ this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", skipping reconnection");
609
+ this.downAddresses.delete(addressStr);
610
+ return;
611
+ }
612
+ // Check if we're already trying to connect to this address
613
+ var connectionManager = this.client.getConnectionManager();
614
+ var establishedConnections = connectionManager.getEstablishedConnections();
615
+ var pendingConnections = Object.keys(connectionManager.getPendingConnections()).length;
616
+ if (pendingConnections > 0) {
617
+ this.logger.debug('ClusterService', "Already have pending connections, skipping reconnection to " + addressStr);
618
+ return;
619
+ }
485
620
  // Remove from down addresses to allow connection attempt
486
621
  this.downAddresses.delete(addressStr);
487
622
  this.logger.debug('ClusterService', "Attempting reconnection to " + addressStr);
@@ -489,8 +624,14 @@ var ClusterService = /** @class */ (function () {
489
624
  this.client.getConnectionManager().getOrConnect(address, false)
490
625
  .then(function (connection) {
491
626
  _this.logger.info('ClusterService', "Successfully reconnected to " + addressStr);
492
- // Check if this reconnected node should become the new owner
493
- _this.evaluateOwnershipChange(address, connection);
627
+ // Only evaluate ownership change if we don't have an owner or current owner is unhealthy
628
+ if (!_this.ownerConnection || !_this.ownerConnection.isHealthy()) {
629
+ _this.logger.info('ClusterService', "Evaluating ownership change for " + addressStr);
630
+ _this.evaluateOwnershipChange(address, connection);
631
+ }
632
+ else {
633
+ _this.logger.debug('ClusterService', "Keeping " + addressStr + " as member connection, current owner is healthy");
634
+ }
494
635
  // Trigger partition service refresh to update routing information
495
636
  _this.client.getPartitionService().refresh();
496
637
  }).catch(function (error) {
@@ -518,12 +659,8 @@ var ClusterService = /** @class */ (function () {
518
659
  this.promoteToOwner(connection, address);
519
660
  return;
520
661
  }
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
- }
662
+ // Don't switch ownership if current owner is healthy
663
+ this.logger.debug('ClusterService', "Current owner is healthy, keeping " + address.toString() + " as member connection");
527
664
  };
528
665
  /**
529
666
  * Promotes a connection to owner status
@@ -559,6 +696,18 @@ var ClusterService = /** @class */ (function () {
559
696
  var _this = this;
560
697
  var addressStr = address.toString();
561
698
  var now = Date.now();
699
+ // Don't block if we already have a healthy connection to this address
700
+ if (this.client.getConnectionManager().hasConnection(address)) {
701
+ this.logger.debug('ClusterService', "Not blocking " + addressStr + " as we have a healthy connection");
702
+ return;
703
+ }
704
+ // Don't block if this would leave us with no available nodes
705
+ var totalDownAddresses = this.downAddresses.size;
706
+ var totalMembers = this.members.length;
707
+ if (totalDownAddresses >= totalMembers - 1) {
708
+ this.logger.warn('ClusterService', "Not blocking " + addressStr + " as it would leave us with no available nodes");
709
+ return;
710
+ }
562
711
  this.downAddresses.set(addressStr, now);
563
712
  this.logger.warn('ClusterService', "Marked address " + addressStr + " as down, will be blocked for " + blockDuration + "ms");
564
713
  // Schedule cleanup of this address after block duration
@@ -289,6 +289,10 @@ var InvocationService = /** @class */ (function () {
289
289
  throw new Error("Still no partition owner for partition " + partitionId + " after refresh");
290
290
  }
291
291
  return _this.invokeOnAddress(invocation, newOwnerAddress);
292
+ }).catch(function (error) {
293
+ _this.logger.error('InvocationService', "Failed to refresh partition table for partition " + partitionId + ":", error);
294
+ // If partition refresh fails, reject the invocation instead of hanging
295
+ throw new Error("Cannot find partition owner for partition " + partitionId + ": " + error.message);
292
296
  });
293
297
  }
294
298
  return this.client.getConnectionManager().getOrConnect(ownerAddress).then(function (connection) {
@@ -301,6 +305,10 @@ var InvocationService = /** @class */ (function () {
301
305
  return _this.client.getPartitionService().refresh().then(function () {
302
306
  // Retry the invocation with updated partition information
303
307
  return _this.doInvoke(invocation);
308
+ }).catch(function (refreshError) {
309
+ _this.logger.error('InvocationService', "Failed to refresh partition table after partition owner failure:", refreshError);
310
+ // If refresh fails, reject the invocation instead of hanging
311
+ throw new Error("Partition owner " + ownerAddress.toString() + " unavailable and partition refresh failed: " + refreshError.message);
304
312
  });
305
313
  }
306
314
  throw new HazelcastError_1.IOError(ownerAddress.toString() + '(partition owner) is not available.', e);
@@ -130,20 +130,32 @@ var ProxyManager = /** @class */ (function () {
130
130
  };
131
131
  ProxyManager.prototype.findNextAddress = function () {
132
132
  var members = this.client.getClusterService().getMembers();
133
+ // If no members available, return null but log the issue
134
+ if (!members || members.length === 0) {
135
+ this.logger.warn('ProxyManager', 'No cluster members available for proxy creation');
136
+ return null;
137
+ }
133
138
  var liteMember = null;
139
+ var dataMember = null;
134
140
  for (var _i = 0, members_1 = members; _i < members_1.length; _i++) {
135
141
  var member = members_1[_i];
136
142
  if (member != null && member.isLiteMember === false) {
137
- return member.address;
143
+ dataMember = member;
144
+ break; // Prefer data members
138
145
  }
139
146
  else if (member != null && member.isLiteMember) {
140
147
  liteMember = member;
141
148
  }
142
149
  }
143
- if (liteMember != null) {
150
+ // Return data member if available, otherwise lite member, otherwise null
151
+ if (dataMember != null) {
152
+ return dataMember.address;
153
+ }
154
+ else if (liteMember != null) {
144
155
  return liteMember.address;
145
156
  }
146
157
  else {
158
+ this.logger.warn('ProxyManager', 'No valid members found for proxy creation');
147
159
  return null;
148
160
  }
149
161
  };
@@ -151,6 +163,12 @@ var ProxyManager = /** @class */ (function () {
151
163
  var _this = this;
152
164
  if (Date.now() <= deadline) {
153
165
  var address = this.findNextAddress();
166
+ if (!address) {
167
+ var error = new Error('No cluster members available for proxy creation: ' + proxyObject.getName());
168
+ this.logger.error('ProxyManager', error.message);
169
+ promise.reject(error);
170
+ return;
171
+ }
154
172
  var request = ClientCreateProxyCodec_1.ClientCreateProxyCodec.encodeRequest(proxyObject.getName(), proxyObject.getServiceName(), address);
155
173
  var invocation = new InvocationService_1.Invocation(this.client, request);
156
174
  invocation.address = address;
@@ -160,10 +178,13 @@ var ProxyManager = /** @class */ (function () {
160
178
  if (_this.isRetryable(error)) {
161
179
  _this.logger.warn('ProxyManager', 'Create proxy request for ' + proxyObject.getName() +
162
180
  ' failed. Retrying in ' + _this.invocationRetryPauseMillis + 'ms. ' + error);
163
- setTimeout(_this.initializeProxy.bind(_this, proxyObject, promise, deadline), _this.invocationRetryPauseMillis);
181
+ setTimeout(function () {
182
+ _this.initializeProxy(proxyObject, promise, deadline);
183
+ }, _this.invocationRetryPauseMillis);
164
184
  }
165
185
  else {
166
- _this.logger.warn('ProxyManager', 'Create proxy request for ' + proxyObject.getName() + ' failed ' + error);
186
+ _this.logger.error('ProxyManager', 'Create proxy request for ' + proxyObject.getName() + ' failed ' + error);
187
+ promise.reject(error);
167
188
  }
168
189
  });
169
190
  }
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-10",
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": {