@celerispay/hazelcast-client 3.12.5 → 3.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -87
- package/CHANGES_UNCOMMITTED.md +52 -0
- package/FAILOVER_FIXES.md +148 -230
- package/FAULT_TOLERANCE_IMPROVEMENTS.md +208 -0
- package/HAZELCAST_CLIENT_EVOLUTION.md +402 -0
- package/QUICK_START.md +184 -95
- package/RELEASE_SUMMARY.md +227 -147
- package/lib/HeartbeatService.js +11 -2
- package/lib/PartitionService.d.ts +14 -0
- package/lib/PartitionService.js +32 -9
- package/lib/invocation/ClientConnection.d.ts +14 -0
- package/lib/invocation/ClientConnection.js +95 -1
- package/lib/invocation/ClientConnectionManager.d.ts +95 -0
- package/lib/invocation/ClientConnectionManager.js +369 -7
- package/lib/invocation/ClusterService.d.ts +75 -5
- package/lib/invocation/ClusterService.js +430 -15
- package/lib/invocation/ConnectionAuthenticator.d.ts +11 -0
- package/lib/invocation/ConnectionAuthenticator.js +85 -12
- package/lib/invocation/CredentialPreservationService.d.ts +137 -0
- package/lib/invocation/CredentialPreservationService.js +369 -0
- package/lib/invocation/HazelcastFailoverManager.d.ts +102 -0
- package/lib/invocation/HazelcastFailoverManager.js +285 -0
- package/lib/invocation/InvocationService.js +8 -0
- package/lib/nearcache/StaleReadDetectorImpl.js +31 -4
- package/lib/proxy/ProxyManager.js +25 -4
- package/package.json +20 -28
|
@@ -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 =
|
|
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,12 @@ 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
|
+
};
|
|
101
114
|
/**
|
|
102
115
|
* Returns the list of members in the cluster.
|
|
103
116
|
* @returns
|
|
@@ -139,16 +152,15 @@ var ClusterService = /** @class */ (function () {
|
|
|
139
152
|
ClusterService.prototype.getClientInfo = function () {
|
|
140
153
|
var info = new ClientInfo_1.ClientInfo();
|
|
141
154
|
info.uuid = this.uuid;
|
|
142
|
-
|
|
155
|
+
var ownerConnection = this.getOwnerConnection();
|
|
156
|
+
if (ownerConnection) {
|
|
157
|
+
info.localAddress = ownerConnection.getLocalAddress();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
info.localAddress = null;
|
|
161
|
+
}
|
|
143
162
|
return info;
|
|
144
163
|
};
|
|
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
164
|
/**
|
|
153
165
|
* Adds MembershipListener to listen for membership updates. There is no check for duplicate registrations,
|
|
154
166
|
* so if you register the listener twice, it will get events twice.
|
|
@@ -180,7 +192,11 @@ var ClusterService = /** @class */ (function () {
|
|
|
180
192
|
var handleAttributeChange = _this.handleMemberAttributeChange.bind(_this);
|
|
181
193
|
ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.handle(m, handleMember, handleMemberList, handleAttributeChange, null);
|
|
182
194
|
};
|
|
183
|
-
|
|
195
|
+
var ownerConnection = this.getOwnerConnection();
|
|
196
|
+
if (!ownerConnection) {
|
|
197
|
+
return Promise.reject(new Error('Cannot initialize membership listener: no owner connection available'));
|
|
198
|
+
}
|
|
199
|
+
return this.client.getInvocationService().invokeOnConnection(ownerConnection, request, handler)
|
|
184
200
|
.then(function (resp) {
|
|
185
201
|
_this.logger.trace('ClusterService', 'Registered listener with id '
|
|
186
202
|
+ ClientAddMembershipListenerCodec_1.ClientAddMembershipListenerCodec.decodeResponse(resp).response);
|
|
@@ -196,6 +212,8 @@ var ClusterService = /** @class */ (function () {
|
|
|
196
212
|
};
|
|
197
213
|
ClusterService.prototype.onConnectionClosed = function (connection) {
|
|
198
214
|
this.logger.warn('ClusterService', 'Connection closed to ' + connection.toString());
|
|
215
|
+
// Mark the address as down when connection is closed
|
|
216
|
+
this.markAddressAsDown(connection.getAddress());
|
|
199
217
|
if (connection.isAuthenticatedAsOwner()) {
|
|
200
218
|
this.ownerConnection = null;
|
|
201
219
|
this.triggerFailover();
|
|
@@ -203,6 +221,8 @@ var ClusterService = /** @class */ (function () {
|
|
|
203
221
|
};
|
|
204
222
|
ClusterService.prototype.onHeartbeatStopped = function (connection) {
|
|
205
223
|
this.logger.warn('ClusterService', connection.toString() + ' stopped heartbeating.');
|
|
224
|
+
// Mark the address as down when heartbeat stops
|
|
225
|
+
this.markAddressAsDown(connection.getAddress());
|
|
206
226
|
if (connection.isAuthenticatedAsOwner()) {
|
|
207
227
|
this.client.getConnectionManager().destroyConnection(connection.getAddress());
|
|
208
228
|
this.ownerConnection = null;
|
|
@@ -218,23 +238,69 @@ var ClusterService = /** @class */ (function () {
|
|
|
218
238
|
}
|
|
219
239
|
this.failoverInProgress = true;
|
|
220
240
|
this.lastFailoverAttempt = now;
|
|
221
|
-
this.logger.info('ClusterService', 'Starting failover process...');
|
|
241
|
+
this.logger.info('ClusterService', '🚀 Starting failover process - SERVER-FIRST APPROACH...');
|
|
242
|
+
// SERVER-FIRST: No credential preservation needed
|
|
243
|
+
// We trust the server will provide correct member information
|
|
244
|
+
this.logger.info('ClusterService', '🎯 SERVER-FIRST: No credential management - trusting server data');
|
|
245
|
+
// Log state before failover
|
|
246
|
+
this.logCurrentState();
|
|
247
|
+
// Force cleanup of all dead connections to prevent leakage
|
|
248
|
+
this.client.getConnectionManager().forceCleanupDeadConnections();
|
|
222
249
|
// Clear any stale partition information
|
|
223
250
|
this.client.getPartitionService().clearPartitionTable();
|
|
224
251
|
// Attempt to reconnect to cluster
|
|
225
252
|
this.connectToCluster()
|
|
226
253
|
.then(function () {
|
|
227
|
-
_this.logger.info('ClusterService', 'Failover completed successfully');
|
|
254
|
+
_this.logger.info('ClusterService', '✅ Failover completed successfully - SERVER-FIRST approach');
|
|
255
|
+
// No credential management needed - server handles everything
|
|
256
|
+
_this.logCurrentState(); // Log state after successful failover
|
|
228
257
|
})
|
|
229
258
|
.catch(function (error) {
|
|
230
259
|
_this.logger.error('ClusterService', 'Failover failed', error);
|
|
231
|
-
// If failover fails,
|
|
232
|
-
_this.
|
|
260
|
+
// If failover fails, try to unblock at least one address to allow recovery
|
|
261
|
+
_this.attemptEmergencyRecovery();
|
|
262
|
+
_this.logCurrentState(); // Log state after failed failover
|
|
263
|
+
// Don't shutdown immediately, give recovery a chance
|
|
233
264
|
})
|
|
234
265
|
.finally(function () {
|
|
235
266
|
_this.failoverInProgress = false;
|
|
236
267
|
});
|
|
237
268
|
};
|
|
269
|
+
/**
|
|
270
|
+
* Attempts emergency recovery when failover fails
|
|
271
|
+
*/
|
|
272
|
+
ClusterService.prototype.attemptEmergencyRecovery = function () {
|
|
273
|
+
var _this = this;
|
|
274
|
+
this.logger.warn('ClusterService', 'Attempting emergency recovery...');
|
|
275
|
+
// Unblock at least one address to allow recovery
|
|
276
|
+
if (this.downAddresses.size > 0) {
|
|
277
|
+
var firstBlockedAddress_1 = Array.from(this.downAddresses.keys())[0];
|
|
278
|
+
this.logger.info('ClusterService', "Emergency unblocking address " + firstBlockedAddress_1);
|
|
279
|
+
this.downAddresses.delete(firstBlockedAddress_1);
|
|
280
|
+
// Try to connect to the unblocked address
|
|
281
|
+
try {
|
|
282
|
+
var _a = firstBlockedAddress_1.split(':'), host = _a[0], portStr = _a[1];
|
|
283
|
+
var port = parseInt(portStr, 10);
|
|
284
|
+
if (host && !isNaN(port)) {
|
|
285
|
+
var address_1 = new Address(host, port);
|
|
286
|
+
this.logger.info('ClusterService', "Attempting emergency connection to " + firstBlockedAddress_1);
|
|
287
|
+
// Try to connect without blocking
|
|
288
|
+
this.client.getConnectionManager().getOrConnect(address_1, false)
|
|
289
|
+
.then(function (connection) {
|
|
290
|
+
_this.logger.info('ClusterService', "Emergency connection successful to " + firstBlockedAddress_1);
|
|
291
|
+
_this.evaluateOwnershipChange(address_1, connection);
|
|
292
|
+
_this.client.getPartitionService().refresh();
|
|
293
|
+
})
|
|
294
|
+
.catch(function (error) {
|
|
295
|
+
_this.logger.warn('ClusterService', "Emergency connection failed to " + firstBlockedAddress_1 + ":", error);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.logger.error('ClusterService', 'Error during emergency recovery:', error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
238
304
|
ClusterService.prototype.isAddressKnownDown = function (address) {
|
|
239
305
|
var addressStr = address.toString();
|
|
240
306
|
var downTime = this.downAddresses.get(addressStr);
|
|
@@ -341,6 +407,8 @@ var ClusterService = /** @class */ (function () {
|
|
|
341
407
|
this.members = members;
|
|
342
408
|
this.client.getPartitionService().refresh();
|
|
343
409
|
this.logger.info('ClusterService', 'Members received.', this.members);
|
|
410
|
+
// Log current state after member list update
|
|
411
|
+
this.logCurrentState();
|
|
344
412
|
var events = this.detectMembershipEvents(prevMembers);
|
|
345
413
|
for (var _i = 0, events_1 = events; _i < events_1.length; _i++) {
|
|
346
414
|
var event = events_1[_i];
|
|
@@ -399,6 +467,8 @@ var ClusterService = /** @class */ (function () {
|
|
|
399
467
|
};
|
|
400
468
|
ClusterService.prototype.memberAdded = function (member) {
|
|
401
469
|
this.members.push(member);
|
|
470
|
+
// Handle member added and update preserved credentials
|
|
471
|
+
this.handleMemberAdded(member);
|
|
402
472
|
var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.ADDED, this.members);
|
|
403
473
|
this.fireMembershipEvent(membershipEvent);
|
|
404
474
|
};
|
|
@@ -408,10 +478,355 @@ var ClusterService = /** @class */ (function () {
|
|
|
408
478
|
var removedMemberList = this.members.splice(memberIndex, 1);
|
|
409
479
|
assert(removedMemberList.length === 1);
|
|
410
480
|
}
|
|
411
|
-
this
|
|
481
|
+
// Check if we have a healthy connection to this member
|
|
482
|
+
var connectionManager = this.client.getConnectionManager();
|
|
483
|
+
var existingConnection = connectionManager.getConnection(member.address);
|
|
484
|
+
if (existingConnection && existingConnection.isHealthy()) {
|
|
485
|
+
// If the connection is healthy, don't destroy it immediately
|
|
486
|
+
// This prevents unnecessary disconnections during temporary network issues
|
|
487
|
+
this.logger.info('ClusterService', "Member removed but connection is healthy: " + member.address.toString() + ", preserving connection");
|
|
488
|
+
// Only destroy if we're not in failover mode
|
|
489
|
+
if (!this.failoverInProgress) {
|
|
490
|
+
this.logger.debug('ClusterService', "Destroying healthy connection to removed member: " + member.address.toString());
|
|
491
|
+
connectionManager.destroyConnection(member.address);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.logger.debug('ClusterService', "Preserving healthy connection during failover: " + member.address.toString());
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// If connection is unhealthy, destroy it
|
|
499
|
+
this.logger.info('ClusterService', "Member removed with unhealthy connection: " + member.address.toString() + ", destroying connection");
|
|
500
|
+
connectionManager.destroyConnection(member.address);
|
|
501
|
+
}
|
|
412
502
|
var membershipEvent = new MembershipEvent_1.MembershipEvent(member, MemberEvent.REMOVED, this.members);
|
|
413
503
|
this.fireMembershipEvent(membershipEvent);
|
|
414
504
|
};
|
|
505
|
+
/**
|
|
506
|
+
* Performs comprehensive credential cleanup when cluster membership changes
|
|
507
|
+
* This ensures ALL credentials are consistent with the current cluster owner UUID
|
|
508
|
+
* @param connectionManager The connection manager instance
|
|
509
|
+
* @param currentClusterOwnerUuid The current cluster owner UUID
|
|
510
|
+
*/
|
|
511
|
+
/**
|
|
512
|
+
* Handles member added event - SERVER-FIRST APPROACH
|
|
513
|
+
* We trust what the server tells us and store it as credentials
|
|
514
|
+
* @param member The member that was added
|
|
515
|
+
*/
|
|
516
|
+
ClusterService.prototype.handleMemberAdded = function (member) {
|
|
517
|
+
this.logger.info('ClusterService', "\u2705 SERVER CONFIRMED: Member[ uuid: " + member.uuid + ", address: " + member.address.toString() + "] added to cluster");
|
|
518
|
+
// SERVER-FIRST: Store server data as credentials
|
|
519
|
+
// The server is the authority - we store what it tells us
|
|
520
|
+
this.logger.info('ClusterService', "\uD83C\uDFAF SERVER-FIRST: Storing server member data as credentials - server is authority");
|
|
521
|
+
var connectionManager = this.client.getConnectionManager();
|
|
522
|
+
// Store the server-provided UUID as the authoritative credential
|
|
523
|
+
if (connectionManager && typeof connectionManager.updatePreservedCredentials === 'function') {
|
|
524
|
+
connectionManager.updatePreservedCredentials(member.address, member.uuid);
|
|
525
|
+
}
|
|
526
|
+
// Record that we received a member added event for this address
|
|
527
|
+
if (connectionManager && typeof connectionManager.recordMemberAddedEvent === 'function') {
|
|
528
|
+
connectionManager.recordMemberAddedEvent(member.address);
|
|
529
|
+
}
|
|
530
|
+
// Find the current owner from the cluster state
|
|
531
|
+
var currentOwner = this.findCurrentOwner();
|
|
532
|
+
if (currentOwner) {
|
|
533
|
+
this.logger.info('ClusterService', "\uD83D\uDD04 SERVER-FIRST: Updating ALL credentials with current owner UUID: " + currentOwner.uuid);
|
|
534
|
+
// Update all credentials with the current owner UUID from server
|
|
535
|
+
if (connectionManager && typeof connectionManager.updateAllCredentialsWithNewOwnerUuid === 'function') {
|
|
536
|
+
connectionManager.updateAllCredentialsWithNewOwnerUuid(currentOwner.uuid);
|
|
537
|
+
}
|
|
538
|
+
// CRITICAL FIX: Update client's own UUIDs to match server expectations
|
|
539
|
+
this.logger.info('ClusterService', "\uD83D\uDD04 SERVER-FIRST: Updating client UUIDs to match server state");
|
|
540
|
+
this.logger.info('ClusterService', " - Old Client UUID: " + (this.uuid || 'NOT SET'));
|
|
541
|
+
this.logger.info('ClusterService', " - Old Owner UUID: " + (this.ownerUuid || 'NOT SET'));
|
|
542
|
+
// Update client's own UUIDs with server-provided data
|
|
543
|
+
// The client UUID should match the owner's UUID for authentication
|
|
544
|
+
this.uuid = currentOwner.uuid;
|
|
545
|
+
this.ownerUuid = currentOwner.uuid;
|
|
546
|
+
this.logger.info('ClusterService', " - New Client UUID: " + this.uuid);
|
|
547
|
+
this.logger.info('ClusterService', " - New Owner UUID: " + this.ownerUuid);
|
|
548
|
+
}
|
|
549
|
+
// Refresh partition table (KEEPING REFRESH METHOD UNTOUCHED as requested)
|
|
550
|
+
this.client.getPartitionService().refresh();
|
|
551
|
+
this.logger.info('ClusterService', "\u2705 SERVER-FIRST: Member " + member.uuid + " at " + member.address.toString() + " credentials stored from server data");
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Finds the current owner from the cluster state
|
|
555
|
+
* @returns The current owner member or null if not found
|
|
556
|
+
*/
|
|
557
|
+
ClusterService.prototype.findCurrentOwner = function () {
|
|
558
|
+
// Check if we have an active owner connection
|
|
559
|
+
var ownerConnection = this.ownerConnection;
|
|
560
|
+
if (ownerConnection && ownerConnection.isAlive()) {
|
|
561
|
+
var ownerAddress = ownerConnection.getAddress();
|
|
562
|
+
// Find the member with this address
|
|
563
|
+
for (var _i = 0, _a = this.members; _i < _a.length; _i++) {
|
|
564
|
+
var member = _a[_i];
|
|
565
|
+
if (member.address.toString() === ownerAddress.toString()) {
|
|
566
|
+
this.logger.debug('ClusterService', "Found current owner: " + member.uuid + " at " + member.address.toString());
|
|
567
|
+
return member;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// Fallback: look for any member that might be the owner
|
|
572
|
+
this.logger.debug('ClusterService', "No active owner connection found, checking member list for potential owner");
|
|
573
|
+
return null;
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* Logs the current state for debugging purposes
|
|
577
|
+
*/
|
|
578
|
+
ClusterService.prototype.logCurrentState = function () {
|
|
579
|
+
var _this = this;
|
|
580
|
+
var activeConnections = Object.keys(this.client.getConnectionManager().getEstablishedConnections()).length;
|
|
581
|
+
var memberCount = this.members.length;
|
|
582
|
+
var downAddressesCount = this.downAddresses.size;
|
|
583
|
+
var hasOwner = !!this.ownerConnection;
|
|
584
|
+
this.logger.info('ClusterService', "Current State - Members: " + memberCount + ", Active Connections: " + activeConnections + ", Down Addresses: " + downAddressesCount + ", Has Owner: " + hasOwner);
|
|
585
|
+
if (this.ownerConnection) {
|
|
586
|
+
this.logger.info('ClusterService', "Owner Connection: " + this.ownerConnection.getAddress().toString() + ", Alive: " + this.ownerConnection.isAlive());
|
|
587
|
+
}
|
|
588
|
+
// Log all active connections
|
|
589
|
+
var connections = this.client.getConnectionManager().getEstablishedConnections();
|
|
590
|
+
Object.keys(connections).forEach(function (addressStr) {
|
|
591
|
+
var connection = connections[addressStr];
|
|
592
|
+
_this.logger.debug('ClusterService', "Connection to " + addressStr + ": Alive=" + connection.isAlive() + ", Owner=" + connection.isAuthenticatedAsOwner());
|
|
593
|
+
});
|
|
594
|
+
// Log down addresses
|
|
595
|
+
if (downAddressesCount > 0) {
|
|
596
|
+
var downAddresses = Array.from(this.downAddresses.keys());
|
|
597
|
+
this.logger.debug('ClusterService', "Down Addresses: " + downAddresses.join(', '));
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
ClusterService.prototype.startReconnectionTask = function () {
|
|
601
|
+
var _this = this;
|
|
602
|
+
// Periodically attempt to reconnect to previously failed addresses
|
|
603
|
+
this.reconnectionTask = setInterval(function () {
|
|
604
|
+
_this.attemptReconnectionToFailedNodes();
|
|
605
|
+
}, this.reconnectionInterval);
|
|
606
|
+
};
|
|
607
|
+
/**
|
|
608
|
+
* Starts a periodic task to log the current state for debugging
|
|
609
|
+
*/
|
|
610
|
+
ClusterService.prototype.startStateLoggingTask = function () {
|
|
611
|
+
var _this = this;
|
|
612
|
+
// Log state every 30 seconds for debugging
|
|
613
|
+
setInterval(function () {
|
|
614
|
+
if (_this.client.getLifecycleService().isRunning()) {
|
|
615
|
+
_this.logCurrentState();
|
|
616
|
+
}
|
|
617
|
+
}, 30000);
|
|
618
|
+
};
|
|
619
|
+
ClusterService.prototype.attemptReconnectionToFailedNodes = function () {
|
|
620
|
+
var _this = this;
|
|
621
|
+
// Allow reconnection even during failover, but be more careful
|
|
622
|
+
if (this.failoverInProgress) {
|
|
623
|
+
this.logger.debug('ClusterService', 'Skipping reconnection attempt during failover');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
var now = Date.now();
|
|
627
|
+
var addressesToReconnect = [];
|
|
628
|
+
var totalDownAddresses = this.downAddresses.size;
|
|
629
|
+
// If we have no down addresses, we can skip
|
|
630
|
+
if (totalDownAddresses === 0) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// Find addresses that are no longer blocked
|
|
634
|
+
this.downAddresses.forEach(function (downTime, addressStr) {
|
|
635
|
+
var timeSinceDown = now - downTime;
|
|
636
|
+
if (timeSinceDown > _this.addressBlockDuration) {
|
|
637
|
+
// Parse the address string back to Address object
|
|
638
|
+
try {
|
|
639
|
+
var _a = addressStr.split(':'), host = _a[0], portStr = _a[1];
|
|
640
|
+
var port = parseInt(portStr, 10);
|
|
641
|
+
if (host && !isNaN(port)) {
|
|
642
|
+
var address = new Address(host, port);
|
|
643
|
+
// Check if we already have a connection to this address
|
|
644
|
+
if (_this.client.getConnectionManager().hasConnection(address)) {
|
|
645
|
+
_this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", removing from down addresses");
|
|
646
|
+
_this.downAddresses.delete(addressStr);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
addressesToReconnect.push(address);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
_this.logger.warn('ClusterService', "Failed to parse address " + addressStr + " for reconnection:", error);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
if (addressesToReconnect.length > 0) {
|
|
658
|
+
this.logger.info('ClusterService', "Attempting to reconnect to " + addressesToReconnect.length + " previously failed nodes: " + addressesToReconnect.map(function (addr) { return addr.toString(); }).join(', '));
|
|
659
|
+
// Attempt to establish connections to each unblocked address
|
|
660
|
+
addressesToReconnect.forEach(function (address) {
|
|
661
|
+
_this.attemptReconnectionToAddress(address);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
else if (totalDownAddresses > 0) {
|
|
665
|
+
// Log remaining blocked addresses for debugging
|
|
666
|
+
var remainingBlocked = Array.from(this.downAddresses.keys()).map(function (addr) {
|
|
667
|
+
var downTime = _this.downAddresses.get(addr);
|
|
668
|
+
var timeSinceDown = now - downTime;
|
|
669
|
+
var remainingTime = Math.max(0, _this.addressBlockDuration - timeSinceDown);
|
|
670
|
+
return addr + " (" + Math.ceil(remainingTime / 1000) + "s remaining)";
|
|
671
|
+
});
|
|
672
|
+
this.logger.debug('ClusterService', "Still waiting for " + totalDownAddresses + " addresses to unblock: " + remainingBlocked.join(', '));
|
|
673
|
+
}
|
|
674
|
+
// Log current state after reconnection attempts
|
|
675
|
+
this.logCurrentState();
|
|
676
|
+
};
|
|
677
|
+
/**
|
|
678
|
+
* Attempts to establish a connection to a specific address
|
|
679
|
+
* @param address The address to reconnect to
|
|
680
|
+
*/
|
|
681
|
+
ClusterService.prototype.attemptReconnectionToAddress = function (address) {
|
|
682
|
+
var _this = this;
|
|
683
|
+
var addressStr = address.toString();
|
|
684
|
+
// Check if we already have a connection to this address
|
|
685
|
+
if (this.client.getConnectionManager().hasConnection(address)) {
|
|
686
|
+
this.logger.debug('ClusterService', "Already have active connection to " + addressStr + ", skipping reconnection");
|
|
687
|
+
this.downAddresses.delete(addressStr);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
// Check if we're already trying to connect to this address
|
|
691
|
+
var connectionManager = this.client.getConnectionManager();
|
|
692
|
+
var establishedConnections = connectionManager.getEstablishedConnections();
|
|
693
|
+
var pendingConnections = Object.keys(connectionManager.getPendingConnections()).length;
|
|
694
|
+
if (pendingConnections > 0) {
|
|
695
|
+
this.logger.debug('ClusterService', "Already have pending connections, skipping reconnection to " + addressStr);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
// Remove from down addresses to allow connection attempt
|
|
699
|
+
this.downAddresses.delete(addressStr);
|
|
700
|
+
this.logger.debug('ClusterService', "Attempting reconnection to " + addressStr);
|
|
701
|
+
// Attempt to establish connection (not as owner, just as regular member connection)
|
|
702
|
+
this.client.getConnectionManager().getOrConnect(address, false)
|
|
703
|
+
.then(function (connection) {
|
|
704
|
+
_this.logger.info('ClusterService', "Successfully reconnected to " + addressStr);
|
|
705
|
+
// Only evaluate ownership change if we don't have an owner or current owner is unhealthy
|
|
706
|
+
if (!_this.ownerConnection || !_this.ownerConnection.isHealthy()) {
|
|
707
|
+
_this.logger.info('ClusterService', "Evaluating ownership change for " + addressStr);
|
|
708
|
+
_this.evaluateOwnershipChange(address, connection);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
_this.logger.debug('ClusterService', "Keeping " + addressStr + " as member connection, current owner is healthy");
|
|
712
|
+
}
|
|
713
|
+
// Trigger partition service refresh to update routing information
|
|
714
|
+
_this.client.getPartitionService().refresh();
|
|
715
|
+
}).catch(function (error) {
|
|
716
|
+
_this.logger.warn('ClusterService', "Reconnection attempt to " + addressStr + " failed:", error);
|
|
717
|
+
// Mark the address as down again, but with a shorter block duration for reconnection attempts
|
|
718
|
+
var shorterBlockDuration = Math.min(_this.addressBlockDuration / 2, 15000); // Max 15 seconds
|
|
719
|
+
_this.markAddressAsDownWithDuration(address, shorterBlockDuration);
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
/**
|
|
723
|
+
* Evaluates whether we should switch ownership to a reconnected node
|
|
724
|
+
* @param address The address of the reconnected node
|
|
725
|
+
* @param connection The connection to the reconnected node
|
|
726
|
+
*/
|
|
727
|
+
ClusterService.prototype.evaluateOwnershipChange = function (address, connection) {
|
|
728
|
+
// If we don't have an owner connection, this reconnected node becomes the owner
|
|
729
|
+
if (!this.ownerConnection) {
|
|
730
|
+
this.logger.info('ClusterService', "Promoting reconnected node " + address.toString() + " to owner status");
|
|
731
|
+
this.promoteToOwner(connection, address);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
// If our current owner connection is having issues, consider switching
|
|
735
|
+
if (this.ownerConnection && !this.ownerConnection.isAlive()) {
|
|
736
|
+
this.logger.info('ClusterService', "Current owner is unhealthy, switching to reconnected node " + address.toString());
|
|
737
|
+
this.promoteToOwner(connection, address);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
// Don't switch ownership if current owner is healthy
|
|
741
|
+
this.logger.debug('ClusterService', "Current owner is healthy, keeping " + address.toString() + " as member connection");
|
|
742
|
+
};
|
|
743
|
+
/**
|
|
744
|
+
* Promotes a connection to owner status
|
|
745
|
+
* @param connection The connection to promote
|
|
746
|
+
* @param address The address of the promoted connection
|
|
747
|
+
*/
|
|
748
|
+
ClusterService.prototype.promoteToOwner = function (connection, address) {
|
|
749
|
+
try {
|
|
750
|
+
// Close the old owner connection if it exists
|
|
751
|
+
if (this.ownerConnection && this.ownerConnection !== connection) {
|
|
752
|
+
this.logger.info('ClusterService', "Closing previous owner connection to " + this.ownerConnection.getAddress().toString());
|
|
753
|
+
this.client.getConnectionManager().destroyConnection(this.ownerConnection.getAddress());
|
|
754
|
+
}
|
|
755
|
+
// Set the new owner connection
|
|
756
|
+
connection.setAuthenticatedAsOwner(true);
|
|
757
|
+
this.ownerConnection = connection;
|
|
758
|
+
this.logger.info('ClusterService', "Successfully promoted " + address.toString() + " to owner status");
|
|
759
|
+
// Refresh partition information with the new owner
|
|
760
|
+
this.client.getPartitionService().refresh();
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
this.logger.error('ClusterService', "Failed to promote " + address.toString() + " to owner:", error);
|
|
764
|
+
// If promotion fails, mark the address as down again
|
|
765
|
+
this.markAddressAsDown(address);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* Marks an address as down with a custom block duration
|
|
770
|
+
* @param address The address to mark as down
|
|
771
|
+
* @param blockDuration The duration to block the address (in milliseconds)
|
|
772
|
+
*/
|
|
773
|
+
ClusterService.prototype.markAddressAsDownWithDuration = function (address, blockDuration) {
|
|
774
|
+
var _this = this;
|
|
775
|
+
var addressStr = address.toString();
|
|
776
|
+
var now = Date.now();
|
|
777
|
+
// Don't block if we already have a healthy connection to this address
|
|
778
|
+
if (this.client.getConnectionManager().hasConnection(address)) {
|
|
779
|
+
this.logger.debug('ClusterService', "Not blocking " + addressStr + " as we have a healthy connection");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
// Don't block if this would leave us with no available nodes
|
|
783
|
+
var totalDownAddresses = this.downAddresses.size;
|
|
784
|
+
var totalMembers = this.members.length;
|
|
785
|
+
if (totalDownAddresses >= totalMembers - 1) {
|
|
786
|
+
this.logger.warn('ClusterService', "Not blocking " + addressStr + " as it would leave us with no available nodes");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.downAddresses.set(addressStr, now);
|
|
790
|
+
this.logger.warn('ClusterService', "Marked address " + addressStr + " as down, will be blocked for " + blockDuration + "ms");
|
|
791
|
+
// Schedule cleanup of this address after block duration
|
|
792
|
+
setTimeout(function () {
|
|
793
|
+
if (_this.downAddresses.has(addressStr)) {
|
|
794
|
+
_this.logger.info('ClusterService', "Unblocking address " + addressStr + " after block duration");
|
|
795
|
+
_this.downAddresses.delete(addressStr);
|
|
796
|
+
}
|
|
797
|
+
}, blockDuration);
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Handles ownership change when failover occurs
|
|
801
|
+
* @param newOwnerAddress The address of the new owner
|
|
802
|
+
* @param newOwnerConnection The connection to the new owner
|
|
803
|
+
*/
|
|
804
|
+
ClusterService.prototype.handleOwnershipChange = function (newOwnerAddress, newOwnerConnection) {
|
|
805
|
+
this.logger.info('ClusterService', "Handling ownership change to " + newOwnerAddress.toString());
|
|
806
|
+
// Update owner connection
|
|
807
|
+
this.ownerConnection = newOwnerConnection;
|
|
808
|
+
// Note: Owner UUID will be updated when authentication completes
|
|
809
|
+
// For now, we'll keep the existing owner UUID
|
|
810
|
+
// Clear any stale partition information
|
|
811
|
+
this.client.getPartitionService().clearPartitionTable();
|
|
812
|
+
// Refresh partition information with the new owner
|
|
813
|
+
this.client.getPartitionService().refresh();
|
|
814
|
+
this.logger.info('ClusterService', "Ownership change completed, new owner: " + newOwnerAddress.toString());
|
|
815
|
+
};
|
|
816
|
+
/**
|
|
817
|
+
* Gets the failover manager for external access
|
|
818
|
+
* @returns The failover manager instance
|
|
819
|
+
*/
|
|
820
|
+
ClusterService.prototype.getFailoverManager = function () {
|
|
821
|
+
return this.failoverManager;
|
|
822
|
+
};
|
|
823
|
+
ClusterService.prototype.shutdown = function () {
|
|
824
|
+
if (this.reconnectionTask) {
|
|
825
|
+
clearInterval(this.reconnectionTask);
|
|
826
|
+
this.reconnectionTask = null;
|
|
827
|
+
}
|
|
828
|
+
this.downAddresses.clear();
|
|
829
|
+
};
|
|
415
830
|
return ClusterService;
|
|
416
831
|
}());
|
|
417
832
|
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
|
}
|