@celerispay/hazelcast-client 3.12.5 → 3.12.7-2

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.
@@ -24,6 +24,8 @@ var Util_1 = require("../Util");
24
24
  var MemberAttributeEvent_1 = require("../core/MemberAttributeEvent");
25
25
  var MembershipEvent_1 = require("../core/MembershipEvent");
26
26
  var UuidUtil_1 = require("../util/UuidUtil");
27
+ var Address = require("../Address");
28
+ var HazelcastFailoverManager_1 = require("./HazelcastFailoverManager");
27
29
  var MemberEvent;
28
30
  (function (MemberEvent) {
29
31
  MemberEvent[MemberEvent["ADDED"] = 1] = "ADDED";
@@ -49,10 +51,15 @@ var ClusterService = /** @class */ (function () {
49
51
  this.lastFailoverAttempt = 0;
50
52
  this.failoverCooldown = 5000; // 5 seconds cooldown between failover attempts
51
53
  this.downAddresses = new Map(); // address -> timestamp when marked down
52
- this.addressBlockDuration = 30000; // 30 seconds block duration for down addresses
54
+ this.addressBlockDuration = 15000; // Reduced from 30000ms to 15000ms
55
+ this.reconnectionTask = null;
56
+ this.reconnectionInterval = 10000; // 10 seconds between reconnection attempts
53
57
  this.client = client;
54
58
  this.logger = this.client.getLoggingService().getLogger();
55
59
  this.members = [];
60
+ this.startReconnectionTask();
61
+ this.startStateLoggingTask();
62
+ this.failoverManager = new HazelcastFailoverManager_1.HazelcastFailoverManager(client, this.logger);
56
63
  }
57
64
  /**
58
65
  * Starts cluster service.
@@ -98,6 +105,24 @@ var ClusterService = /** @class */ (function () {
98
105
  return Array.from(new Set(Array.from(addresses).concat(Array.from(providerAddresses))));
99
106
  });
100
107
  };
108
+ /**
109
+ * Returns the owner connection if available
110
+ */
111
+ ClusterService.prototype.getOwnerConnection = function () {
112
+ return this.ownerConnection;
113
+ };
114
+ /**
115
+ * Returns whether failover is currently in progress
116
+ */
117
+ ClusterService.prototype.isFailoverInProgress = function () {
118
+ return this.failoverInProgress;
119
+ };
120
+ /**
121
+ * Returns the list of known addresses in the cluster
122
+ */
123
+ ClusterService.prototype.getKnownAddresses = function () {
124
+ return this.knownAddresses.slice(); // Return a copy to prevent external modification
125
+ };
101
126
  /**
102
127
  * Returns the list of members in the cluster.
103
128
  * @returns
@@ -139,16 +164,15 @@ var ClusterService = /** @class */ (function () {
139
164
  ClusterService.prototype.getClientInfo = function () {
140
165
  var info = new ClientInfo_1.ClientInfo();
141
166
  info.uuid = this.uuid;
142
- info.localAddress = this.getOwnerConnection().getLocalAddress();
167
+ var ownerConnection = this.getOwnerConnection();
168
+ if (ownerConnection) {
169
+ info.localAddress = ownerConnection.getLocalAddress();
170
+ }
171
+ else {
172
+ info.localAddress = null;
173
+ }
143
174
  return info;
144
175
  };
145
- /**
146
- * Returns the connection associated with owner node of this client.
147
- * @returns {ClientConnection}
148
- */
149
- ClusterService.prototype.getOwnerConnection = function () {
150
- return this.ownerConnection;
151
- };
152
176
  /**
153
177
  * Adds MembershipListener to listen for membership updates. There is no check for duplicate registrations,
154
178
  * so if you register the listener twice, it will get events twice.
@@ -180,7 +204,11 @@ var ClusterService = /** @class */ (function () {
180
204
  var handleAttributeChange = _this.handleMemberAttributeChange.bind(_this);
181
205
  ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.handle(m, handleMember, handleMemberList, handleAttributeChange, null);
182
206
  };
183
- return this.client.getInvocationService().invokeOnConnection(this.getOwnerConnection(), request, handler)
207
+ var ownerConnection = this.getOwnerConnection();
208
+ if (!ownerConnection) {
209
+ return Promise.reject(new Error('Cannot initialize membership listener: no owner connection available'));
210
+ }
211
+ return this.client.getInvocationService().invokeOnConnection(ownerConnection, request, handler)
184
212
  .then(function (resp) {
185
213
  _this.logger.trace('ClusterService', 'Registered listener with id '
186
214
  + ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.decodeResponse(resp).response);
@@ -196,6 +224,8 @@ var ClusterService = /** @class */ (function () {
196
224
  };
197
225
  ClusterService.prototype.onConnectionClosed = function (connection) {
198
226
  this.logger.warn('ClusterService', 'Connection closed to ' + connection.toString());
227
+ // Mark the address as down when connection is closed
228
+ this.markAddressAsDown(connection.getAddress());
199
229
  if (connection.isAuthenticatedAsOwner()) {
200
230
  this.ownerConnection = null;
201
231
  this.triggerFailover();
@@ -203,6 +233,8 @@ var ClusterService = /** @class */ (function () {
203
233
  };
204
234
  ClusterService.prototype.onHeartbeatStopped = function (connection) {
205
235
  this.logger.warn('ClusterService', connection.toString() + ' stopped heartbeating.');
236
+ // Mark the address as down when heartbeat stops
237
+ this.markAddressAsDown(connection.getAddress());
206
238
  if (connection.isAuthenticatedAsOwner()) {
207
239
  this.client.getConnectionManager().destroyConnection(connection.getAddress());
208
240
  this.ownerConnection = null;
@@ -210,7 +242,6 @@ var ClusterService = /** @class */ (function () {
210
242
  }
211
243
  };
212
244
  ClusterService.prototype.triggerFailover = function () {
213
- var _this = this;
214
245
  var now = Date.now();
215
246
  if (this.failoverInProgress || (now - this.lastFailoverAttempt) < this.failoverCooldown) {
216
247
  this.logger.debug('ClusterService', 'Failover already in progress or too soon since last attempt');
@@ -218,23 +249,116 @@ var ClusterService = /** @class */ (function () {
218
249
  }
219
250
  this.failoverInProgress = true;
220
251
  this.lastFailoverAttempt = now;
221
- this.logger.info('ClusterService', 'Starting failover process...');
252
+ // Check if this is a single-node scenario
253
+ var isSingleNode = this.knownAddresses.length === 1;
254
+ if (isSingleNode) {
255
+ this.logger.info('ClusterService', '🔄 SINGLE-NODE CLUSTER RESET: Node restart detected, starting fresh...');
256
+ this.handleSingleNodeClusterReset();
257
+ }
258
+ else {
259
+ this.logger.info('ClusterService', '🚀 Starting failover process - SERVER-FIRST APPROACH...');
260
+ // SERVER-FIRST: No credential preservation needed
261
+ // We trust the server will provide correct member information
262
+ this.logger.info('ClusterService', '🎯 SERVER-FIRST: No credential management - trusting server data');
263
+ this.handleMultiNodeFailover();
264
+ }
265
+ };
266
+ /**
267
+ * Handles single-node cluster reset - treats node restart as fresh cluster
268
+ */
269
+ ClusterService.prototype.handleSingleNodeClusterReset = function () {
270
+ var _this = this;
271
+ this.logger.info('ClusterService', '🧹 SINGLE-NODE RESET: Clearing all credentials and state...');
272
+ // Clear all stored credentials - node restart means fresh cluster
273
+ this.client.getConnectionManager().clearAllCredentials();
274
+ // Reset client UUIDs - will be assigned fresh by server
275
+ this.uuid = null;
276
+ this.ownerUuid = null;
277
+ // Log state before reset
278
+ this.logCurrentState();
279
+ // Force cleanup of all dead connections
280
+ this.client.getConnectionManager().forceCleanupDeadConnections();
281
+ // Clear partition information
282
+ this.client.getPartitionService().clearPartitionTable();
283
+ // Direct reconnection without waiting for member events
284
+ this.logger.info('ClusterService', '🔄 SINGLE-NODE RESET: Attempting direct reconnection...');
285
+ this.connectToCluster()
286
+ .then(function () {
287
+ _this.logger.info('ClusterService', '✅ Single-node cluster reset completed successfully');
288
+ _this.logCurrentState();
289
+ })
290
+ .catch(function (error) {
291
+ _this.logger.error('ClusterService', 'Single-node cluster reset failed', error);
292
+ _this.logCurrentState();
293
+ })
294
+ .finally(function () {
295
+ _this.failoverInProgress = false;
296
+ });
297
+ };
298
+ /**
299
+ * Handles multi-node failover - preserves existing logic
300
+ */
301
+ ClusterService.prototype.handleMultiNodeFailover = function () {
302
+ var _this = this;
303
+ // Log state before failover
304
+ this.logCurrentState();
305
+ // Force cleanup of all dead connections to prevent leakage
306
+ this.client.getConnectionManager().forceCleanupDeadConnections();
222
307
  // Clear any stale partition information
223
308
  this.client.getPartitionService().clearPartitionTable();
224
309
  // Attempt to reconnect to cluster
225
310
  this.connectToCluster()
226
311
  .then(function () {
227
- _this.logger.info('ClusterService', 'Failover completed successfully');
312
+ _this.logger.info('ClusterService', 'Failover completed successfully - SERVER-FIRST approach');
313
+ // No credential management needed - server handles everything
314
+ _this.logCurrentState(); // Log state after successful failover
228
315
  })
229
316
  .catch(function (error) {
230
317
  _this.logger.error('ClusterService', 'Failover failed', error);
231
- // If failover fails, shutdown the client to prevent further issues
232
- _this.client.shutdown();
318
+ // If failover fails, try to unblock at least one address to allow recovery
319
+ _this.attemptEmergencyRecovery();
320
+ _this.logCurrentState(); // Log state after failed failover
321
+ // Don't shutdown immediately, give recovery a chance
233
322
  })
234
323
  .finally(function () {
235
324
  _this.failoverInProgress = false;
236
325
  });
237
326
  };
327
+ /**
328
+ * Attempts emergency recovery when failover fails
329
+ */
330
+ ClusterService.prototype.attemptEmergencyRecovery = function () {
331
+ var _this = this;
332
+ this.logger.warn('ClusterService', 'Attempting emergency recovery...');
333
+ // Unblock at least one address to allow recovery
334
+ if (this.downAddresses.size > 0) {
335
+ var firstBlockedAddress_1 = Array.from(this.downAddresses.keys())[0];
336
+ this.logger.info('ClusterService', "Emergency unblocking address " + firstBlockedAddress_1);
337
+ this.downAddresses.delete(firstBlockedAddress_1);
338
+ // Try to connect to the unblocked address
339
+ try {
340
+ var _a = firstBlockedAddress_1.split(':'), host = _a[0], portStr = _a[1];
341
+ var port = parseInt(portStr, 10);
342
+ if (host && !isNaN(port)) {
343
+ var address_1 = new Address(host, port);
344
+ this.logger.info('ClusterService', "Attempting emergency connection to " + firstBlockedAddress_1);
345
+ // Try to connect without blocking
346
+ this.client.getConnectionManager().getOrConnect(address_1, false)
347
+ .then(function (connection) {
348
+ _this.logger.info('ClusterService', "Emergency connection successful to " + firstBlockedAddress_1);
349
+ _this.evaluateOwnershipChange(address_1, connection);
350
+ _this.client.getPartitionService().refresh();
351
+ })
352
+ .catch(function (error) {
353
+ _this.logger.warn('ClusterService', "Emergency connection failed to " + firstBlockedAddress_1 + ":", error);
354
+ });
355
+ }
356
+ }
357
+ catch (error) {
358
+ this.logger.error('ClusterService', 'Error during emergency recovery:', error);
359
+ }
360
+ }
361
+ };
238
362
  ClusterService.prototype.isAddressKnownDown = function (address) {
239
363
  var addressStr = address.toString();
240
364
  var downTime = this.downAddresses.get(addressStr);
@@ -285,7 +409,7 @@ var ClusterService = /** @class */ (function () {
285
409
  + ', attempt period: ' + attemptPeriod + ', down addresses: ' + this.getDownAddressesInfo());
286
410
  if (this.knownAddresses.length <= index) {
287
411
  remainingAttemptLimit = remainingAttemptLimit - 1;
288
- if (remainingAttemptLimit === 0) {
412
+ if (remainingAttemptLimit <= 0) {
289
413
  var errorMessage = 'Unable to connect to any of the following addresses: ' +
290
414
  this.knownAddresses.map(function (element) {
291
415
  return element.toString();
@@ -341,6 +465,8 @@ var ClusterService = /** @class */ (function () {
341
465
  this.members = members;
342
466
  this.client.getPartitionService().refresh();
343
467
  this.logger.info('ClusterService', 'Members received.', this.members);
468
+ // Log current state after member list update
469
+ this.logCurrentState();
344
470
  var events = this.detectMembershipEvents(prevMembers);
345
471
  for (var _i = 0, events_1 = events; _i < events_1.length; _i++) {
346
472
  var event = events_1[_i];
@@ -399,6 +525,8 @@ var ClusterService = /** @class */ (function () {
399
525
  };
400
526
  ClusterService.prototype.memberAdded = function (member) {
401
527
  this.members.push(member);
528
+ // Handle member added and update preserved credentials
529
+ this.handleMemberAdded(member);
402
530
  var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.ADDED, this.members);
403
531
  this.fireMembershipEvent(membershipEvent);
404
532
  };
@@ -408,10 +536,355 @@ var ClusterService = /** @class */ (function () {
408
536
  var removedMemberList = this.members.splice(memberIndex, 1);
409
537
  assert(removedMemberList.length === 1);
410
538
  }
411
- this.client.getConnectionManager().destroyConnection(member.address);
539
+ // Check if we have a healthy connection to this member
540
+ var connectionManager = this.client.getConnectionManager();
541
+ var existingConnection = connectionManager.getConnection(member.address);
542
+ if (existingConnection && existingConnection.isHealthy()) {
543
+ // If the connection is healthy, don't destroy it immediately
544
+ // This prevents unnecessary disconnections during temporary network issues
545
+ this.logger.info('ClusterService', "Member removed but connection is healthy: " + member.address.toString() + ", preserving connection");
546
+ // Only destroy if we're not in failover mode
547
+ if (!this.failoverInProgress) {
548
+ this.logger.debug('ClusterService', "Destroying healthy connection to removed member: " + member.address.toString());
549
+ connectionManager.destroyConnection(member.address);
550
+ }
551
+ else {
552
+ this.logger.debug('ClusterService', "Preserving healthy connection during failover: " + member.address.toString());
553
+ }
554
+ }
555
+ else {
556
+ // If connection is unhealthy, destroy it
557
+ this.logger.info('ClusterService', "Member removed with unhealthy connection: " + member.address.toString() + ", destroying connection");
558
+ connectionManager.destroyConnection(member.address);
559
+ }
412
560
  var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.REMOVED, this.members);
413
561
  this.fireMembershipEvent(membershipEvent);
414
562
  };
563
+ /**
564
+ * Performs comprehensive credential cleanup when cluster membership changes
565
+ * This ensures ALL credentials are consistent with the current cluster owner UUID
566
+ * @param connectionManager The connection manager instance
567
+ * @param currentClusterOwnerUuid The current cluster owner UUID
568
+ */
569
+ /**
570
+ * Handles member added event - SERVER-FIRST APPROACH
571
+ * We trust what the server tells us and store it as credentials
572
+ * @param member The member that was added
573
+ */
574
+ ClusterService.prototype.handleMemberAdded = function (member) {
575
+ this.logger.info('ClusterService', "\u2705 SERVER CONFIRMED: Member[ uuid: " + member.uuid + ", address: " + member.address.toString() + "] added to cluster");
576
+ // SERVER-FIRST: Store server data as credentials
577
+ // The server is the authority - we store what it tells us
578
+ this.logger.info('ClusterService', "\uD83C\uDFAF SERVER-FIRST: Storing server member data as credentials - server is authority");
579
+ var connectionManager = this.client.getConnectionManager();
580
+ // Store the server-provided UUID as the authoritative credential
581
+ if (connectionManager && typeof connectionManager.updatePreservedCredentials === 'function') {
582
+ connectionManager.updatePreservedCredentials(member.address, member.uuid);
583
+ }
584
+ // Record that we received a member added event for this address
585
+ if (connectionManager && typeof connectionManager.recordMemberAddedEvent === 'function') {
586
+ connectionManager.recordMemberAddedEvent(member.address);
587
+ }
588
+ // Find the current owner from the cluster state
589
+ var currentOwner = this.findCurrentOwner();
590
+ if (currentOwner) {
591
+ this.logger.info('ClusterService', "\uD83D\uDD04 SERVER-FIRST: Updating ALL credentials with current owner UUID: " + currentOwner.uuid);
592
+ // Update all credentials with the current owner UUID from server
593
+ if (connectionManager && typeof connectionManager.updateAllCredentialsWithNewOwnerUuid === 'function') {
594
+ connectionManager.updateAllCredentialsWithNewOwnerUuid(currentOwner.uuid);
595
+ }
596
+ // CRITICAL FIX: Update client's own UUIDs to match server expectations
597
+ this.logger.info('ClusterService', "\uD83D\uDD04 SERVER-FIRST: Updating client UUIDs to match server state");
598
+ this.logger.info('ClusterService', " - Old Client UUID: " + (this.uuid || 'NOT SET'));
599
+ this.logger.info('ClusterService', " - Old Owner UUID: " + (this.ownerUuid || 'NOT SET'));
600
+ // Update client's own UUIDs with server-provided data
601
+ // The client UUID should match the owner's UUID for authentication
602
+ this.uuid = currentOwner.uuid;
603
+ this.ownerUuid = currentOwner.uuid;
604
+ this.logger.info('ClusterService', " - New Client UUID: " + this.uuid);
605
+ this.logger.info('ClusterService', " - New Owner UUID: " + this.ownerUuid);
606
+ }
607
+ // Refresh partition table (KEEPING REFRESH METHOD UNTOUCHED as requested)
608
+ this.client.getPartitionService().refresh();
609
+ this.logger.info('ClusterService', "\u2705 SERVER-FIRST: Member " + member.uuid + " at " + member.address.toString() + " credentials stored from server data");
610
+ };
611
+ /**
612
+ * Finds the current owner from the cluster state
613
+ * @returns The current owner member or null if not found
614
+ */
615
+ ClusterService.prototype.findCurrentOwner = function () {
616
+ // Check if we have an active owner connection
617
+ var ownerConnection = this.ownerConnection;
618
+ if (ownerConnection && ownerConnection.isAlive()) {
619
+ var ownerAddress = ownerConnection.getAddress();
620
+ // Find the member with this address
621
+ for (var _i = 0, _a = this.members; _i < _a.length; _i++) {
622
+ var member = _a[_i];
623
+ if (member.address.toString() === ownerAddress.toString()) {
624
+ this.logger.debug('ClusterService', "Found current owner: " + member.uuid + " at " + member.address.toString());
625
+ return member;
626
+ }
627
+ }
628
+ }
629
+ // Fallback: look for any member that might be the owner
630
+ this.logger.debug('ClusterService', "No active owner connection found, checking member list for potential owner");
631
+ return null;
632
+ };
633
+ /**
634
+ * Logs the current state for debugging purposes
635
+ */
636
+ ClusterService.prototype.logCurrentState = function () {
637
+ var _this = this;
638
+ var activeConnections = Object.keys(this.client.getConnectionManager().getEstablishedConnections()).length;
639
+ var memberCount = this.members.length;
640
+ var downAddressesCount = this.downAddresses.size;
641
+ var hasOwner = !!this.ownerConnection;
642
+ this.logger.info('ClusterService', "Current State - Members: " + memberCount + ", Active Connections: " + activeConnections + ", Down Addresses: " + downAddressesCount + ", Has Owner: " + hasOwner);
643
+ if (this.ownerConnection) {
644
+ this.logger.info('ClusterService', "Owner Connection: " + this.ownerConnection.getAddress().toString() + ", Alive: " + this.ownerConnection.isAlive());
645
+ }
646
+ // Log all active connections
647
+ var connections = this.client.getConnectionManager().getEstablishedConnections();
648
+ Object.keys(connections).forEach(function (addressStr) {
649
+ var connection = connections[addressStr];
650
+ _this.logger.debug('ClusterService', "Connection to " + addressStr + ": Alive=" + connection.isAlive() + ", Owner=" + connection.isAuthenticatedAsOwner());
651
+ });
652
+ // Log down addresses
653
+ if (downAddressesCount > 0) {
654
+ var downAddresses = Array.from(this.downAddresses.keys());
655
+ this.logger.debug('ClusterService', "Down Addresses: " + downAddresses.join(', '));
656
+ }
657
+ };
658
+ ClusterService.prototype.startReconnectionTask = function () {
659
+ var _this = this;
660
+ // Periodically attempt to reconnect to previously failed addresses
661
+ this.reconnectionTask = setInterval(function () {
662
+ _this.attemptReconnectionToFailedNodes();
663
+ }, this.reconnectionInterval);
664
+ };
665
+ /**
666
+ * Starts a periodic task to log the current state for debugging
667
+ */
668
+ ClusterService.prototype.startStateLoggingTask = function () {
669
+ var _this = this;
670
+ // Log state every 30 seconds for debugging
671
+ setInterval(function () {
672
+ if (_this.client.getLifecycleService().isRunning()) {
673
+ _this.logCurrentState();
674
+ }
675
+ }, 30000);
676
+ };
677
+ ClusterService.prototype.attemptReconnectionToFailedNodes = function () {
678
+ var _this = this;
679
+ // Allow reconnection even during failover, but be more careful
680
+ if (this.failoverInProgress) {
681
+ this.logger.debug('ClusterService', 'Skipping reconnection attempt during failover');
682
+ return;
683
+ }
684
+ var now = Date.now();
685
+ var addressesToReconnect = [];
686
+ var totalDownAddresses = this.downAddresses.size;
687
+ // If we have no down addresses, we can skip
688
+ if (totalDownAddresses === 0) {
689
+ return;
690
+ }
691
+ // Find addresses that are no longer blocked
692
+ this.downAddresses.forEach(function (downTime, addressStr) {
693
+ var timeSinceDown = now - downTime;
694
+ if (timeSinceDown > _this.addressBlockDuration) {
695
+ // Parse the address string back to Address object
696
+ try {
697
+ var _a = addressStr.split(':'), host = _a[0], portStr = _a[1];
698
+ var port = parseInt(portStr, 10);
699
+ if (host && !isNaN(port)) {
700
+ var address = new Address(host, port);
701
+ // Check if we already have a connection to this address
702
+ if (_this.client.getConnectionManager().hasConnection(address)) {
703
+ _this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", removing from down addresses");
704
+ _this.downAddresses.delete(addressStr);
705
+ return;
706
+ }
707
+ addressesToReconnect.push(address);
708
+ }
709
+ }
710
+ catch (error) {
711
+ _this.logger.warn('ClusterService', "Failed to parse address " + addressStr + " for reconnection:", error);
712
+ }
713
+ }
714
+ });
715
+ if (addressesToReconnect.length > 0) {
716
+ this.logger.info('ClusterService', "Attempting to reconnect to " + addressesToReconnect.length + " previously failed nodes: " + addressesToReconnect.map(function (addr) { return addr.toString(); }).join(', '));
717
+ // Attempt to establish connections to each unblocked address
718
+ addressesToReconnect.forEach(function (address) {
719
+ _this.attemptReconnectionToAddress(address);
720
+ });
721
+ }
722
+ else if (totalDownAddresses > 0) {
723
+ // Log remaining blocked addresses for debugging
724
+ var remainingBlocked = Array.from(this.downAddresses.keys()).map(function (addr) {
725
+ var downTime = _this.downAddresses.get(addr);
726
+ var timeSinceDown = now - downTime;
727
+ var remainingTime = Math.max(0, _this.addressBlockDuration - timeSinceDown);
728
+ return addr + " (" + Math.ceil(remainingTime / 1000) + "s remaining)";
729
+ });
730
+ this.logger.debug('ClusterService', "Still waiting for " + totalDownAddresses + " addresses to unblock: " + remainingBlocked.join(', '));
731
+ }
732
+ // Log current state after reconnection attempts
733
+ this.logCurrentState();
734
+ };
735
+ /**
736
+ * Attempts to establish a connection to a specific address
737
+ * @param address The address to reconnect to
738
+ */
739
+ ClusterService.prototype.attemptReconnectionToAddress = function (address) {
740
+ var _this = this;
741
+ var addressStr = address.toString();
742
+ // Check if we already have a connection to this address
743
+ if (this.client.getConnectionManager().hasConnection(address)) {
744
+ this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", skipping reconnection");
745
+ this.downAddresses.delete(addressStr);
746
+ return;
747
+ }
748
+ // Check if we're already trying to connect to this address
749
+ var connectionManager = this.client.getConnectionManager();
750
+ var establishedConnections = connectionManager.getEstablishedConnections();
751
+ var pendingConnections = Object.keys(connectionManager.getPendingConnections()).length;
752
+ if (pendingConnections > 0) {
753
+ this.logger.debug('ClusterService', "Already have pending connections, skipping reconnection to " + addressStr);
754
+ return;
755
+ }
756
+ // Remove from down addresses to allow connection attempt
757
+ this.downAddresses.delete(addressStr);
758
+ this.logger.debug('ClusterService', "Attempting reconnection to " + addressStr);
759
+ // Attempt to establish connection (not as owner, just as regular member connection)
760
+ this.client.getConnectionManager().getOrConnect(address, false)
761
+ .then(function (connection) {
762
+ _this.logger.info('ClusterService', "Successfully reconnected to " + addressStr);
763
+ // Only evaluate ownership change if we don't have an owner or current owner is unhealthy
764
+ if (!_this.ownerConnection || !_this.ownerConnection.isHealthy()) {
765
+ _this.logger.info('ClusterService', "Evaluating ownership change for " + addressStr);
766
+ _this.evaluateOwnershipChange(address, connection);
767
+ }
768
+ else {
769
+ _this.logger.debug('ClusterService', "Keeping " + addressStr + " as member connection, current owner is healthy");
770
+ }
771
+ // Trigger partition service refresh to update routing information
772
+ _this.client.getPartitionService().refresh();
773
+ }).catch(function (error) {
774
+ _this.logger.warn('ClusterService', "Reconnection attempt to " + addressStr + " failed:", error);
775
+ // Mark the address as down again, but with a shorter block duration for reconnection attempts
776
+ var shorterBlockDuration = Math.min(_this.addressBlockDuration / 2, 15000); // Max 15 seconds
777
+ _this.markAddressAsDownWithDuration(address, shorterBlockDuration);
778
+ });
779
+ };
780
+ /**
781
+ * Evaluates whether we should switch ownership to a reconnected node
782
+ * @param address The address of the reconnected node
783
+ * @param connection The connection to the reconnected node
784
+ */
785
+ ClusterService.prototype.evaluateOwnershipChange = function (address, connection) {
786
+ // If we don't have an owner connection, this reconnected node becomes the owner
787
+ if (!this.ownerConnection) {
788
+ this.logger.info('ClusterService', "Promoting reconnected node " + address.toString() + " to owner status");
789
+ this.promoteToOwner(connection, address);
790
+ return;
791
+ }
792
+ // If our current owner connection is having issues, consider switching
793
+ if (this.ownerConnection && !this.ownerConnection.isAlive()) {
794
+ this.logger.info('ClusterService', "Current owner is unhealthy, switching to reconnected node " + address.toString());
795
+ this.promoteToOwner(connection, address);
796
+ return;
797
+ }
798
+ // Don't switch ownership if current owner is healthy
799
+ this.logger.debug('ClusterService', "Current owner is healthy, keeping " + address.toString() + " as member connection");
800
+ };
801
+ /**
802
+ * Promotes a connection to owner status
803
+ * @param connection The connection to promote
804
+ * @param address The address of the promoted connection
805
+ */
806
+ ClusterService.prototype.promoteToOwner = function (connection, address) {
807
+ try {
808
+ // Close the old owner connection if it exists
809
+ if (this.ownerConnection && this.ownerConnection !== connection) {
810
+ this.logger.info('ClusterService', "Closing previous owner connection to " + this.ownerConnection.getAddress().toString());
811
+ this.client.getConnectionManager().destroyConnection(this.ownerConnection.getAddress());
812
+ }
813
+ // Set the new owner connection
814
+ connection.setAuthenticatedAsOwner(true);
815
+ this.ownerConnection = connection;
816
+ this.logger.info('ClusterService', "Successfully promoted " + address.toString() + " to owner status");
817
+ // Refresh partition information with the new owner
818
+ this.client.getPartitionService().refresh();
819
+ }
820
+ catch (error) {
821
+ this.logger.error('ClusterService', "Failed to promote " + address.toString() + " to owner:", error);
822
+ // If promotion fails, mark the address as down again
823
+ this.markAddressAsDown(address);
824
+ }
825
+ };
826
+ /**
827
+ * Marks an address as down with a custom block duration
828
+ * @param address The address to mark as down
829
+ * @param blockDuration The duration to block the address (in milliseconds)
830
+ */
831
+ ClusterService.prototype.markAddressAsDownWithDuration = function (address, blockDuration) {
832
+ var _this = this;
833
+ var addressStr = address.toString();
834
+ var now = Date.now();
835
+ // Don't block if we already have a healthy connection to this address
836
+ if (this.client.getConnectionManager().hasConnection(address)) {
837
+ this.logger.debug('ClusterService', "Not blocking " + addressStr + " as we have a healthy connection");
838
+ return;
839
+ }
840
+ // Don't block if this would leave us with no available nodes
841
+ var totalDownAddresses = this.downAddresses.size;
842
+ var totalMembers = this.members.length;
843
+ if (totalDownAddresses >= totalMembers - 1) {
844
+ this.logger.warn('ClusterService', "Not blocking " + addressStr + " as it would leave us with no available nodes");
845
+ return;
846
+ }
847
+ this.downAddresses.set(addressStr, now);
848
+ this.logger.warn('ClusterService', "Marked address " + addressStr + " as down, will be blocked for " + blockDuration + "ms");
849
+ // Schedule cleanup of this address after block duration
850
+ setTimeout(function () {
851
+ if (_this.downAddresses.has(addressStr)) {
852
+ _this.logger.info('ClusterService', "Unblocking address " + addressStr + " after block duration");
853
+ _this.downAddresses.delete(addressStr);
854
+ }
855
+ }, blockDuration);
856
+ };
857
+ /**
858
+ * Handles ownership change when failover occurs
859
+ * @param newOwnerAddress The address of the new owner
860
+ * @param newOwnerConnection The connection to the new owner
861
+ */
862
+ ClusterService.prototype.handleOwnershipChange = function (newOwnerAddress, newOwnerConnection) {
863
+ this.logger.info('ClusterService', "Handling ownership change to " + newOwnerAddress.toString());
864
+ // Update owner connection
865
+ this.ownerConnection = newOwnerConnection;
866
+ // Note: Owner UUID will be updated when authentication completes
867
+ // For now, we'll keep the existing owner UUID
868
+ // Clear any stale partition information
869
+ this.client.getPartitionService().clearPartitionTable();
870
+ // Refresh partition information with the new owner
871
+ this.client.getPartitionService().refresh();
872
+ this.logger.info('ClusterService', "Ownership change completed, new owner: " + newOwnerAddress.toString());
873
+ };
874
+ /**
875
+ * Gets the failover manager for external access
876
+ * @returns The failover manager instance
877
+ */
878
+ ClusterService.prototype.getFailoverManager = function () {
879
+ return this.failoverManager;
880
+ };
881
+ ClusterService.prototype.shutdown = function () {
882
+ if (this.reconnectionTask) {
883
+ clearInterval(this.reconnectionTask);
884
+ this.reconnectionTask = null;
885
+ }
886
+ this.downAddresses.clear();
887
+ };
415
888
  return ClusterService;
416
889
  }());
417
890
  exports.ClusterService = ClusterService;
@@ -10,5 +10,16 @@ export declare class ConnectionAuthenticator {
10
10
  private logger;
11
11
  constructor(connection: ClientConnection, client: HazelcastClient);
12
12
  authenticate(asOwner: boolean): Promise<void>;
13
+ /**
14
+ * Gets a human-readable description of authentication status
15
+ */
16
+ private getStatusDescription(status);
17
+ /**
18
+ * Creates credentials with optional restoration from preservation service
19
+ * @param asOwner Whether this is an owner connection
20
+ * @param preservedCredentials Optional preserved credentials to use
21
+ * @returns The credentials message
22
+ */
23
+ createCredentialsWithRestoration(asOwner: boolean, preservedCredentials?: any): ClientMessage;
13
24
  createCredentials(asOwner: boolean): ClientMessage;
14
25
  }