When the whole cluster goes down, handleCommunicationIssue fires on both active and passive simultaneously. Both connections independently spin up reconnect threads and hammer the same dead brokers. This doubles the reconnect load hitting the brokers the moment they come back up. At scale — many application instances, each with two competing threads firing every 0–21 ms — the recovering brokers can be flooded before they stabilise.
More critically: even when active reconnects successfully via super.reconnectImplConnect(), newPassive() is never called. The warm standby is silently lost with no log entry or alert. The application then runs in active-only mode — with none of the AP failover guarantees — until the next restart, with nothing in the logs to indicate the degraded state.
Current code path (simplified):
// ApConnection.reconnectImplConnect() — whole-cluster path
if (!passive.isConnected()) {
super.reconnectImplConnect(); // active reconnects, but...
return; // passive's independent thread still running
// newPassive() never called — warm standby silently lost
}
Proposed fix — close the passive connection before reconnecting active so its competing thread is stopped, then re-establish the warm standby once active is back:
if (!passive.isConnected()) {
NatsConnection deadPassive = this.passiveConnection;
this.passiveConnection = null; // null before close so re-entrant guard still works
deadPassive.close(); // stops passive's independent reconnect thread
super.reconnectImplConnect(); // active is now the sole reconnect driver
if (isConnected()) {
apServerPool.setActiveServer(currentServer); // update pool before newPassive()
newPassive(); // re-establish warm standby once active is back
}
return;
}
One additional consequence worth flagging: if maxReconnects is finite, passive's racing thread accumulates connectFailed() calls against the shared underlying pool before it is stopped. Once a broker's failed-attempt counter reaches the limit, that broker is physically removed from entryList. By the time active recovers and newPassive() runs, the pool may already have entries removed — nextServer() returns null immediately and the passive is never re-established. With maxReconnects = -1 this does not apply, but it is a real risk for any deployment using a finite reconnect limit.
When the whole cluster goes down, handleCommunicationIssue fires on both active and passive simultaneously. Both connections independently spin up reconnect threads and hammer the same dead brokers. This doubles the reconnect load hitting the brokers the moment they come back up. At scale — many application instances, each with two competing threads firing every 0–21 ms — the recovering brokers can be flooded before they stabilise.
More critically: even when active reconnects successfully via super.reconnectImplConnect(), newPassive() is never called. The warm standby is silently lost with no log entry or alert. The application then runs in active-only mode — with none of the AP failover guarantees — until the next restart, with nothing in the logs to indicate the degraded state.
Current code path (simplified):
Proposed fix — close the passive connection before reconnecting active so its competing thread is stopped, then re-establish the warm standby once active is back:
One additional consequence worth flagging: if maxReconnects is finite, passive's racing thread accumulates connectFailed() calls against the shared underlying pool before it is stopped. Once a broker's failed-attempt counter reaches the limit, that broker is physically removed from entryList. By the time active recovers and newPassive() runs, the pool may already have entries removed — nextServer() returns null immediately and the passive is never re-established. With maxReconnects = -1 this does not apply, but it is a real risk for any deployment using a finite reconnect limit.