Skip to content

competing reconnect loops when the entire cluster goes down — important for production stability #24

Description

@scottf

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions