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/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 162 insertions(+), 0 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
index ce66a7e..135942e 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
@@ -39,8 +39,19 @@
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.PreparedStatement;
+import java.sql.SQLDataException;
import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.sql.SQLInvalidAuthorizationSpecException;
+import java.sql.SQLNonTransientConnectionException;
+import java.sql.SQLNonTransientException;
+import java.sql.SQLRecoverableException;
+import java.sql.SQLSyntaxErrorException;
import java.sql.SQLTimeoutException;
+import java.sql.SQLTransactionRollbackException;
+import java.sql.SQLTransientConnectionException;
+import java.sql.SQLTransientException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
@@ -942,6 +953,157 @@
}
/**
+ * A link that says the connection is gone by its type alone still says so once it is rebuilt
+ * (#1074). The type is what the JDBC contract gives a driver to say it - write() asks it before
+ * the SQLState - so a copy made as a plain SQLException reads as a failure that says nothing of
+ * the connection, and only at the deployment whose url has a password in it.
+ */
+ @Test(timeOut = 60000)
+ public void testALinkThatSaysTheConnectionIsGoneByItsTypeKeepsItThroughRedaction() throws Exception {
+ final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj";
+ for (final SQLException gone : new SQLException[] { new SQLRecoverableException("io error"),
+ new SQLNonTransientConnectionException("closed"), new SQLTransientConnectionException("reset") }) {
+ final SQLException failure = new SQLException("login to " + url + " failed", "S0001", 18456);
+ failure.setNextException(gone);
+
+ final SQLException reported = CachedConnection.reported(failure, url);
+
+ assertNoCredentials(reported);
+ assertTrue(JDBCStorage.isConnectionFailure(reported),
+ "a rebuilt " + gone.getClass().getSimpleName() + " no longer says the connection is gone");
+ }
+ }
+
+ /**
+ * ... and so does one standing past the budget of the rebuild, whatever it says it by: the link
+ * that stands for the rest of the chain there is all that the classification of write() gets to
+ * read of it. The url has no password, and the chain is rebuilt all the same - it is longer than
+ * the walk looking for credentials, which answers "yes" past it.
+ */
+ @Test(timeOut = 60000)
+ public void testALinkThatSaysTheConnectionIsGonePastTheBudgetOfARebuildStillSaysSo() throws Exception {
+ final String url = "jdbc:postgresql://127.0.0.1:5432/opendj";
+ for (final SQLException gone : new SQLException[] { new SQLException("connection reset", "08S01", 10054),
+ new SQLRecoverableException("io error") }) {
+ final SQLException failure = plainChain(40);
+ failure.setNextException(gone);
+
+ final SQLException reported = CachedConnection.reported(failure, url);
+
+ assertTrue(reported != failure, "a chain this long is expected to be rebuilt");
+ assertTrue(JDBCStorage.isConnectionFailure(reported),
+ gone + " past the budget of the rebuild no longer says the connection is gone");
+ }
+ }
+
+ /**
+ * ... and so does one past the budget on any other edge the rebuild cuts: the cause of a link, what
+ * was suppressed on it - where establish() puts the close that failed (#929) - and the cause of a
+ * link that is no SQLException, which is rebuilt on a road of its own. Each chain spends the budget
+ * before the rebuild reaches the edge, so the link that says the connection is gone is behind the
+ * one that stands for the rest; the last one has it on what was suppressed on a link that is cut,
+ * which the rest is read for as well.
+ */
+ @Test(timeOut = 60000)
+ public void testALinkThatSaysTheConnectionIsGoneOnACauseOrASuppressedEdgePastTheBudgetStillSaysSo()
+ throws Exception {
+ final String url = "jdbc:postgresql://127.0.0.1:5432/opendj";
+ final SQLException byCause = plainChain(40);
+ byCause.initCause(new IOException("socket closed", new SQLException("connection reset", "08S01", 10054)));
+ final SQLException bySuppressed = plainChain(40);
+ bySuppressed.addSuppressed(new SQLException("the connection is closed", "08003"));
+ final SQLException byTheCauseOfALinkThatIsNoSQLException = new SQLException("login failed", "S0001", 18456);
+ Throwable wrapper = new SQLException("connection reset", "08S01", 10054);
+ for (int i = 0; i < 40; i++) {
+ wrapper = new IOException("wrapper " + i, wrapper);
+ }
+ byTheCauseOfALinkThatIsNoSQLException.initCause(wrapper);
+ final SQLException byTheSuppressedOfALinkCut = plainChain(40);
+ lastOf(byTheSuppressedOfALinkCut).addSuppressed(new SQLException("the connection is closed", "08003"));
+ final Object[][] cases = {
+ { "the cause", byCause },
+ { "the suppressed", bySuppressed },
+ { "the cause of a link that is no SQLException", byTheCauseOfALinkThatIsNoSQLException },
+ { "what was suppressed on a link cut", byTheSuppressedOfALinkCut } };
+ for (final Object[] edge : cases) {
+ final SQLException failure = (SQLException) edge[1];
+ assertTrue(JDBCStorage.isConnectionFailure(failure), edge[0] + ": the failure itself says the connection is gone");
+
+ final SQLException reported = CachedConnection.reported(failure, url);
+
+ assertNotSame(reported, failure, edge[0] + ": a chain this long is expected to be rebuilt");
+ assertTrue(JDBCStorage.isConnectionFailure(reported),
+ edge[0] + " cut past the budget no longer says the connection is gone");
+ }
+ }
+
+ /**
+ * Every standard type of JDBC a link can be of survives its rebuild, not only the three that say
+ * the connection is gone: each is read somewhere - a SQLTimeoutException is what sends a borrow of
+ * an import to the debug log rather than the warn one (borrowedOrShared()) - and a rebuild that
+ * turned one into its supertype would classify the same failure one way where the url of the
+ * backend has no password and the other where it does. Each type is asked for before its
+ * supertype, so a check out of order fails here as well.
+ */
+ @Test(timeOut = 60000)
+ public void testEveryStandardTypeOfALinkIsKeptThroughRedaction() throws Exception {
+ final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj";
+ final String message = "login to " + url + " failed";
+ for (final SQLException original : new SQLException[] {
+ new SQLRecoverableException(message, "08006", 17002),
+ new SQLNonTransientConnectionException(message, "08001", 0),
+ new SQLTransientConnectionException(message, "08001", 0),
+ new SQLTimeoutException(message, "HYT00", 0),
+ new SQLTransactionRollbackException(message, "40001", 1205),
+ new SQLFeatureNotSupportedException(message, "0A000", 0),
+ new SQLIntegrityConstraintViolationException(message, "23000", 2627),
+ new SQLInvalidAuthorizationSpecException(message, "28000", 18456),
+ new SQLSyntaxErrorException(message, "42000", 0),
+ new SQLDataException(message, "22000", 0),
+ new SQLTransientException(message, "S1000", 0),
+ new SQLNonTransientException(message, "S1000", 0),
+ new SQLException(message, "S1000", 0) }) {
+ final SQLException reported = CachedConnection.reported(original, url);
+
+ assertNoCredentials(reported);
+ assertEquals(reported.getClass(), original.getClass(), "the rebuild changed the type of the link");
+ assertEquals(reported.getSQLState(), original.getSQLState(), "the rebuild changed the SQLState of the link");
+ assertEquals(reported.getErrorCode(), original.getErrorCode(), "the rebuild changed the vendor code of the link");
+ }
+ }
+
+ /**
+ * The link standing for the rest says only what the rest says: a chain that says nothing of the
+ * connection is not made to say it is gone by being cut. That would replay the write and distrust
+ * the pool over a database that refused a connection for a reason of its own.
+ */
+ @Test(timeOut = 60000)
+ public void testALongChainThatSaysNothingOfTheConnectionIsNotMadeToSayItByTheRebuild() throws Exception {
+ final SQLException reported = CachedConnection.reported(plainChain(40), "jdbc:postgresql://127.0.0.1:5432/opendj");
+
+ assertFalse(JDBCStorage.isConnectionFailure(reported),
+ "the rebuild made a failure that says nothing of the connection say it is gone");
+ }
+
+ /** That many plain links of the next exception chain, none of which says the connection is gone. */
+ private static SQLException plainChain(int links) {
+ final SQLException head = new SQLException("error 0", "S0001", 1);
+ for (int i = 1; i < links; i++) {
+ head.setNextException(new SQLException("error " + i, "S0001", 1));
+ }
+ return head;
+ }
+
+ /** The last link of the next exception chain of a failure. */
+ private static SQLException lastOf(SQLException failure) {
+ SQLException last = failure;
+ while (last.getNextException() != null) {
+ last = last.getNextException();
+ }
+ return last;
+ }
+
+ /**
* A credential named by a suppressed link alone is redacted like any other. The close of a
* connection whose set-up failed rides there (establish(), #929) and an interrupt that ended a
* wait for a catalog connect does, and a driver names the url it could not close as readily as
--
Gitblit v1.10.0