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