Skip to content

CachedConnection.getConnection retries every SQLException forever, so a failed connection hangs instead of failing #875

Description

@vharseko

CachedConnection.getConnection treats every SQLException from DriverManager.getConnection as pool exhaustion and recurses with a doubling wait. The recursion has no bound, so a wrong password, an unreachable host or a database that is down never surfaces as an error — the caller simply never returns.

Surfaced while reviewing #867; it does not belong to that PR.

The loop

// opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java:82
static Connection getConnection(String connectionString, final int waitTime) throws Exception {
    CachedConnection con = cached.get(connectionString).poll(waitTime, TimeUnit.MILLISECONDS);

    while (con != null) { ... }
    try {
        final Connection conNew = DriverManager.getConnection(connectionString);
        conNew.setAutoCommit(false);
        conNew.setTransactionIsolation(TRANSACTION_READ_COMMITTED);
        return new CachedConnection(connectionString, conNew);
    } catch (SQLException e) { // max_connection server error: try recursion for reuse connection
        return getConnection(connectionString, (waitTime == 0) ? 1 : waitTime * 2);
    }
}

Two problems, both in that one catch.

The condition is not checked. The comment names max_connections, but the catch takes any SQLException: bad credentials (28000), unknown database, a refused TCP connection, an expired certificate. None of them clears by waiting, and none of them is ever reported. e is not even logged, so there is nothing in the log to explain the hang.

There is no bound. waitTime doubles per attempt, so the wait is roughly 2^n ms: about a second by attempt 10, seventeen minutes by attempt 20, eleven days by attempt 30. In practice the call simply never returns. Being recursion rather than a loop, it also grows the stack by one frame per attempt, but the delays reach absurd values long before the stack does, so the visible symptom is a hang, not a StackOverflowError.

The wait itself is spent in poll(waitTime, ...), so a connection released by another thread is picked up — which is the case the code was written for and does work.

Effect

A JDBC backend whose credentials are wrong, or whose database is down at startup, blocks the thread that opened it instead of failing initialization with the driver's message. JDBCStorage.open() and every read/write go through this method, so the same applies at runtime after the database goes away.

Suggested fix

Bound the retry and narrow it to the condition it was written for: keep retrying while the pool is plausibly exhausted, up to a wall-clock deadline, and rethrow anything else at once. Logging e at debug on each attempt and at warn when the deadline expires would make the difference between "waiting for a connection" and "cannot connect" visible in the log, which today it is not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions