You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The pool of the JDBC backend has no upper bound on the number of connections it holds, and the TTL that is supposed to close the idle ones does not do what its name says.
1. No upper bound. The queue is an unbounded LinkedBlockingQueue and the cache has no maximumSize, so nothing in OpenDJ limits how many connections a backend opens: as many as there are threads asking at once. The only ceiling is the max_connections of the database itself — which the backend then treats as a retry condition, so a burst of concurrent operations turns into a burst of connect attempts against a server already at its limit.
2. The TTL sits on the URL, not on the connection.expireAfterAccess is keyed by the connection string, and the key is touched by every borrow (getConnection) and every return (close()). Under continuous traffic the entry never expires, so the connections in the queue never expire either: a burst that opened 200 connections keeps all 200 open for as long as the backend sees any traffic at all. The documented behaviour of org.openidentityplatform.opendj.jdbc.ttl — "the time after which an idle pooled connection is closed" — only holds when the whole backend is idle, not when a connection is.
3. Expiry is lazy, and the case it is for is the one it misses. The cache is built without a scheduler(), so Caffeine expires an entry during a later cache operation. On a backend that has gone completely idle — the only situation in which the entry can expire at all, per (2) — there is no later operation to trigger it, so the connections stay open on the database side until its own idle reaper takes them.
4. A closed backend does not release its connections.JDBCStorage.close() only flips the status:
The pool entry for its URL is left alone, so disabling or removing a JDBC backend leaves its connections open, and by (3) possibly indefinitely.
There is also a narrow race in the return path: close() does cached.get(connectionString).add(this), and if the entry is evicted between the get and the add, the connection lands in a queue nothing refers to any more — never handed out again, never closed.
Impact
A deployment sized for its steady-state connection count can hold its peak count open indefinitely, and a database shared with other applications sees a backend that never gives connections back. Restarting or disabling a backend does not release them either. On a database with a modest max_connections, the unbounded pool is also what makes the "too many connections" path reachable in the first place.
Expected behavior
an upper bound on the pooled connections of one backend, configurable, with a borrow above it waiting for a returned connection rather than opening another (the deadline of org.openidentityplatform.opendj.jdbc.pool.timeout added in [#872] Bound the connect of the JDBC pool and report a connect it cannot make #876 already covers the wait);
the TTL applied per connection — closing one that has been idle for it — rather than to the pool as a whole;
expiry that happens without a subsequent borrow, so an idle backend really does release its connections;
JDBCStorage.close() releasing the pool of its connection string;
the return path leaving no connection in an evicted queue.
Environment
master (5.2.x), all four JDBC dialects. The line numbers above are of master as it stands; #876 rewrites getConnection but touches none of the cache shape.
Describe the bug
The pool of the JDBC backend has no upper bound on the number of connections it holds, and the TTL that is supposed to close the idle ones does not do what its name says.
Four things follow from that shape.
1. No upper bound. The queue is an unbounded
LinkedBlockingQueueand the cache has nomaximumSize, so nothing in OpenDJ limits how many connections a backend opens: as many as there are threads asking at once. The only ceiling is themax_connectionsof the database itself — which the backend then treats as a retry condition, so a burst of concurrent operations turns into a burst of connect attempts against a server already at its limit.2. The TTL sits on the URL, not on the connection.
expireAfterAccessis keyed by the connection string, and the key is touched by every borrow (getConnection) and every return (close()). Under continuous traffic the entry never expires, so the connections in the queue never expire either: a burst that opened 200 connections keeps all 200 open for as long as the backend sees any traffic at all. The documented behaviour oforg.openidentityplatform.opendj.jdbc.ttl— "the time after which an idle pooled connection is closed" — only holds when the whole backend is idle, not when a connection is.3. Expiry is lazy, and the case it is for is the one it misses. The cache is built without a
scheduler(), so Caffeine expires an entry during a later cache operation. On a backend that has gone completely idle — the only situation in which the entry can expire at all, per (2) — there is no later operation to trigger it, so the connections stay open on the database side until its own idle reaper takes them.4. A closed backend does not release its connections.
JDBCStorage.close()only flips the status:The pool entry for its URL is left alone, so disabling or removing a JDBC backend leaves its connections open, and by (3) possibly indefinitely.
There is also a narrow race in the return path:
close()doescached.get(connectionString).add(this), and if the entry is evicted between thegetand theadd, the connection lands in a queue nothing refers to any more — never handed out again, never closed.Impact
A deployment sized for its steady-state connection count can hold its peak count open indefinitely, and a database shared with other applications sees a backend that never gives connections back. Restarting or disabling a backend does not release them either. On a database with a modest
max_connections, the unbounded pool is also what makes the "too many connections" path reachable in the first place.Expected behavior
org.openidentityplatform.opendj.jdbc.pool.timeoutadded in [#872] Bound the connect of the JDBC pool and report a connect it cannot make #876 already covers the wait);JDBCStorage.close()releasing the pool of its connection string;Environment
master (5.2.x), all four JDBC dialects. The line numbers above are of master as it stands; #876 rewrites
getConnectionbut touches none of the cache shape.Noticed while fixing #872 (PR #876).