From 3fe8fc5280bcb0ced976a698fcae2dc977450ce6 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 28 Sep 2026 07:05:50 +0000
Subject: [PATCH] [#1074] Keep what says the connection is gone through the redaction of a connect failure (#1107)
---
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java | 101 ++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 88 insertions(+), 13 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java
index 814304a..05efb36 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java
@@ -258,7 +258,8 @@
* How many links of the cause and getNextException() chains of a failure the two walks that
* have a reason to stop look at: {@link #holdsCredentials} answers "yes" past it, since the
* cost of the other answer is the password of the backend in the log, and
- * {@link #redactedCopy} rebuilds that far and names the rest in one link. The walk whose verdict
+ * {@link #redactedCopy} rebuilds that far and names the rest in one link - carrying, where the
+ * rest says the connection is gone, what says it (#1074). The walk whose verdict
* decides something, {@link #isWorthRetrying}, is not one of them: a count does not leave that
* question unanswered, it answers it with the verdict of a failure that carries nothing
* (issue #1076), and the visited set of every walk here terminates it on its own.
@@ -2135,8 +2136,11 @@
* prints a failure prints the causes along with it - a debug build of
* stackTraceToSingleLineString walks them, the config manager traces them, and
* RootContainer.open() makes the message of the cause the message of what it throws - so a
- * link left as it stands would carry the password past the wrapper. The SQLState and the
- * vendor code of every link survive it: they are what tells a caller what happened.
+ * link left as it stands would carry the password past the wrapper. The SQLState, the vendor
+ * code and the standard JDBC type of every link survive it: they are what tells a caller what
+ * happened, and JDBCStorage.write() asks the type before the state whether the connection is
+ * gone. Past the budget of the rebuild the one link standing for the rest says that much of it
+ * where the rest says so (#1074).
* <p>
* The whole chain is the causes, the further exceptions of {@code getNextException()} and what
* was suppressed on each of them - the last being where this class puts the failure of a close
@@ -2223,40 +2227,110 @@
private static SQLException redactedCopy(SQLException e, String connectionString, int[] budget) {
budget[0]--;
final SQLException copy =
- new SQLException(redact(e.getMessage(), connectionString), e.getSQLState(), e.getErrorCode());
+ sameKind(e, redact(e.getMessage(), connectionString), e.getSQLState(), e.getErrorCode());
copy.setStackTrace(e.getStackTrace());
if (e.getNextException() != null) {
copy.setNextException(budget[0] > 0
- ? redactedCopy(e.getNextException(), connectionString, budget) : droppedTail());
+ ? redactedCopy(e.getNextException(), connectionString, budget)
+ : droppedTail(Collections.<Throwable>singletonList(e.getNextException())));
}
if (e.getCause() != null) {
- copy.initCause(budget[0] > 0 ? redactedLink(e.getCause(), connectionString, budget) : droppedTail());
+ copy.initCause(budget[0] > 0 ? redactedLink(e.getCause(), connectionString, budget)
+ : droppedTail(Collections.singletonList(e.getCause())));
}
copySuppressed(e, copy, connectionString, budget);
return copy;
}
+ /**
+ * A new exception of the kind JDBC names the given one as. Not the class of the driver itself,
+ * which this cannot be sure of building, but the standard type it extends: that type is what
+ * the JDBC contract gives a driver to say what happened beside the SQLState, and JDBCStorage
+ * reads it first - a SQLRecoverableException says the connection is gone whatever state it
+ * carries, and a copy made as a plain SQLException says nothing of the kind (#1074).
+ */
+ private static SQLException sameKind(SQLException e, String message, String sqlState, int vendorCode) {
+ if (e instanceof SQLRecoverableException) {
+ return new SQLRecoverableException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLNonTransientConnectionException) {
+ return new SQLNonTransientConnectionException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLTransientConnectionException) {
+ return new SQLTransientConnectionException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLTimeoutException) {
+ return new SQLTimeoutException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLTransactionRollbackException) {
+ return new SQLTransactionRollbackException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLFeatureNotSupportedException) {
+ return new SQLFeatureNotSupportedException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLIntegrityConstraintViolationException) {
+ return new SQLIntegrityConstraintViolationException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLInvalidAuthorizationSpecException) {
+ return new SQLInvalidAuthorizationSpecException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLSyntaxErrorException) {
+ return new SQLSyntaxErrorException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLDataException) {
+ return new SQLDataException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLTransientException) {
+ return new SQLTransientException(message, sqlState, vendorCode);
+ }
+ if (e instanceof SQLNonTransientException) {
+ return new SQLNonTransientException(message, sqlState, vendorCode);
+ }
+ return new SQLException(message, sqlState, vendorCode);
+ }
+
// The suppressed links of a failure are rebuilt with the rest of it and counted against the
// same budget. They are not decoration here: the failure of a close that could not be made
// rides on the failure being unwound (establish(), #929), and a rebuild that dropped them
// would lose it at the one deployment whose log is redacted - which is the deployment whose
// url has a password in it, the log that is worth reading.
private static void copySuppressed(Throwable from, Throwable to, String connectionString, int[] budget) {
- for (final Throwable suppressed : from.getSuppressed()) {
+ final Throwable[] suppressed = from.getSuppressed();
+ for (int i = 0; i < suppressed.length; i++) {
if (budget[0] <= 0) {
- to.addSuppressed(droppedTail());
+ to.addSuppressed(droppedTail(Arrays.asList(suppressed).subList(i, suppressed.length)));
return;
}
- to.addSuppressed(redactedLink(suppressed, connectionString, budget));
+ to.addSuppressed(redactedLink(suppressed[i], connectionString, budget));
}
}
// What stands where the budget ran out. Without it the same failure logs its root cause when
// the url of the backend has no password in it and loses it without a word when it has, which
// is a report of a connect nobody can read against a report of one they can.
- private static SQLException droppedTail() {
- return new SQLException("the rest of this failure was left out: a chain of more than "
- + MAX_CHAIN_LENGTH + " links is rebuilt only that far");
+ // It also says what the rest says about the connection, where the rest says it is gone: that
+ // is a verdict JDBCStorage.write() reads off every link of the failure - it replays the attempt
+ // and distrusts the pool on it - and a tail saying nothing would answer it with the "no" of a
+ // failure that carries nothing (#1074, the #961 thesis on the budget of this rebuild). Only
+ // that, and only where the rest does say it, so that the cut makes no failure say it either.
+ // The rest is read by the very walk isConnectionFailure() answers from, to its end, rather than
+ // by a copy of it here: the two would read the same edges today and drift the day one of them
+ // changes. It builds nothing, so what it costs is a walk of the links the driver has already
+ // allocated.
+ private static SQLException droppedTail(List<Throwable> rest) {
+ final String message = "the rest of this failure was left out: a chain of more than "
+ + MAX_CHAIN_LENGTH + " links is rebuilt only that far";
+ SQLException gone = null;
+ for (final Throwable t : rest) {
+ gone = JDBCStorage.connectionFailureLink(t);
+ if (gone != null) {
+ break;
+ }
+ }
+ return gone == null
+ ? new SQLException(message)
+ : sameKind(gone, message + "; a link of it says the connection is gone", gone.getSQLState(),
+ gone.getErrorCode());
}
// A link that is no SQLException keeps its class name in the message: its type is not one this
@@ -2270,7 +2344,8 @@
+ (t.getMessage() == null ? "" : ": " + redact(t.getMessage(), connectionString)));
copy.setStackTrace(t.getStackTrace());
if (t.getCause() != null) {
- copy.initCause(budget[0] > 0 ? redactedLink(t.getCause(), connectionString, budget) : droppedTail());
+ copy.initCause(budget[0] > 0 ? redactedLink(t.getCause(), connectionString, budget)
+ : droppedTail(Collections.singletonList(t.getCause())));
}
copySuppressed(t, copy, connectionString, budget);
return copy;
--
Gitblit v1.10.0