Skip to content

[#929] Establish a catalog connection the way the pool establishes its own - #1009

Open
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issues/929-jdbc-shared-connect
Open

[#929] Establish a catalog connection the way the pool establishes its own#1009
vharseko wants to merge 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issues/929-jdbc-shared-connect

Conversation

@vharseko

@vharseko vharseko commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #929.

Problem

JDBCStorage.connectCatalog() was a copy of CachedConnection.connect(): a fresh Properties, dialect.bound(...), DriverManager.getConnection(...), setAutoCommit(false), TRANSACTION_READ_COMMITTED, close-on-failure, and the lift of the read bound of the login. The retry loop around it repeated the pool's own besides.

The copy had already drifted, one commit after it landed. #885 (#934) replaced the lift in connect() with applyStandingReadBound(), which both lifts the bound of the login and puts org.openidentityplatform.opendj.jdbc.read.timeout on the connection for the rest of its life. connectCatalog() was not touched, so the catalog connection was left with the lift alone — nothing bounding the reads between its statements: its commit(), the rollback() of a session given up, the close() of one that lost the race to another thread. That is precisely the gap section 2 of #885 exists to close, and this connection was established beside the pooled ones it was closed for.

What this changes

One place a connect is made. CachedConnection.establish() holds the login and the set-up behind it — the half that is the same wherever this backend connects. It hands back the connection, whether the read bound of the login is off it, and the moment the login was known to be through; the pool wraps that into a CachedConnection, the tree catalog takes the connection as it is.

What is not shared is what becomes of a connection whose read bound would not come off, which is why the warning names both the connection and its fate: the pool closes such a connection rather than pooling it, while the catalog keeps it — this backend has one catalog connection and no borrower behind it to hand another to, and failing there instead would stop a backend opening on a driver whose setNetworkTimeout is not implemented at all, where the pooled connection beside it works.

The catalog connection now carries the standing read bound. The behaviour change of this PR, and the drift above put right: a deployment that asked for read.timeout gets it on this connection as it gets it on every pooled one. A deployment on the default of that property (no bound) is exactly where it was. Its statements were, and stay, bounded by the class of the work they belong to (#877, #882); what is new is the socket bound standing behind everything else on the connection.

The retry loops stay two. Folding them would have to model an iteration that makes no attempt — the pool-full wait, which the catalog has no analogue of — over two deadlines, two exception types and two ways of waiting. What they share is the backoff schedule, now CachedConnection.nextBackoffMs(), and the two deliberate differences are named in the code rather than left to be rediscovered:

  • the pool hands its wait to the poll of its deque, where a peer returning a connection ends it early, while a catalog connect has no peer to wait for and sleeps it out;
  • the pool's timeout carries 08001 where the catalog's carries the driver's own state, since saysTheConnectionIsGone() reads class 08 as a dropped connection and a manufactured one would put a healthy attempt into the replay.

Also here: closeQuietly(con, unwinding) — the variant that reports the failure of the close on the exception being unwound — moved into CachedConnection and replaced the swallowing one, so a driver that will not close is visible on the pooled path too. It cannot change a retry decision: isWorthRetrying() walks the cause and getNextException() chains, not the suppressed ones.

Tests

CatalogConnectionTestCase gains three cases: the connection carries the configured read bound; it carries it whether or not its login had one to lift (connect.timeout=0); and a read bound standing in the connection string is not replaced by it, which is a guard - the value comes from standingReadBoundMillis(), so a bound put on from the property alone would pass every other case of the class. The first two fail on the head of this branch, the third passes there.

CachedConnectionTestCase gains one for the shared backoff schedule: a millisecond, doubling to the ceiling and staying there.

Run locally, one JVM per class: CatalogConnectionTestCase 16, CachedConnectionTestCase 100, JDBCStatementBoundTestCase 44, JDBCStorageRetryTest 67, StampConnectionTestCase 5 — all green. The container suites of the four dialects are left to CI.

Version

master (5.2.x), all four JDBC dialects.

… pool establishes its own

CachedConnection.establish() is now the one place a connect of this backend is
made: the properties the driver is handed, the login, the transaction the rows
of this backend need, and the read bound the connection carries afterwards. The
pool wraps what it returns, the tree catalog takes the connection as it is, and
what becomes of one whose read bound would not come off is the caller's - the
pool closes it rather than pooling it, the catalog keeps it, having no second
connection to fall back to.

Written out twice, that half had already drifted: OpenIdentityPlatform#885 gave the standing read
bound to the pooled connect alone, leaving the catalog connection with the lift
and nothing bounding the reads between its statements - its commit(), the
rollback() of a session given up, the close() of one that lost the race. It now
carries org.openidentityplatform.opendj.jdbc.read.timeout as a pooled connection
does; a deployment on the default of that property is where it was.

The two retry loops stay two: the pool hands its wait to the poll of its deque,
where a peer returning a connection ends it early, while a catalog connect has
no peer to wait for and sleeps it out, and the timeout each raises differs on
purpose - 08001 against the driver's own state, which write() must not read as a
connection the database dropped. What they share is the backoff schedule, now
CachedConnection.nextBackoffMs(), and both differences are named in the code.
@vharseko
vharseko requested a review from maximthomas September 9, 2026 17:14
@vharseko vharseko added enhancement jdbc tests Test suites: fixing, enabling, un-disabling labels Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement jdbc tests Test suites: fixing, enabling, un-disabling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JDBC backend: the catalog connect duplicates the pool's retry loop, so the two carry one policy in two files

1 participant