/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems, LLC. */ package org.opends.server.backends.jdbc; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.server.config.server.JDBCBackendCfg; import org.opends.server.DirectoryServerTestCase; import org.opends.server.backends.pluggable.spi.TreeName; import org.opends.server.backends.pluggable.spi.AccessMode; import org.opends.server.backends.pluggable.spi.Importer; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.ServerSocket; import java.sql.Connection; import java.sql.Driver; 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; import java.util.IdentityHashMap; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import org.mockito.InOrder; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /** * The pool every operation of the JDBC backend borrows from must bound both of its phases and * report a connect it cannot make, rather than retrying it out of sight of the caller (#872). * Needs no database: the dialects are exercised against a socket that never answers and against a * driver of this test, so a regression fails the build wherever it runs. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "jdbc" }, sequential = true) public class CachedConnectionTestCase extends DirectoryServerTestCase { /** A connect attempt of a bounded dialect must give up in about this long, plus room for a slow machine. */ private static final long BOUND_SECONDS = 2; /** * Room for a slow machine on top of a bound, and no more than that. A minute of it turned * every assertion below into "it does not run forever": a connect that has to be reported at * once and a borrow that has to give up at its two second deadline both passed at 59 s. */ private static final long BOUND_MARGIN_MS = 10000; private final StubDriver stub = new StubDriver(); /** The window as this JVM was started with it, put back after every test that varies it. */ private static final long CONFIGURED_ALIVE_BYPASS_NANOS = CachedConnection.aliveBypassNanos; /** The standing read bound as this JVM was started with it, put back after every test that varies it. */ private static final int CONFIGURED_READ_TIMEOUT_MILLIS = CachedConnection.readTimeoutMillis; @BeforeClass public void registerStubDriver() throws Exception { DriverManager.registerDriver(stub); } @AfterClass public void deregisterStubDriver() throws Exception { DriverManager.deregisterDriver(stub); } /** * Most of the tests below seed the pool by hand, and a connection built a moment ago is inside * the alive window - they are about what the validation of a borrow does, so the window is off * unless the test at hand is one of the window's own. */ @BeforeMethod public void validateEveryBorrow() { CachedConnection.aliveBypassNanos = 0; } @AfterMethod public void clearProperties() { System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY); System.clearProperty(CachedConnection.POOL_TIMEOUT_PROPERTY); System.clearProperty(CachedConnection.POOL_MAX_PROPERTY); System.clearProperty(CachedConnection.TTL_PROPERTY); System.clearProperty(CachedConnection.ALIVE_BYPASS_PROPERTY); System.clearProperty(CachedConnection.READ_TIMEOUT_PROPERTY); // what has been reported once is remembered for the life of the jvm: left standing, the key // of one test is what the next one finds when it asserts that it reported something itself CachedConnection.warnedOnce.clear(); CachedConnection.aliveBypassNanos = CONFIGURED_ALIVE_BYPASS_NANOS; CachedConnection.readTimeoutMillis = CONFIGURED_READ_TIMEOUT_MILLIS; } /** * Nothing used to limit how many connections a backend opened: the pool was an unbounded queue * behind a cache with no maximum size, so a burst of concurrent operations opened as many * connections as there were threads asking, and the only ceiling left was the max_connections of * the database itself (#878). */ @Test(timeOut = 120000) public void testThePoolDoesNotGrowPastItsBound() throws Exception { final String url = StubDriver.PREFIX + "bounded"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "2"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); stub.answerWith(null); // One thread per borrow, as the worker threads of the server are: two borrows on one thread // are nested by definition, and a nested one is allowed past the bound on purpose. final Connection first = borrowOnAThreadOfItsOwn(url); final Connection second = borrowOnAThreadOfItsOwn(url); assertEquals(CachedConnection.poolOf(url).meteredCount(), 2); try { borrowOnAThreadOfItsOwn(url); fail("a third connection was opened past the bound of two"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof SQLTimeoutException, String.valueOf(e.getCause())); assertTrue(e.getCause().getMessage().contains("all 2 connections"), e.getCause().getMessage()); } // The bound waits for a returned connection rather than refusing outright: it is a ceiling // on the connections held, not on the operations served. first.close(); final Connection third = borrowOnAThreadOfItsOwn(url); assertSame(third, first); third.close(); second.close(); CachedConnection.poolOf(url).drainIdle(); } /** Borrows the way the server does, one operation to a thread. */ private static Connection borrowOnAThreadOfItsOwn(String url) throws Exception { return startBorrow(url).get(120, TimeUnit.SECONDS); } /** The same, left running: a borrow that waits has to be looked at while it does. */ private static FutureTask startBorrow(String url) { final FutureTask borrow = new FutureTask<>(() -> CachedConnection.getConnection(url)); final Thread thread = new Thread(borrow, "borrow-" + url); thread.setDaemon(true); thread.start(); return borrow; } /** * A borrow made while this thread already holds a connection must not wait for the bound: the * two are held at once, so it would wait for itself. PersistentCompressedSchema.store() opens a * write of its own and is reached from inside a transaction by EntryContainer.importEntry and * EntryContainer.modifyDN, both of which encode the entry inside it. */ @Test(timeOut = 120000) public void testABorrowNestedInAnotherMayPassTheBound() throws Exception { final String url = StubDriver.PREFIX + "reentrant"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "1"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); stub.answerWith(null); final Connection outer = CachedConnection.getConnection(url); final Connection nested = CachedConnection.getConnection(url); assertNotSame(nested, outer); // It holds no permit of the pool, so pooling it would leave the pool one connection over // its bound for good: it is closed instead. nested.close(); verify(((CachedConnection) nested).parent).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 0); outer.close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1); CachedConnection.poolOf(url).drainIdle(); } /** * The TTL used to sit on the pool rather than on a connection - keyed by the connection string, * and touched by every borrow and every return - so under continuous traffic nothing in it ever * expired (#878). */ @Test(timeOut = 120000) public void testAnIdleConnectionIsClosedAfterItsTtl() throws Exception { final String url = StubDriver.PREFIX + "ttl"; stub.answerWith(null); final CachedConnection first = (CachedConnection) CachedConnection.getConnection(url); first.close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1); first.returnedAtMillis = System.currentTimeMillis() - 60000; System.setProperty(CachedConnection.TTL_PROPERTY, "1000"); final Connection second = CachedConnection.getConnection(url); assertNotSame(second, first, "a connection idle far longer than the TTL was handed out"); verify(first.parent).close(); second.close(); CachedConnection.poolOf(url).drainIdle(); } /** * Expiry has to happen without a borrow behind it: the cache was built without a scheduler, so * an entry was only ever expired by a later cache operation - and a backend that has gone idle, * the one case the TTL exists for, performs none (#878). This is what the sweeper thread runs. */ @Test(timeOut = 120000) public void testTheSweepClosesAnIdleConnectionWithNoBorrowBehindIt() throws Exception { final String url = StubDriver.PREFIX + "sweep"; stub.answerWith(null); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); con.close(); // The sweeper of the server is running while this case does, over every pool and reading // the TTL as it goes: out of its reach, so that the sweep asserted here is the one below. System.setProperty(CachedConnection.TTL_PROPERTY, "600000"); con.returnedAtMillis = System.currentTimeMillis() - 60000; final CachedConnection.Pool pool = CachedConnection.poolOf(url); assertEquals(pool.idleCount(), 1); pool.sweep(1000); assertEquals(pool.idleCount(), 0); verify(con.parent).close(); assertEquals(pool.meteredCount(), 0, "a swept connection kept its place in the pool"); } /** A closed backend has no use for its connections; they used to be left open (#878). */ @Test(timeOut = 120000) public void testClosingTheLastUserReleasesTheConnections() throws Exception { final String url = StubDriver.PREFIX + "release"; stub.answerWith(null); CachedConnection.openPool(url); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); con.close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1); CachedConnection.closePool(url); assertEquals(CachedConnection.poolOf(url).idleCount(), 0); verify(con.parent).close(); assertEquals(CachedConnection.poolOf(url).meteredCount(), 0); } /** * A pool belongs to a database rather than to a backend: two backends may address one database, * and closing one of them must not take the connections of the other with it. */ @Test(timeOut = 120000) public void testConnectionsSurviveWhileAnotherBackendStillUsesTheDatabase() throws Exception { final String url = StubDriver.PREFIX + "shared"; stub.answerWith(null); CachedConnection.openPool(url); CachedConnection.openPool(url); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); con.close(); CachedConnection.closePool(url); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "the second backend lost its connections"); CachedConnection.closePool(url); assertEquals(CachedConnection.poolOf(url).idleCount(), 0); verify(con.parent).close(); } /** A connection out on loan when the last backend closed is closed when it comes back. */ @Test(timeOut = 120000) public void testAConnectionReturnedAfterTheLastUserLeftIsClosed() throws Exception { final String url = StubDriver.PREFIX + "return-after-close"; stub.answerWith(null); CachedConnection.openPool(url); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); CachedConnection.closePool(url); con.close(); verify(con.parent).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 0); } /** A backend closed and opened again pools its connections as before: addUser() clears the flag. */ @Test(timeOut = 120000) public void testABackendClosedAndOpenedAgainPoolsItsConnections() throws Exception { final String url = StubDriver.PREFIX + "reopen"; stub.answerWith(null); CachedConnection.openPool(url); CachedConnection.getConnection(url).close(); CachedConnection.closePool(url); assertEquals(CachedConnection.poolOf(url).idleCount(), 0); CachedConnection.openPool(url); CachedConnection.getConnection(url).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "a reopened backend stopped pooling its connections"); CachedConnection.closePool(url); } /** * A connect that fails with something other than a SQLException must not cost the pool a * permit. DriverManager catches SQLException alone, so an unchecked failure of a driver reaches * the borrow: Connector/J hands a url with a "%" in it to URLDecoder, and this backend keeps * its credentials in the url. Only a live connection carries a permit, so one left behind is * left behind for good - after as many failures as the bound the pool would report that every * connection is in use while holding none (#878). */ @Test(timeOut = 120000) public void testAConnectFailingUncheckedCostsThePoolNothing() throws Exception { final String url = StubDriver.PREFIX + "unchecked"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "2"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); final CachedConnection.Pool pool = CachedConnection.poolOf(url); stub.failWith(new IllegalArgumentException("URLDecoder: Illegal hex characters in escape (%) pattern"), StubDriver.ALWAYS); for (int i = 1; i <= 2 * pool.max(); i++) { try { CachedConnection.getConnection(url); fail("the connect did not fail"); } catch (IllegalArgumentException expected) { // reported to the caller, as a configuration error has to be } assertEquals(pool.meteredCount(), 0, "attempt " + i + " kept a permit of the pool"); } // and the pool still serves, rather than reporting connections it does not hold as in use stub.answerWith(null); final Connection con = CachedConnection.getConnection(url); assertNotNull(con); con.close(); CachedConnection.poolOf(url).drainIdle(); } /** * The exemption of a nested borrow belongs to one pool: a thread holding a connection to one * database holds nothing of another, so the bound of that other pool applies and its connection * comes back to it rather than being closed. */ @Test(timeOut = 120000) public void testHoldingAConnectionToOneDatabaseDoesNotExemptABorrowFromAnother() throws Exception { final String first = StubDriver.PREFIX + "held-first"; final String second = StubDriver.PREFIX + "held-second"; stub.answerWith(null); final Connection held = CachedConnection.getConnection(first); final Connection other = CachedConnection.getConnection(second); assertEquals(CachedConnection.poolOf(second).meteredCount(), 1, "the borrow passed the bound of the other pool"); other.close(); assertEquals(CachedConnection.poolOf(second).idleCount(), 1, "the borrow was taken for a nested one and closed"); held.close(); CachedConnection.poolOf(first).drainIdle(); CachedConnection.poolOf(second).drainIdle(); } /** * A connection returned on a thread other than the one that borrowed it still lowers the depth * of the borrower. The depth used to be lowered only where the returning thread was the * borrowing one, and nulled either way, so a cross-thread return left the borrower standing at a * depth it could never come down from: that thread was taken for a nested borrow for the life of * the server, exempt from the wait at the bound, and every operation on it opened an unmetered * connection that the return then closed - a physical connect apiece, past a bound the operator * set (#878). */ @Test(timeOut = 120000) public void testAReturnOnAnotherThreadLowersTheDepthOfTheBorrower() throws Exception { final String url = StubDriver.PREFIX + "cross-thread-return"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "1"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); stub.answerWith(null); final CachedConnection.Pool pool = CachedConnection.poolOf(url); final ExecutorService borrower = Executors.newSingleThreadExecutor(runnable -> { final Thread thread = new Thread(runnable, "cross-thread-borrower"); thread.setDaemon(true); return thread; }); try { // borrowed there, returned here final Connection borrowed = borrower.submit(() -> CachedConnection.getConnection(url)) .get(120, TimeUnit.SECONDS); borrowed.close(); assertEquals(pool.idleCount(), 1, "the connection was not pooled by the return"); // the one place of the pool goes to somebody else, so the borrower thread has to wait // for it - and, having no connection of its own any more, has to give up when it does // not come final Connection held = borrowOnAThreadOfItsOwn(url); try { borrower.submit(() -> CachedConnection.getConnection(url)).get(120, TimeUnit.SECONDS); fail("the borrower thread was taken for a nested borrow and passed the bound of the pool"); } catch (ExecutionException expected) { assertTrue(expected.getCause() instanceof SQLTimeoutException, "the bound was passed rather than waited out: " + expected.getCause()); } held.close(); } finally { borrower.shutdownNow(); } CachedConnection.poolOf(url).drainIdle(); } /** JDBC makes close() on a closed connection a no-op; a second return would pool the same one twice. */ @Test(timeOut = 120000) public void testASecondCloseDoesNotPoolTheConnectionTwice() throws Exception { final String url = StubDriver.PREFIX + "double-close"; stub.answerWith(null); final Connection con = CachedConnection.getConnection(url); con.close(); con.close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "one connection was pooled twice"); CachedConnection.poolOf(url).drainIdle(); } /** * What the sweeper runs hands the close elsewhere instead of running it. The sweep of every * pool shares one thread and scheduleWithFixedDelay never overlaps its runs, so one close that * does not return would stop the expiry of every pool in the JVM, silently (#878). */ @Test(timeOut = 120000) public void testTheSweepDoesNotCloseOnTheSweeperThread() throws Exception { final String url = StubDriver.PREFIX + "sweep-elsewhere"; stub.answerWith(null); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); con.close(); System.setProperty(CachedConnection.TTL_PROPERTY, "600000"); // see the case above con.returnedAtMillis = System.currentTimeMillis() - 60000; final CachedConnection.Pool pool = CachedConnection.poolOf(url); final List handedOff = new ArrayList<>(); pool.sweep(1000, handedOff::add); assertEquals(pool.idleCount(), 0, "the expired connection kept its place in the pool"); verify(con.parent, never()).close(); assertEquals(handedOff.size(), 1); handedOff.get(0).run(); verify(con.parent).close(); assertEquals(pool.meteredCount(), 0, "a swept connection kept its permit"); } /** * And the sweep the scheduled sweeper actually runs closes elsewhere too: the case above * supplies an executor of its own, so it would pass just as well with the production one left * closing inline. */ @Test(timeOut = 120000) public void testTheScheduledSweepClosesOnAThreadOfItsOwn() throws Exception { final String url = StubDriver.PREFIX + "sweeper-thread"; stub.answerWith(null); final CachedConnection con = (CachedConnection) CachedConnection.getConnection(url); con.close(); final AtomicReference closedOn = new AtomicReference<>(); doAnswer(invocation -> { closedOn.set(Thread.currentThread().getName()); return null; }).when(con.parent).close(); con.returnedAtMillis = System.currentTimeMillis() - 60000; System.setProperty(CachedConnection.TTL_PROPERTY, "1000"); CachedConnection.sweep(); // what the scheduled sweeper runs, with nothing supplied to it for (int i = 0; i < 200 && closedOn.get() == null; i++) { Thread.sleep(50); } assertNotNull(closedOn.get(), "the sweep never closed the expired connection"); assertFalse(closedOn.get().contains("sweeper"), "the close ran on the sweeper thread: " + closedOn.get()); assertTrue(closedOn.get().startsWith("JDBC backend connection pool closer"), closedOn.get()); } /** * A borrow may not outlast the deadline it was given while emptying the pool. A poll of no * duration still hands out whatever the deque holds, and a connection whose socket is half-open * - a moved VIP, a firewall that dropped the idle sockets - costs the validation timeout to * discard, so draining a pool of its full bound overran the deadline by minutes, before the * connect that follows it had even started (#878). */ @Test(timeOut = 120000) public void testABorrowStopsAtItsDeadlineRatherThanDrainingThePool() throws Exception { final String url = StubDriver.PREFIX + "deadline-drain"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "6"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); final CachedConnection.Pool pool = CachedConnection.poolOf(url); // Fresh by the TTL, and each one a second to find broken: the pool a burst of traffic left // behind, against a database that has stopped answering. for (int i = 0; i < 6; i++) { final Connection halfOpen = mock(Connection.class); when(halfOpen.isValid(anyInt())).thenAnswer(invocation -> { Thread.sleep(1000); return false; }); assertTrue(pool.tryReserve()); pool.addIdle(new CachedConnection(url, halfOpen, pool, true, true)); } stub.answerWith(null); final long startedAt = System.currentTimeMillis(); final Connection borrowed = CachedConnection.getConnection(url); final long elapsed = System.currentTimeMillis() - startedAt; assertNotNull(borrowed); assertTrue(elapsed < 3500, "the borrow drained the pool past its deadline: " + elapsed + " ms"); borrowed.close(); CachedConnection.poolOf(url).drainIdle(); } /** 0 means "no bound" for the size of the pool, and an invalid value means "the default". */ @Test(timeOut = 120000) public void testTheBoundOfThePoolReadsItsBoundaryValues() throws Exception { assertEquals(poolWithMax("unbounded", "0").max(), Integer.MAX_VALUE, "0 must mean no bound"); assertEquals(poolWithMax("negative", "-1").max(), CachedConnection.DEFAULT_POOL_MAX); assertEquals(poolWithMax("not-a-number", "sixteen").max(), CachedConnection.DEFAULT_POOL_MAX); } private static CachedConnection.Pool poolWithMax(String name, String max) { System.setProperty(CachedConnection.POOL_MAX_PROPERTY, max); return CachedConnection.poolOf(StubDriver.PREFIX + "bound-" + name); // read when the pool is built } /** 0 means "wait without limit" for a borrow, rather than "give up at once". */ @Test(timeOut = 120000) public void testABorrowWithNoDeadlineWaitsForAReturnedConnection() throws Exception { final String url = StubDriver.PREFIX + "no-deadline"; System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "1"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0"); stub.answerWith(null); final Connection held = borrowOnAThreadOfItsOwn(url); final FutureTask waiting = startBorrow(url); try { waiting.get(1500, TimeUnit.MILLISECONDS); fail("the borrow gave up although it was given no deadline"); } catch (TimeoutException expected) { // still waiting for the connection of the pool to come back, which is the point } held.close(); final Connection served = waiting.get(120, TimeUnit.SECONDS); assertSame(served, held, "the borrow was served by something other than the returned connection"); served.close(); CachedConnection.poolOf(url).drainIdle(); } /** 0 means "keep nothing" for the TTL: an idle connection is not handed out again. */ @Test(timeOut = 120000) public void testAZeroTtlKeepsNoIdleConnection() throws Exception { final String url = StubDriver.PREFIX + "zero-ttl"; stub.answerWith(null); final CachedConnection first = (CachedConnection) CachedConnection.getConnection(url); first.close(); first.returnedAtMillis = System.currentTimeMillis() - 5; System.setProperty(CachedConnection.TTL_PROPERTY, "0"); final Connection second = CachedConnection.getConnection(url); assertNotSame(second, first, "a connection was kept although the TTL keeps none"); verify(first.parent).close(); second.close(); CachedConnection.poolOf(url).drainIdle(); } /** * The storage borrows from the pool it registered with, and gives that registration back when * it closes. db-directory may be changed on a running backend - applyConfigurationChange takes * it and nothing refuses it - and a borrow that followed the change would leave the pool this * storage registered with holding a user that never borrows, while the pool it borrowed from * has none: the leak of #878 back through the configuration, and a pool another backend may * drain while this one is still borrowing from it. */ @Test(timeOut = 120000) public void testTheStorageBorrowsFromThePoolItRegisteredWith() throws Exception { final String registered = StubDriver.PREFIX + "storage-registered"; final String changed = StubDriver.PREFIX + "storage-changed"; stub.answerWith(null); final JDBCBackendCfg cfg = mock(JDBCBackendCfg.class); when(cfg.getDBDirectory()).thenReturn(registered); final JDBCStorage storage = new JDBCStorage(cfg, null); storage.open(AccessMode.READ_WRITE); when(cfg.getDBDirectory()).thenReturn(changed); // the configuration changed under it try (final Connection con = storage.getConnection()) { assertEquals(((CachedConnection) con).connectionString, registered, "the borrow left the pool this storage registered with"); } assertEquals(CachedConnection.poolOf(changed).meteredCount(), 0, "a pool with no user was borrowed from"); storage.close(); assertEquals(CachedConnection.poolOf(registered).idleCount(), 0, "close() left the connections of the pool it registered with behind"); } /** * An open that failed has to leave the storage saying so. The status used to be set inside the * try-with-resources of the validating borrow, so a throw from the implicit close() - the return * rolls back, and the rollback goes to the database - left the storage at working() while open() * failed and gave the registration of the pool back. write() and ImporterImpl both skip the * re-open when the status says working, so the pool was left with no user at all: every * connection returned to it destroyed on the spot, pooling off for that database for as long as * the server runs (#878). */ @Test(timeOut = 120000) public void testAnOpenThatFailsOnTheReturnLeavesTheStorageClosed() throws Exception { final String url = StubDriver.PREFIX + "open-return-failure"; final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); doThrow(new SQLException("the socket went away")).when(parent).rollback(); stub.answerWith(parent); final JDBCBackendCfg cfg = mock(JDBCBackendCfg.class); when(cfg.getDBDirectory()).thenReturn(url); final JDBCStorage storage = new JDBCStorage(cfg, null); try { storage.open(AccessMode.READ_WRITE); fail("a validated borrow that could not be returned must be reported"); } catch (SQLException expected) { assertEquals(expected.getMessage(), "the socket went away"); } assertFalse(storage.getStorageStatus().isWorking(), "an open that failed left the storage reporting working"); // and the open that follows is not skipped: it registers with the pool again, which is what // makes the connections returned to it pooled rather than destroyed on the spot doNothing().when(parent).rollback(); storage.open(AccessMode.READ_WRITE); assertTrue(storage.getStorageStatus().isWorking(), "the storage did not reopen"); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "the reopened storage stopped pooling its connections"); storage.close(); } /** * An import gives its connection back however its commit went. The commit used to be guarded * against SQLException alone, so an Error out of a bulk import - or a driver failing unchecked * - left the connection borrowed and its permit with it; a pool is never removed from the map, * so that permit was gone for the life of the server and enough imports walked the bound of * the pool down to nothing (#878). */ @Test(timeOut = 120000) public void testAnImportGivesItsConnectionBackWhenTheCommitFailsUnchecked() throws Exception { final String url = StubDriver.PREFIX + "import-unchecked-commit"; final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); // the statement of the write below, which is what gives the commit of close() something to do: // a connection an import wrote nothing through is not committed at all (#891), so an import // that writes nothing would reach neither the Error this test injects nor the return it is about when(parent.prepareStatement(anyString())).thenReturn(mock(PreparedStatement.class)); doThrow(new Error("out of memory while importing")).when(parent).commit(); stub.answerWith(parent); final JDBCBackendCfg cfg = mock(JDBCBackendCfg.class); when(cfg.getDBDirectory()).thenReturn(url); final JDBCStorage storage = new JDBCStorage(cfg, null); storage.open(AccessMode.READ_WRITE); final Importer importer = storage.startImport(); importer.put(new TreeName("dc=example,dc=com", "id2entry"), ByteString.valueOfUtf8("key"), ByteString.valueOfUtf8("value")); try { importer.close(); fail("the failure of the commit was not reported"); } catch (Error expected) { // reported to the caller, which is what an Error out of an import has to be. Asserted // rather than accepted whole: an AssertionError of the fail() above is an Error too, and // would otherwise be caught here and read as the injected one assertEquals(expected.getMessage(), "out of memory while importing"); } assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "the import kept the connection of the pool"); storage.close(); assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "the import kept a permit of the pool"); } /** * A driver that is not on the classpath - the JDBC backend needs one dropped into * lib/extensions by hand - is a configuration error the caller has to see. Retried, it is * indistinguishable from a database that hangs. */ @Test(timeOut = 120000) public void testMissingDriverIsReportedAtOnce() throws Exception { final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection("jdbc:nosuchengine://127.0.0.1:5432/opendj"); fail("a connection string no registered driver accepts must be reported"); } catch (SQLException expected) { assertTrue(expected.getMessage().contains("No suitable driver"), expected.getMessage()); } assertElapsedWithinBound(startedAt, 0); } /** * ... and the report of it carries no password. This is the path the credentials leave by: the * jdk itself builds "No suitable driver found for " + url, a missing driver jar is the ordinary * oracle misconfiguration, and what this class throws reaches the server error log in full - * JDBCStorage.open() hands it to RootContainer, which makes the message of the cause the * message of what it throws, and BackendConfigManager logs that at ERROR and answers a config * change with it. Every link of the chain is asserted, not only the message on top: everything * that prints a failure prints its causes along with it. */ @Test(timeOut = 120000) public void testAReportedConnectCarriesNoCredentials() throws Exception { final String url = "jdbc:nosuchengine://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; try { CachedConnection.getConnection(url); fail("a connection string no registered driver accepts must be reported"); } catch (SQLException expected) { assertNoCredentials(expected); assertTrue(expected.getMessage().contains("No suitable driver"), "the failure has to stay recognizable: " + expected.getMessage()); assertTrue(expected.getMessage().contains("127.0.0.1:5432"), "the host is what a report of a connect is read for: " + expected.getMessage()); } } /** * A url this class knows no timeout properties for is reported once. The properties bounding a * connect are the ones of a driver, so a driver outside the four - an admin-added mariadb, an * h2 - leaves every attempt unbounded, and the deadline of the borrow cannot reach into a * connect already under way: the driver is the only thing holding the socket. Silently, that * is #872 again, for a backend nobody thinks of as unbounded. */ @Test(timeOut = 120000) public void testAUrlThisBackendCannotBoundIsReportedOnce() throws Exception { final String url = "jdbc:nosuchengine-unbounded://127.0.0.1:5432/opendj"; final String key = CachedConnection.safeUrl(url) + "|unknown-dialect"; // the set is the gate warnOnce() logs behind, so it has to start empty for this to be a // test of what this borrow reported rather than of what some earlier one left behind CachedConnection.warnedOnce.clear(); borrowExpectingFailure(url); assertEquals(CachedConnection.warnedOnce, Collections.singleton(key), "a connection string no bound of this class can reach was not reported"); // ... and once: every operation of the backend borrows through here, so a report per borrow // is one nobody reads. A second borrow that would log again is one that adds a key again. CachedConnection.warnedOnce.remove(key); borrowExpectingFailure(url); assertEquals(CachedConnection.warnedOnce, Collections.singleton(key), "the report is not the one warnOnce() gates: " + CachedConnection.warnedOnce); borrowExpectingFailure(url); assertEquals(CachedConnection.warnedOnce, Collections.singleton(key), "the same url was reported a second time"); } /** A borrow that has to fail: what the test is after is what was logged on the way. */ private static void borrowExpectingFailure(String url) throws Exception { try { CachedConnection.getConnection(url); fail("a connection string no registered driver accepts must be reported"); } catch (SQLException expected) { // the point of the test is what was logged on the way, not what came back } } /** * ... and so is a postgresql url that turns the read bound off: a parameter of one outranks the * property this class supplies, so a "socketTimeout=0" there cannot be replaced. Nothing is * left to end a borrow that reaches a database accepting the connection and answering nothing, * and an administrator who wrote that zero has to be able to find it in the log. */ @Test public void testAReadBoundTurnedOffInAPostgresUrlIsReportedOnce() throws Exception { final String url = "jdbc:postgresql://reported:5432/db?socketTimeout=0"; final String key = CachedConnection.safeUrl(url) + "|unbounded|socketTimeout"; CachedConnection.warnedOnce.clear(); assertFalse(CachedConnection.ConnectDialect.POSTGRES.bound(url, new Properties(), 7)); assertEquals(CachedConnection.warnedOnce, Collections.singleton(key), "a url leaving the reads of its login unbounded was not reported"); assertFalse(CachedConnection.ConnectDialect.POSTGRES.bound(url, new Properties(), 7)); assertEquals(CachedConnection.warnedOnce, Collections.singleton(key), "the same url was reported a second time"); } /** * ... and every parameter of it that is turned off, not only the first one. A url is free to * turn off the read bound and the login bound both, and the login bound is the per-host budget * of a failover url - safeUrl() keeps neither parameter, so a key of the url alone would name * the first offender, remember the url as reported, and leave the rest of them unmentionable. */ @Test public void testEveryBoundTurnedOffInAPostgresUrlIsReported() throws Exception { final String url = "jdbc:postgresql://reported-twice:5432/db?socketTimeout=0&loginTimeout=0"; final String safe = CachedConnection.safeUrl(url); CachedConnection.warnedOnce.clear(); CachedConnection.ConnectDialect.POSTGRES.bound(url, new Properties(), 7); assertTrue(CachedConnection.warnedOnce.contains(safe + "|unbounded|socketTimeout"), "the read bound left at 0 was not reported: " + CachedConnection.warnedOnce); assertTrue(CachedConnection.warnedOnce.contains(safe + "|unbounded|loginTimeout"), "the login bound left at 0 was not reported: " + CachedConnection.warnedOnce); } /** * Nothing in the chain of a failure names the password of the backend, however deep it stands. * Walked by identity rather than link by link: a driver is free to make the cause and the next * exception of a link the same failure, which is the very shape the sibling test builds, and a * helper looping on it would hang the run it is checking for exactly that. *

* The suppressed links along with the rest: the close of a connection whose set-up failed is * carried there (#929), and everything that prints a failure prints those too. */ private static void assertNoCredentials(Throwable failure) { final Set seen = Collections.newSetFromMap(new IdentityHashMap()); final Deque pending = new ArrayDeque<>(); enqueue(pending, seen, failure); while (!pending.isEmpty()) { final Throwable t = pending.poll(); assertFalse(String.valueOf(t.getMessage()).contains("S3cretOfTheBackend"), "the password of the backend reached a message: " + t); if (t instanceof SQLException) { enqueue(pending, seen, ((SQLException) t).getNextException()); } enqueue(pending, seen, t.getCause()); for (final Throwable suppressed : t.getSuppressed()) { enqueue(pending, seen, suppressed); } } } private static void enqueue(Deque pending, Set seen, Throwable t) { if (t != null && seen.add(t)) { pending.add(t); } } /** * A link of a chain carries a cause and a next exception both, and a driver is free to make the * two the same failure. Rebuilt under a bound on the depth alone, a chain of those is copied * twice over at every step - 2^32 links for one reaching the bound, which is a report of a * failed connect that never comes back. What bounds the redaction is the number of links it * rebuilds, the way the walk looking for credentials is bounded by the ones it visits. */ @Test(timeOut = 60000) public void testAFailureWhoseCauseIsItsNextExceptionIsRedactedInBoundedTime() throws Exception { final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; SQLException chain = new SQLException("connect to " + url + " failed", "08006", 1); for (int i = 0; i < 64; i++) { final SQLException link = new SQLException("link " + i + " of " + url, "08006", i); link.setNextException(chain); link.initCause(chain); chain = link; } final SQLException reported = CachedConnection.reported(chain, url); assertNoCredentials(reported); assertEquals(reported.getSQLState(), "08006", "the SQLState of a link has to survive its redaction"); } /** * ... and the chain of one is walked link by link rather than copy by copy. Enqueued twice, a * failure whose cause is its own next exception fans out into a level twice the size of the one * above it, so five levels of it are enough to spend the whole budget of the walk: the link * that names the url stands at level six of seven and was never reached - and a walk that ends * without finding credentials is one that reports the failure as it stands, password and all. */ @Test(timeOut = 60000) public void testACredentialBehindADuplicatedChainIsStillRedacted() throws Exception { final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; SQLException chain = new SQLException("connect to " + url + " failed", "08006", 1); for (int i = 0; i < 6; i++) { final SQLException link = new SQLException("wrapper " + i, "08006", i); link.setNextException(chain); link.initCause(chain); chain = link; } assertNoCredentials(CachedConnection.reported(chain, url)); } /** * The tail a rebuild has no budget left for is named rather than dropped. Without it the same * failure keeps its root cause where the url of the backend carries no password and loses it * without a word where it does - and the root cause is what a report of a failed connect is * read for. */ @Test(timeOut = 60000) public void testTheTailOfALongChainIsNamedRatherThanDropped() throws Exception { final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; SQLException chain = new SQLException("Connection to 127.0.0.1:5432 refused", "08006", 1); for (int i = 0; i < 40; i++) { final SQLException link = new SQLException("wrapper " + i + " of " + url, "08006", i); link.initCause(chain); chain = link; } final SQLException reported = CachedConnection.reported(chain, url); assertNoCredentials(reported); Throwable last = reported; while (last.getCause() != null) { last = last.getCause(); } assertTrue(String.valueOf(last.getMessage()).contains("left out"), "the tail a rebuild had no budget for has to say so: " + last.getMessage()); } /** * 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 * the one it could not open. Left out of the walk, such a link is the one way the password of * this backend reaches the server error log as it stands: the failure it hangs on says nothing * of the url, so the failure is handed on unredacted, suppressed link and all. */ @Test(timeOut = 60000) public void testACredentialNamedOnlyBySuppressedIsStillRedacted() throws Exception { final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; final SQLException setupFailure = new SQLException("Connection to 127.0.0.1:5432 refused", "08006", 1); setupFailure.addSuppressed(new SQLException("could not close " + url, "08003", 2)); assertNoCredentials(CachedConnection.reported(setupFailure, url)); } /** * ... and the link itself survives the rebuild rather than being dropped along with the * password. A redacted failure is the only failure the deployment with a password in its url * ever sees, so a rebuild that left the suppressed links behind would answer "the set-up failed" * where the log of that deployment alone has to say "and the connection would not close either". */ @Test(timeOut = 60000) public void testTheSuppressedLinksOfAFailureSurviveItsRedaction() throws Exception { final String url = "jdbc:postgresql://opendj:S3cretOfTheBackend@127.0.0.1:5432/opendj"; final SQLException setupFailure = new SQLException("no transaction on " + url, "08006", 1); setupFailure.addSuppressed(new SQLException("the connection would not close", "08003", 2)); final SQLException reported = CachedConnection.reported(setupFailure, url); assertNoCredentials(reported); assertEquals(reported.getSuppressed().length, 1, "the rebuild of a redacted failure dropped what was suppressed on it"); assertTrue(String.valueOf(reported.getSuppressed()[0].getMessage()).contains("would not close"), "the suppressed link of a redacted failure: " + reported.getSuppressed()[0]); } /** * A database that is not listening at all: every dialect reports it instead of retrying the * refused connect until the caller gives up on the operation. */ @Test(timeOut = 120000) public void testRefusedConnectIsReportedAtOnce() throws Exception { final int closedPort = closedPort(); System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, Long.toString(BOUND_SECONDS)); for (final String url : urlsOf(closedPort)) { final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection(url); fail("a refused connect must be reported: " + CachedConnection.safeUrl(url)); } catch (SQLException expected) { // the failure of the moment, reported rather than retried } assertElapsedWithinBound(startedAt, BOUND_SECONDS * 1000); } } /** * The failure this bound exists for: a database that completes the TCP connection and then * says nothing - a moved VIP, a proxy at its connection limit, a host that lost its answer - * leaving the login of the driver, and with it the operation, without an end. The connection * of the accept queue is never answered here, so every dialect has to give up on its own. */ @Test(timeOut = 300000) public void testLoginIsBoundedWhenTheDatabaseNeverAnswers() throws Exception { System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, Long.toString(BOUND_SECONDS)); // a socket that is bound and never accepted: the kernel completes the handshake, so the // connect of the driver succeeds and every read of the login that follows hangs try (final ServerSocket blackhole = new ServerSocket(0, 50, InetAddress.getLoopbackAddress())) { for (final String url : urlsOf(blackhole.getLocalPort())) { // borrowed on a thread of its own: a bound that a driver does not honour has to // fail this test at once, and not by hanging the run it is part of final FutureTask borrow = new FutureTask<>(() -> CachedConnection.getConnection(url)); final Thread thread = new Thread(borrow, "borrow-" + CachedConnection.safeUrl(url)); thread.setDaemon(true); thread.start(); try { final Connection con = borrow.get(BOUND_SECONDS * 1000 + BOUND_MARGIN_MS, TimeUnit.MILLISECONDS); fail("a database that never answers must not hand out a connection: " + con); } catch (TimeoutException e) { fail("the login of " + CachedConnection.safeUrl(url) + " is not bounded: it never gave up"); } catch (ExecutionException expected) { assertTrue(expected.getCause() instanceof SQLException, String.valueOf(expected.getCause())); } } } } /** * The deadline of the borrow bounds the attempt inside it as well: the pool timeout stands for * the whole borrow, and an attempt left to run out its own bound would overrun it by that bound. */ @Test(timeOut = 120000) public void testTheAttemptIsBoundedByTheDeadlineOfTheBorrow() throws Exception { System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "600"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "2"); try (final ServerSocket blackhole = new ServerSocket(0, 50, InetAddress.getLoopbackAddress())) { final String url = "jdbc:postgresql://127.0.0.1:" + blackhole.getLocalPort() + "/opendj?user=opendj&password=opendj"; final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection(url); fail("a database that never answers must not hand out a connection"); } catch (SQLException expected) { // reported, and within the borrow it was given rather than the 600 s of the attempt } final long elapsed = System.currentTimeMillis() - startedAt; assertTrue(elapsed < 2000 + BOUND_MARGIN_MS, "the attempt outlived the deadline of the borrow: " + elapsed + " ms"); } } /** Pool exhaustion stays a retry - one of our own connections is on its way back to the pool. */ @Test(timeOut = 120000) public void testConnectionLimitIsRetried() throws Exception { final String url = StubDriver.PREFIX + "retried"; stub.failWith(tooManyConnections(), 2); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); final Connection con = CachedConnection.getConnection(url); assertNotNull(con); assertEquals(stub.attempts.get(), 3, "the connect must be retried while the database is at its limit"); } /** ... but under a deadline: the retry used to double its wait from 1 ms with no end to it. */ @Test(timeOut = 120000) public void testConnectionLimitGivesUpAtTheDeadline() throws Exception { final String url = StubDriver.PREFIX + "deadline"; stub.failWith(tooManyConnections(), StubDriver.ALWAYS); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "2"); final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection(url); fail("a database that stays at its connection limit must be reported, not waited out forever"); } catch (SQLTimeoutException expected) { assertTrue(expected.getMessage().contains("2s"), expected.getMessage()); assertEquals(((SQLException) expected.getCause()).getSQLState(), "53300"); // the state of a connect that did not happen, rather than none at all: this is the // failure of a borrow, and monitoring reading the state off what it caught would // otherwise see null where the driver's own exception carried one assertEquals(expected.getSQLState(), "08001"); } final long elapsed = System.currentTimeMillis() - startedAt; assertTrue(elapsed >= 2000, "gave up after " + elapsed + " ms, before the deadline it was given"); assertElapsedWithinBound(startedAt, 2000); assertTrue(stub.attempts.get() > 1, "the connect must be retried while the deadline lasts"); } /** * A database on its way up - starting, recovering, shutting down - says so, and says it for * seconds: the backend it belongs to would otherwise stay locked down until the next restart * of the server, since nothing above JDBCStorage.open() attempts it a second time. */ @Test(timeOut = 120000) public void testDatabaseOnItsWayUpIsRetried() throws Exception { final String url = StubDriver.PREFIX + "starting-up"; stub.failWith(new SQLException("the database system is starting up", "57P03"), 2); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); final Connection con = CachedConnection.getConnection(url); assertNotNull(con); assertEquals(stub.attempts.get(), 3, "a database that is starting up must be waited out"); } /** ... and it is recognized however the driver wrapped it: a SQLException carries two chains. */ @Test(timeOut = 120000) public void testTheWholeChainOfTheFailureIsLookedAt() throws Exception { final String url = StubDriver.PREFIX + "wrapped"; final SQLException wrapped = new SQLException("could not connect to the server", "08006"); wrapped.setNextException(tooManyConnections()); stub.failWith(wrapped, 1); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); assertNotNull(CachedConnection.getConnection(url)); assertEquals(stub.attempts.get(), 2, "the failure behind the one reported must be looked at"); } /** * ... and however far down the chain the driver put it. The walk that decides ends where the * chain ends, not at a count of links: mssql-jdbc chains every error of one message through * setNextException, and a walk that stopped at 32 of them answered "the caller's to see" for * the link it never reached - the connect reported as permanent on a database that would have * taken it a moment later, and a backend that stays locked down for it (issue #1076). */ @Test(timeOut = 120000) public void testARetryableLinkPastTheOldBudgetOfTheWalkIsLookedAt() throws Exception { final String url = StubDriver.PREFIX + "deep-chain"; // 32 links that say nothing of the moment in front of the one that says the database is at its limit SQLException chain = tooManyConnections(); for (int link = 32; link > 0; link--) { final SQLException inFront = new SQLException("error " + link + " of the same message", "08006", link); inFront.setNextException(chain); chain = inFront; } stub.failWith(chain, 1); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); try { assertNotNull(CachedConnection.getConnection(url)); } catch (SQLException reported) { fail("a database at its limit 33 links down the failure must be waited out, not reported: " + reported, reported); } assertEquals(stub.attempts.get(), 2, "the link past the 32nd must be looked at"); } /** * The rest of the insufficient_resources class is not worth waiting out: a server out of disk * is not made whole by a connection of ours coming back to the pool. */ @Test(timeOut = 120000) public void testDiskFullIsNotRetried() throws Exception { final String url = StubDriver.PREFIX + "disk-full"; stub.failWith(new SQLException("could not extend file: No space left on device", "53100"), StubDriver.ALWAYS); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); try { CachedConnection.getConnection(url); fail("a database out of disk must be reported to the caller"); } catch (SQLException expected) { assertEquals(expected.getSQLState(), "53100"); } assertEquals(stub.attempts.get(), 1, "a failure that waiting cannot clear must be attempted once"); } /** * Every other failure is the caller's to report. A password the database does not accept is * never going to be accepted by waiting, and the retry that swallowed it left the operation * hanging with nothing in the log. */ @Test(timeOut = 120000) public void testRejectedLoginIsNotRetried() throws Exception { final String url = StubDriver.PREFIX + "rejected"; stub.failWith(new SQLException("password authentication failed", "28P01"), StubDriver.ALWAYS); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "30"); try { CachedConnection.getConnection(url); fail("a rejected login must be reported to the caller"); } catch (SQLException expected) { assertEquals(expected.getSQLState(), "28P01"); } assertEquals(stub.attempts.get(), 1, "a rejected login must be attempted once"); } /** * The limit an account is given of its own is the same failure as the limit of the server, and * clears the same way: the connections this account may hold at once are held by this pool, and * one of them is on its way back to it. mysql reports it as 1226 ER_USER_LIMIT_REACHED and in * the syntax error class - 42000, where a statement the database refused lands - so the vendor * code is the whole of what tells the two apart (#1011). */ @Test(timeOut = 120000) public void testThePerAccountConnectionLimitOfMysqlIsRetried() { assertTrue(CachedConnection.isWorthRetrying( new SQLException("User 'opendj' has exceeded the 'max_user_connections' resource (current value: 4)", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "a per-account connection limit clears when a connection of this pool goes back to it"); } /** * The same code carries the limits an account is given per hour, and those are cleared by the * top of the hour rather than by a connection coming back. Waiting one out would cost every * borrow the whole deadline of the pool - a worker thread apiece, for as long as the hour lasts * - and would hide the message naming the resource behind a timeout, so it is reported at once. * The resource is named by the server as a literal of its own, which is why it can be read out * of a message whose text is otherwise the server's to translate. */ @Test(timeOut = 120000) public void testTheHourlyLimitOfAMysqlAccountIsNotRetried() { assertFalse(CachedConnection.isWorthRetrying( new SQLException("User 'opendj' has exceeded the 'max_connections_per_hour' resource (current value: 5)", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "an hourly limit is not cleared by a connection of this pool coming back"); } /** * ... and the resource is read by the name the server gives it rather than by the shape of that * name: the queries an account is granted per hour are named max_questions, with no _per_hour * about them (measured against mysql:9.2). This one reaches the road this gate guards - an * account whose quota is spent is refused the connect itself, Connector/J spending what is left * of it on the queries of its own login - so a wait would park a worker thread in every borrow * and every catalog connect until the hour turns, with the message naming the resource hidden * behind the timeout of the borrow. */ @Test(timeOut = 120000) public void testTheHourlyQueryLimitOfAMysqlAccountIsNotRetried() { assertFalse(CachedConnection.isWorthRetrying( new SQLException("User 'opendj' has exceeded the 'max_questions' resource (current value: 5)", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "the queries granted per hour are named max_questions, and no connection coming back clears them"); } /** * The same for the updates granted per hour, which the server names max_updates: this one is met * by a statement that changes data rather than by a connect, so it reaches this gate only where * a driver hands the pool a failure of something else entirely - and it is no more cleared by a * connection coming back than the queries are. */ @Test(timeOut = 120000) public void testTheHourlyUpdateLimitOfAMysqlAccountIsNotRetried() { assertFalse(CachedConnection.isWorthRetrying( new SQLException("User 'opendj' has exceeded the 'max_updates' resource (current value: 5)", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "the updates granted per hour are named max_updates, and no connection coming back clears them"); } /** * A resource this code carries that this server has yet to be given is reported rather than * waited out: what the wait clears is the connections of this pool, and a resource nobody here * has heard of is not those. That is what master did with every one of these codes, so an * unknown resource costs a borrow nothing it did not cost before (#1011). */ @Test(timeOut = 120000) public void testAMysqlResourceThisGateDoesNotKnowIsNotRetried() { assertFalse(CachedConnection.isWorthRetrying( new SQLException("User 'opendj' has exceeded the 'max_statements_per_minute' resource" + " (current value: 5)", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "a resource of this code that is not the concurrent limit is the caller's to see"); } /** * A 1226 that names no resource - a proxy that rewrote the message, a driver that kept the code * and not the text - is waited out rather than reported: the concurrent limit is the one an * account is given in practice, the wait it costs is bounded by the deadline of the borrow, and * reading it as permanent fails an operation a connection of ours would have served. */ @Test(timeOut = 120000) public void testAMysqlAccountLimitWhoseResourceIsNotNamedIsRetried() { assertTrue(CachedConnection.isWorthRetrying(new SQLException("connection rejected", "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "a limit whose resource is not named is taken for the concurrent one"); } /** ... which is the message of a driver that kept the code and dropped the text: none at all. */ @Test(timeOut = 120000) public void testAMysqlAccountLimitWithNoMessageIsRetried() { assertTrue(CachedConnection.isWorthRetrying(new SQLException(null, "42000", 1226), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "a limit arriving without a message is taken for the concurrent one rather than thrown at"); } /** * A host the server blocked is no limit that clears itself: the host cache holds the block until * an administrator flushes it, so waiting the deadline of a borrow out would only hide the one * message naming the remedy behind a timeout. It belongs with the password that is not accepted. */ @Test(timeOut = 120000) public void testAHostMysqlBlockedIsNotRetried() { assertFalse(CachedConnection.isWorthRetrying( new SQLException("Host 'ldap1.example.com' is blocked because of many connection errors;" + " unblock with 'mysqladmin flush-hosts'", "HY000", 1129), CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "a blocked host is cleared by an administrator, not by waiting"); } /** * The resource is read off the link the code arrived on, not off the failure as a whole: a * wrapper - a proxy, a DataSource of a container - is free to carry the text of something else * entirely above the exception that carries the code, and a verdict made of the two together * belongs to neither of them. */ @Test(timeOut = 120000) public void testTheResourceIsReadOffTheLinkCarryingTheCode() { final SQLException wrapped = new SQLException("could not connect: the account has exceeded the" + " 'max_connections_per_hour' resource", "08006", new SQLException("User 'opendj' has exceeded the 'max_user_connections' resource (current value: 4)", "42000", 1226)); assertTrue(CachedConnection.isWorthRetrying(wrapped, CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "the resource of the link carrying the code is what the verdict is made of"); } /** ... and the other way around: the text of the wrapper decides nothing for the code beneath it. */ @Test(timeOut = 120000) public void testTheResourceOfAWrapperDecidesNothing() { final SQLException wrapped = new SQLException("could not connect to the server", "08006", new SQLException("User 'opendj' has exceeded the 'max_connections_per_hour' resource (current value: 5)", "42000", 1226)); assertFalse(CachedConnection.isWorthRetrying(wrapped, CachedConnection.ConnectDialect.of("jdbc:mysql://db.example.com:3306/opendj")), "an hourly limit stays an hourly limit under a wrapper that names no resource"); } /** * The sessions an oracle account may hold at once are the SESSIONS_PER_USER of its profile, and * ORA-02391 is the per-account sibling of the ORA-00020 this table knew already: both are * cleared by a session ending, which for this pool is a connection of its own going back to it. */ @Test(timeOut = 120000) public void testTheSessionLimitOfAnOracleProfileIsRetried() { // the state as ojdbc reported it against a profile with sessions_per_user 1: 61000, no // connection class of its own either, so the vendor code is again the whole of the verdict assertTrue(CachedConnection.isWorthRetrying( new SQLException("ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit", "61000", 2391), CachedConnection.ConnectDialect.of("jdbc:oracle:thin:@db.example.com:1521/FREEPDB1")), "the session limit of a profile is cleared by a session of this pool ending"); } /** ORA-00018, the same limit as the instance keeps it: a session of somebody's has to end. */ @Test(timeOut = 120000) public void testTheSessionLimitOfAnOracleInstanceIsRetried() { // no state: an instance out of sessions is not something this test could provoke to measure // one from, and the vendor code is what the verdict is made of assertTrue(CachedConnection.isWorthRetrying( new SQLException("ORA-00018: maximum number of sessions exceeded", null, 18), CachedConnection.ConnectDialect.of("jdbc:oracle:thin:@db.example.com:1521/FREEPDB1")), "the session limit of an instance is cleared by a session ending"); } /** * A connection the setup of which failed belongs to nobody: it has to be closed, not leaked. *

* And a driver that will not close is said so on the failure being unwound rather than swallowed * (#929): the connection is gone either way, but a driver refusing to close is the shape of a * leak nobody would otherwise hear about, and the failure of the set-up is the one report this * attempt makes. */ @Test(timeOut = 120000) public void testConnectionIsClosedWhenItsSetupFails() throws Exception { final String url = StubDriver.PREFIX + "setup-failure"; final Connection broken = mock(Connection.class); doThrow(new SQLException("read only")).when(broken).setAutoCommit(false); doThrow(new SQLException("will not close")).when(broken).close(); stub.answerWith(broken); try { CachedConnection.getConnection(url); fail("a connection that cannot be set up must be reported"); } catch (SQLException expected) { assertEquals(expected.getMessage(), "read only"); assertEquals(expected.getSuppressed().length, 1, "the failure of the close is not carried on the failure being unwound"); assertEquals(expected.getSuppressed()[0].getMessage(), "will not close"); } verify(broken).close(); } /** The same, for a driver whose failure in the setup is not a SQLException but an unchecked one. */ @Test(timeOut = 120000) public void testConnectionIsClosedWhenItsSetupFailsWithAnUncheckedError() throws Exception { final String url = StubDriver.PREFIX + "setup-unchecked"; final Connection broken = mock(Connection.class); doThrow(new IllegalStateException("driver internal")).when(broken).setTransactionIsolation(anyInt()); doThrow(new IllegalStateException("will not close")).when(broken).close(); stub.answerWith(broken); try { CachedConnection.getConnection(url); fail("a connection that cannot be set up must be reported"); } catch (IllegalStateException expected) { assertEquals(expected.getMessage(), "driver internal"); // the unchecked failure of a close as well: it runs from the catch of a failure it must // not replace (JLS 14.20.2), which is the rule every close of this class keeps assertEquals(expected.getSuppressed().length, 1, "the failure of the close is not carried on the failure being unwound"); assertEquals(expected.getSuppressed()[0].getMessage(), "will not close"); } verify(broken).close(); } /** * isValid(n) is not a bound at the socket on every driver - the SQL Server driver turns it * into a query timeout, which needs an answer from the server to fire - and the read bound of * the login was lifted the moment the connection was established, so the socket carries the * bound of the validation, for the length of the validation only. */ @Test(timeOut = 120000) public void testValidationOfAPooledConnectionIsBoundedAtTheSocket() throws Exception { final String url = StubDriver.PREFIX + "validation-bound"; final Connection pooled = mock(Connection.class); when(pooled.isValid(anyInt())).thenReturn(true); when(pooled.getNetworkTimeout()).thenReturn(0); seedPool(url, pooled); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, pooled); final InOrder inOrder = inOrder(pooled); inOrder.verify(pooled).setNetworkTimeout(any(Executor.class), eq(CachedConnection.VALIDATION_TIMEOUT_SECONDS * 1000)); inOrder.verify(pooled).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); inOrder.verify(pooled).setNetworkTimeout(any(Executor.class), eq(0)); } /** * A driver that takes the call bounding the validation and then fails inside it is told apart * from one that never takes a network timeout at all: it is free to have applied the bound * before failing, so the connection is discarded rather than validated unbounded and handed * out - pooled, it would carry five seconds of ours into every statement for the rest of its * life, and the import batch of a backend open is the first thing to die of that. */ @Test(timeOut = 120000) public void testAPooledConnectionThatCannotBeBoundedForItsValidationIsDiscarded() throws Exception { final String url = StubDriver.PREFIX + "validation-bound-fails"; final Connection pooled = mock(Connection.class); when(pooled.getNetworkTimeout()).thenReturn(0); when(pooled.isValid(anyInt())).thenReturn(true); doThrow(new SQLException("the driver took the bound and then failed")) .when(pooled).setNetworkTimeout(any(Executor.class), anyInt()); seedPool(url, pooled); final Connection fresh = mock(Connection.class); stub.answerWith(fresh); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh, "a connection this could not bound was handed out"); verify(pooled, never()).isValid(anyInt()); verify(pooled).close(); } /** * A driver answering a negative network timeout is outside the contract of the call - 0 is no * limit and nothing below it stands for anything - and taken back as it is, it is one of the * two sentinels this class tells its own outcomes apart by: the bound of the validation would * be read as a bound that was never set, and the connection would go back into the pool still * carrying five seconds of ours into every statement of the next borrower. */ @Test(timeOut = 120000) public void testAPooledConnectionWhoseDriverAnswersANegativeBoundIsPutBackUnbounded() throws Exception { final String url = StubDriver.PREFIX + "validation-negative-bound"; final Connection pooled = mock(Connection.class); when(pooled.isValid(anyInt())).thenReturn(true); when(pooled.getNetworkTimeout()).thenReturn(-1); CachedConnection.poolOf(url).addIdle(new CachedConnection(url, pooled)); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, pooled); final InOrder inOrder = inOrder(pooled); inOrder.verify(pooled) .setNetworkTimeout(any(Executor.class), eq(CachedConnection.VALIDATION_TIMEOUT_SECONDS * 1000)); inOrder.verify(pooled).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); inOrder.verify(pooled).setNetworkTimeout(any(Executor.class), eq(0)); } /** A read bound of the connection string is tighter than ours and stays untouched. */ @Test(timeOut = 120000) public void testValidationLeavesTheBoundOfTheConnectionStringAlone() throws Exception { final String url = StubDriver.PREFIX + "validation-tighter"; final Connection pooled = mock(Connection.class); when(pooled.isValid(anyInt())).thenReturn(true); when(pooled.getNetworkTimeout()).thenReturn(2000); seedPool(url, pooled); assertNotNull(CachedConnection.getConnection(url)); verify(pooled, never()).setNetworkTimeout(any(Executor.class), anyInt()); } /** * The pool has no upper bound on the number of connections it holds, and a validation is a * round trip: after a failover that left them half-open, draining the pool must not outlive * the deadline of the borrow - establishing a connection is the faster answer past it. */ @Test(timeOut = 120000) public void testDrainOfThePoolStopsAtTheDeadline() throws Exception { final String url = StubDriver.PREFIX + "drain-deadline"; final int pooled = 8; final AtomicInteger validated = new AtomicInteger(); for (int i = 0; i < pooled; i++) { final Connection stale = mock(Connection.class); when(stale.isValid(anyInt())).thenAnswer(invocation -> { validated.incrementAndGet(); Thread.sleep(500); // a database that no longer answers: every validation waits out its bound return false; }); seedPool(url, stale); } final Connection fresh = mock(Connection.class); stub.answerWith(fresh); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh); assertTrue(validated.get() > 0 && validated.get() < pooled, "the drain has to start and to stop at the deadline: " + validated.get() + " of " + pooled); } /** A pooled connection that no longer validates is closed and replaced, not handed out. */ @Test(timeOut = 120000) public void testBrokenPooledConnectionIsDiscarded() throws Exception { final String url = StubDriver.PREFIX + "broken-pooled"; final Connection stale = mock(Connection.class); when(stale.isValid(anyInt())).thenReturn(false); seedPool(url, stale); final Connection fresh = mock(Connection.class); when(fresh.isValid(anyInt())).thenReturn(true); stub.answerWith(fresh); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh); verify(stale).close(); // the validation of a pooled connection needs a bound of its own as well verify(stale, never()).isValid(0); verify(stale).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); } /** * The same for a driver whose answer to the validation is an unchecked failure: it unwinds * through poll(), which stands outside every try of the borrow, so the connection it was * raised over is already out of the pool and would be held by nobody. */ @Test(timeOut = 120000) public void testAPooledConnectionWhoseValidationThrowsIsDiscarded() throws Exception { final String url = StubDriver.PREFIX + "validation-unchecked"; final Connection broken = mock(Connection.class); when(broken.isValid(anyInt())).thenThrow(new IllegalStateException("driver internal")); seedPool(url, broken); final Connection fresh = mock(Connection.class); stub.answerWith(fresh); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh); verify(broken).close(); } /** A connection that cannot be rolled back must not go back into the pool - nor be dropped. */ @Test(timeOut = 120000) public void testConnectionThatCannotBeRolledBackIsClosed() throws Exception { final String url = StubDriver.PREFIX + "rollback-failure"; final Connection parent = mock(Connection.class); doThrow(new SQLException("connection is closed")).when(parent).rollback(); try { new CachedConnection(url, parent).close(); fail("a failed rollback must be reported"); } catch (SQLException expected) { assertEquals(expected.getMessage(), "connection is closed"); } verify(parent).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 0, "a connection that cannot be rolled back was pooled"); } /** * The same for the unchecked failure a driver is free to throw instead of a SQLException. close() * runs past the CAS that makes it the one return of this connection, so a rollback escaping it * leaves the connection closed by nothing at all - and its permit released by nothing either, * since only destroy() gives one back. A pool is never removed from the map, so that place in * the bound would be gone for the life of the server, and enough of them leave every borrow to * fail with a SQLTimeoutException while the pool holds no connection at all (#878). */ @Test(timeOut = 120000) public void testAConnectionWhoseRollbackFailsUncheckedIsClosed() throws Exception { final String url = StubDriver.PREFIX + "rollback-unchecked"; final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); doThrow(new IllegalStateException("the connection handle is no longer valid")).when(parent).rollback(); stub.answerWith(parent); final CachedConnection.Pool pool = CachedConnection.poolOf(url); final Connection con = CachedConnection.getConnection(url); assertEquals(pool.meteredCount(), 1, "the borrow took no permit of the pool"); try { con.close(); fail("a rollback that failed unchecked must be reported"); } catch (IllegalStateException expected) { assertEquals(expected.getMessage(), "the connection handle is no longer valid"); } verify(parent).close(); assertEquals(pool.idleCount(), 0, "a connection that could not be rolled back was pooled"); assertEquals(pool.meteredCount(), 0, "the return kept a permit of the pool"); } @Test public void testDialectIsRecognizedByTheConnectionString() throws Exception { assertEquals(CachedConnection.ConnectDialect.of("jdbc:postgresql://h:5432/db"), CachedConnection.ConnectDialect.POSTGRES); assertEquals(CachedConnection.ConnectDialect.of("jdbc:mysql://h:3306/db"), CachedConnection.ConnectDialect.MYSQL); assertEquals(CachedConnection.ConnectDialect.of("jdbc:oracle:thin:@//h:1521/svc"), CachedConnection.ConnectDialect.ORACLE); assertEquals(CachedConnection.ConnectDialect.of("jdbc:sqlserver://h:1433;databaseName=db"), CachedConnection.ConnectDialect.MICROSOFT); assertNull(CachedConnection.ConnectDialect.of("jdbc:h2:mem:db"), "an unknown engine must not be fed the properties of another"); } /** Both phases are bounded, in the units of the driver: the connect alone leaves the login open. */ @Test public void testBothPhasesOfTheLoginAreBounded() throws Exception { // pgjdbc puts an SO_TIMEOUT on the login socket only where socketTimeout is set: without it // loginTimeout bounds the caller alone, and the thread the driver runs the login on stays // parked in the read it abandoned final Properties postgres = new Properties(); assertTrue(CachedConnection.ConnectDialect.POSTGRES.bound("jdbc:postgresql://h:5432/db", postgres, 7), "the read bound of postgresql outlives the login and has to be lifted"); assertEquals(postgres.getProperty("connectTimeout"), "7"); assertEquals(postgres.getProperty("socketTimeout"), "7"); assertEquals(postgres.getProperty("loginTimeout"), "7", "the bound of a url naming more than one host"); final Properties mysql = new Properties(); assertTrue(CachedConnection.ConnectDialect.MYSQL.bound("jdbc:mysql://h:3306/db", mysql, 7), "the read bound of mysql outlives the login and has to be lifted"); assertEquals(mysql.getProperty("connectTimeout"), "7000"); assertEquals(mysql.getProperty("socketTimeout"), "7000"); final Properties oracle = new Properties(); assertTrue(CachedConnection.ConnectDialect.ORACLE.bound("jdbc:oracle:thin:@//h:1521/svc", oracle, 7)); assertEquals(oracle.getProperty("oracle.net.CONNECT_TIMEOUT"), "7000"); assertEquals(oracle.getProperty("oracle.jdbc.ReadTimeout"), "7000"); // the loginTimeout of the sql server driver leaves the read of the prelogin answer open final Properties microsoft = new Properties(); assertTrue(CachedConnection.ConnectDialect.MICROSOFT.bound("jdbc:sqlserver://h:1433;databaseName=db", microsoft, 7)); assertEquals(microsoft.getProperty("loginTimeout"), "7"); assertEquals(microsoft.getProperty("socketTimeout"), "7000"); } /** * A driver with a range of its own for its connect property is never handed a value beyond it: * SQLServerDriverIntProperty.LOGIN_TIMEOUT is validated against [0, 65535], so a bound past * that would not widen the connect, it would fail every one of them. */ @Test public void testConnectBoundStaysInTheRangeTheDriverTakes() throws Exception { final Properties microsoft = new Properties(); CachedConnection.ConnectDialect.MICROSOFT.bound("jdbc:sqlserver://h:1433;databaseName=db", microsoft, 100000); assertEquals(microsoft.getProperty("loginTimeout"), "65535"); assertEquals(microsoft.getProperty("socketTimeout"), "100000000", "the read bound takes any value"); } /** * A bound the administrator put into the connection string by hand - the only workaround this * backend had - keeps precedence, property by property. */ @Test public void testConnectionStringKeepsPrecedence() throws Exception { final Properties postgres = new Properties(); CachedConnection.ConnectDialect.POSTGRES.bound( "jdbc:postgresql://h:5432/db?user=u&password=p&loginTimeout=30&socketTimeout=300", postgres, 7); assertNull(postgres.getProperty("loginTimeout"), "the setting of the connection string was overridden"); assertNull(postgres.getProperty("socketTimeout"), "the setting of the connection string was overridden"); assertNull(postgres.getProperty("connectTimeout"), "the connect side is one budget: a bound of the administrator under either of its names is theirs"); // the sql server driver gives a supplied property precedence over the one of the url final Properties microsoft = new Properties(); CachedConnection.ConnectDialect.MICROSOFT.bound("jdbc:sqlserver://h:1433;loginTimeout=45;databaseName=db", microsoft, 7); assertNull(microsoft.getProperty("loginTimeout"), "the setting of the connection string was overridden"); assertEquals(microsoft.getProperty("socketTimeout"), "7000"); // inside the descriptor of an oracle tns url the property goes by the last segment of its name final Properties oracle = new Properties(); CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(DESCRIPTION=(CONNECT_TIMEOUT=3)(ADDRESS=(HOST=h)(PORT=1521)))", oracle, 7); assertNull(oracle.getProperty("oracle.net.CONNECT_TIMEOUT")); assertEquals(oracle.getProperty("oracle.jdbc.ReadTimeout"), "7000"); // inside the descriptor the read bound goes by READ_TIMEOUT, and one of the administrator // is never lifted after the login, because ours is not set on top of it final Properties read = new Properties(); assertFalse(CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(DESCRIPTION=(READ_TIMEOUT=30)(ADDRESS=(HOST=h)(PORT=1521)))", read, 7), "a read bound of the connection string must not be lifted once the login is through"); assertNull(read.getProperty("oracle.jdbc.ReadTimeout")); // ... while RECV_TIMEOUT is a parameter of sqlnet.ora and of the listener that ojdbc8 does // not read - the name appears nowhere in the driver - so a descriptor carrying one is not // a read bound of the connection and must not take ours off it final Properties recv = new Properties(); assertTrue(CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(DESCRIPTION=(RECV_TIMEOUT=30)(ADDRESS=(HOST=h)(PORT=1521)))", recv, 7), "a parameter no driver reads left the login of this connection unbounded"); assertEquals(recv.getProperty("oracle.jdbc.ReadTimeout"), "7000"); // a name that only appears as the tail of another parameter is not a setting of its own final Properties mysql = new Properties(); CachedConnection.ConnectDialect.MYSQL.bound("jdbc:mysql://h:3306/db?xconnectTimeout=1&socketTimeoutX=2", mysql, 7); assertEquals(mysql.getProperty("connectTimeout"), "7000"); assertEquals(mysql.getProperty("socketTimeout"), "7000"); } /** * A parameter is recognized the way the driver of its dialect recognizes it: pgjdbc and * Connector/J look their properties up by their exact name, so a name of another case is a * parameter of nobody - neither side bounds anything by it - and must not pass for a bound the * administrator set, while the other two match either way. */ @Test public void testTheCaseOfAParameterIsTheOneOfItsDriver() throws Exception { final Properties postgres = new Properties(); CachedConnection.ConnectDialect.POSTGRES.bound("jdbc:postgresql://h:5432/db?ConnectTimeout=5", postgres, 7); assertEquals(postgres.getProperty("connectTimeout"), "7", "pgjdbc ignores a parameter of another case"); // PropertyKey.fromValue("SocketTimeout") answers null, and Connector/J then reads no bound // out of the url either: a mis-cased parameter left the borrower parked on a host that // completes the handshake and says nothing final Properties mysql = new Properties(); CachedConnection.ConnectDialect.MYSQL.bound("jdbc:mysql://h:3306/db?SocketTimeout=1", mysql, 7); assertEquals(mysql.getProperty("socketTimeout"), "7000", "Connector/J ignores a parameter of another case"); final Properties microsoft = new Properties(); CachedConnection.ConnectDialect.MICROSOFT.bound("jdbc:sqlserver://h:1433;LoginTimeout=45", microsoft, 7); assertNull(microsoft.getProperty("loginTimeout"), "the sql server driver normalizes the name of a property"); // the keywords of an oracle descriptor are matched without regard to case as well final Properties oracle = new Properties(); CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(description=(connect_timeout=3)(address=(host=h)(port=1521)))", oracle, 7); assertNull(oracle.getProperty("oracle.net.CONNECT_TIMEOUT"), "an oracle descriptor is read without case"); } /** * The connection string is not the only channel of the administrator: the oracle driver reads * some of its properties out of the system properties as well, which is how a whole jvm is * bounded with -Doracle.jdbc.ReadTimeout. A property supplied to the driver outranks that one * without a word, and this class would then lift it once the login is through as if it were * its own - leaving a connection with no read bound at all where the administrator set one. */ @Test public void testASystemPropertyOfTheAdministratorKeepsPrecedence() throws Exception { System.setProperty("oracle.jdbc.ReadTimeout", "30000"); try { final Properties oracle = new Properties(); assertFalse(CachedConnection.ConnectDialect.ORACLE.bound("jdbc:oracle:thin:@//h:1521/svc", oracle, 7), "a read bound of the administrator must not be lifted once the login is through"); assertNull(oracle.getProperty("oracle.jdbc.ReadTimeout"), "the setting of the administrator was overridden"); assertEquals(oracle.getProperty("oracle.net.CONNECT_TIMEOUT"), "7000", "the property it leaves open must still be bounded"); } finally { System.clearProperty("oracle.jdbc.ReadTimeout"); } // the connect property of the same driver, over the same channel System.setProperty("oracle.net.CONNECT_TIMEOUT", "30000"); try { final Properties oracle = new Properties(); CachedConnection.ConnectDialect.ORACLE.bound("jdbc:oracle:thin:@//h:1521/svc", oracle, 7); assertNull(oracle.getProperty("oracle.net.CONNECT_TIMEOUT"), "the setting of the administrator was overridden"); } finally { System.clearProperty("oracle.net.CONNECT_TIMEOUT"); } // a plain name is common enough to be somebody else's system property: only a name a // driver of these actually reads out of them is one of the administrator's System.setProperty("socketTimeout", "30000"); try { final Properties mysql = new Properties(); assertTrue(CachedConnection.ConnectDialect.MYSQL.bound("jdbc:mysql://h:3306/db", mysql, 7)); assertEquals(mysql.getProperty("socketTimeout"), "7000"); } finally { System.clearProperty("socketTimeout"); } } /** * ... and a dotted name is not a name a driver reads out of the system properties by the shape * of it. ojdbc8 resolves oracle.jdbc.ReadTimeout and oracle.net.CONNECT_TIMEOUT in three tiers * (the properties it was supplied, then System.getProperty, then the properties of the data * source), while oracle.net.READ_TIMEOUT - a dotted name of the same driver, and the name the * socket option is finally read under - reaches the socket from the connection properties * alone: the classes carrying the literal hand it to Properties.get, none of them to * System.getProperty. Taken for a bound of the administrator, a -D of it leaves the login with * no read bound whatever: theirs is not read and ours is not set. */ @Test public void testASystemPropertyNoDriverReadsIsNoBound() throws Exception { System.setProperty("oracle.net.READ_TIMEOUT", "30000"); try { final Properties oracle = new Properties(); assertTrue(CachedConnection.ConnectDialect.ORACLE.bound("jdbc:oracle:thin:@//h:1521/svc", oracle, 7), "a -D the driver never reads left this login with no read bound at all"); assertEquals(oracle.getProperty("oracle.jdbc.ReadTimeout"), "7000"); } finally { System.clearProperty("oracle.net.READ_TIMEOUT"); } // ... while the same name written into the connection string is one the driver does read final Properties declared = new Properties(); assertFalse(CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(DESCRIPTION=(READ_TIMEOUT=30)(ADDRESS=(HOST=h)(PORT=1521)))", declared, 7)); assertNull(declared.getProperty("oracle.jdbc.ReadTimeout")); } /** * A property the administrator set to 0 is not a bound of theirs: every one of these drivers * reads 0 as "wait as long as it takes", which is the default this class exists to replace - * and on the three whose driver lets a supplied property win, ours is set on top of it. * Postgresql is the one where it cannot be, and is covered on its own below. */ @Test public void testAZeroIsNotABoundOfTheAdministrator() throws Exception { final Properties mysql = new Properties(); CachedConnection.ConnectDialect.MYSQL.bound("jdbc:mysql://h:3306/db?connectTimeout=0&socketTimeout=0", mysql, 7); assertEquals(mysql.getProperty("connectTimeout"), "7000"); assertEquals(mysql.getProperty("socketTimeout"), "7000"); // ... and neither is a property left without a value final Properties microsoft = new Properties(); CachedConnection.ConnectDialect.MICROSOFT.bound("jdbc:sqlserver://h:1433;socketTimeout=;databaseName=db", microsoft, 7); assertEquals(microsoft.getProperty("socketTimeout"), "7000"); // the same of a system property, and of the descriptor of an oracle url System.setProperty("oracle.jdbc.ReadTimeout", "0"); try { final Properties oracle = new Properties(); assertTrue(CachedConnection.ConnectDialect.ORACLE.bound( "jdbc:oracle:thin:@(DESCRIPTION=(READ_TIMEOUT=0)(ADDRESS=(HOST=h)(PORT=1521)))", oracle, 7)); assertEquals(oracle.getProperty("oracle.jdbc.ReadTimeout"), "7000"); } finally { System.clearProperty("oracle.jdbc.ReadTimeout"); } // a value that is no number is left to the driver it belongs to: it is not this class's to read final Properties unreadable = new Properties(); assertFalse(CachedConnection.ConnectDialect.MYSQL.bound( "jdbc:mysql://h:3306/db?socketTimeout=PT30S", unreadable, 7)); assertNull(unreadable.getProperty("socketTimeout")); } /** * On postgresql a parameter of the url outranks the property this class supplies: Driver * .connect copies what it was handed into a flat map and parseURL then writes the parameters of * the url on top of it. So a "socketTimeout=0" there cannot be replaced, and setting ours * regardless would leave this class believing it bounded a login that carries no bound - and * lifting a read bound after it that was never in force. The effective values are read back * through the parser of the driver itself, since asserting on the map handed to it is * asserting on the half of the story this bug lived in. */ @Test public void testAParameterOfAPostgresUrlOutranksTheBoundOfThisClass() throws Exception { final String url = "jdbc:postgresql://h:5432/db?connectTimeout=0&socketTimeout=0&loginTimeout=0"; final Properties supplied = new Properties(); assertFalse(CachedConnection.ConnectDialect.POSTGRES.bound(url, supplied, 7), "a read bound that never reaches the driver must not be reported as one to lift"); assertNull(supplied.getProperty("socketTimeout")); assertNull(supplied.getProperty("connectTimeout")); assertNull(supplied.getProperty("loginTimeout")); final Properties effective = org.postgresql.Driver.parseURL(url, supplied); assertEquals(effective.getProperty("socketTimeout"), "0", "the url is what the driver ends up reading"); assertEquals(effective.getProperty("connectTimeout"), "0"); assertEquals(effective.getProperty("loginTimeout"), "0"); } /** * The connect side of a dialect is one budget rather than a set of independent knobs, so a * bound of the administrator under any of its names leaves all of them alone. On postgresql * connectTimeout bounds the socket connect and loginTimeout the login behind it: filling in the * one they left out caps the one they set, since Driver.connect hands the login to a thread of * its own as soon as loginTimeout is anything but 0 and gives up on it there. */ @Test public void testAConnectBudgetOfTheAdministratorIsNotCappedByThisClass() throws Exception { final String url = "jdbc:postgresql://h:5432/db?connectTimeout=300"; final Properties supplied = new Properties(); assertTrue(CachedConnection.ConnectDialect.POSTGRES.bound(url, supplied, 30), "the read bound is a budget of its own and is still set"); assertNull(supplied.getProperty("loginTimeout"), "a loginTimeout of ours caps the connectTimeout the administrator set"); assertNull(supplied.getProperty("connectTimeout")); assertEquals(supplied.getProperty("socketTimeout"), "30"); final Properties effective = org.postgresql.Driver.parseURL(url, supplied); assertEquals(effective.getProperty("connectTimeout"), "300", "the budget of the administrator, in full"); assertNull(effective.getProperty("loginTimeout"), "nothing of ours hands this login to a thread to abandon"); } /** * The bound handed to a driver stays inside the range an int of milliseconds takes. Where the * per-attempt property is off, the attempt takes what is left of the deadline of the borrow, * and the pool timeout has no upper bound of its own - while the SQL Server driver rejects a * socketTimeout past Integer.MAX_VALUE outright, failing every connect of that backend with * the name of a property nobody typed. */ @Test(timeOut = 120000) public void testTheBoundHandedToADriverStaysInTheRangeAnIntTakes() throws Exception { final long deadline = System.currentTimeMillis() + 3000000L * 1000; // 34 days: past 2^31 ms assertEquals(CachedConnection.attemptSeconds(0, deadline), Integer.MAX_VALUE / 1000); System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "0"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "3000000"); // a socket that answers the connect and closes it, rather than a port bound and released: // with every bound of this borrow turned off, anything taking that port in between would // leave the test hanging on the timeOut instead of failing try (final ServerSocket rejecting = rejectingSocket()) { final String url = "jdbc:sqlserver://127.0.0.1:" + rejecting.getLocalPort() + ";databaseName=opendj;user=opendj;password=opendj;encrypt=false"; final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection(url); fail("a connect the database closes must be reported"); } catch (SQLException expected) { assertFalse(expected.getMessage().contains("socketTimeout"), "the driver was handed a bound it does not take: " + expected.getMessage()); } assertElapsedWithinBound(startedAt, 0); } } /** The clamp of the range holds where the borrow has no deadline to take it from either. */ @Test public void testTheBoundOfAnAttemptStaysInRangeWithoutADeadline() throws Exception { assertEquals(CachedConnection.attemptSeconds(Long.MAX_VALUE, Long.MAX_VALUE), Integer.MAX_VALUE / 1000); assertEquals(CachedConnection.attemptSeconds(0, Long.MAX_VALUE), 0, "0 stands for an attempt with no bound"); assertEquals(CachedConnection.attemptSeconds(30, Long.MAX_VALUE), 30); } /** * A deadline so far off that naming it overflows is a wait with no end, and not one already * behind us: a borrow configured to wait practically forever would otherwise give up on its * first retryable failure, which is the opposite of what was asked for. The sum is what has to * be guarded and not only the product - a value under the clamp of the product can still name a * moment past the end of the epoch. */ @Test public void testADeadlineTooFarOffToNameIsAWaitWithNoEnd() throws Exception { final long startedAt = System.currentTimeMillis(); // 0 is the operator asking for no deadline at all assertEquals(CachedConnection.deadlineOf(startedAt, 0), Long.MAX_VALUE); // ... and so is a value whose milliseconds would not fit a long at all assertEquals(CachedConnection.deadlineOf(startedAt, Long.MAX_VALUE / 1000), Long.MAX_VALUE); // the one the product guard lets through, which is the largest value it does: a second under // the clamp, so the milliseconds of it still fit a long - by 1807 of them - while the moment // they name, counted from now, does not. Guarded by the sum alone assertEquals(CachedConnection.deadlineOf(startedAt, Long.MAX_VALUE / 1000 - 1), Long.MAX_VALUE); // and an ordinary value still names the moment it says assertEquals(CachedConnection.deadlineOf(startedAt, 60), startedAt + 60_000); } /** * A borrow that carries a bound of its own waits for the shorter of the two, whichever way the * deployment spelled a wait with no bound at all. *

* Only an import carries one: it holds a connection per tree it writes until it ends (#891), so * the pool it waits at may be full of nothing but its own connections and the wait would be a * deadlock rather than a queue. It has somewhere to go when the wait runs out - the tree is * written through a connection the import already holds - so a long wait of the deployment is * capped rather than merely replaced: paid over again for every tree the pool has nothing to * spare for, it would be the duration of an import rather than a bound on it. */ @Test public void testABorrowWithABoundOfItsOwnWaitsForTheShorterOfTheTwo() { assertEquals(CachedConnection.boundedWait(30, 60), 30, "the wait of the deployment was inside the bound"); assertEquals(CachedConnection.boundedWait(600, 60), 60, "a long finite wait was not capped"); assertEquals(CachedConnection.boundedWait(0, 60), 60, "0 stands for a wait with no bound"); // the other spelling of a wait with no bound: seconds enough that the milliseconds they // stand for do not fit in a long, which the deadline of the borrow reads as forever assertEquals(CachedConnection.boundedWait(Long.MAX_VALUE / 1000, 60), 60, "a wait whose milliseconds overflow a long is unbounded too"); assertEquals(CachedConnection.boundedWait(Long.MAX_VALUE, 60), 60); // ... and a borrow that carries no bound of its own takes the wait of the deployment whole assertEquals(CachedConnection.boundedWait(600, 0), 600); assertEquals(CachedConnection.boundedWait(0, 0), 0); } /** The connection string holds the credentials of the backend: a stall report must not carry them. */ @Test public void testLoggedConnectionStringCarriesNoCredentials() throws Exception { assertEquals(CachedConnection.safeUrl("jdbc:postgresql://h:5432/db?user=u&password=secret"), "jdbc:postgresql://h:5432/db"); // the parameter naming the database stays: two backends of one sql server host answer to // the same url up to it, and a stall report that cannot tell them apart is one of neither assertEquals(CachedConnection.safeUrl("jdbc:sqlserver://h:1433;databaseName=db;password=secret"), "jdbc:sqlserver://h:1433;databaseName=db"); // ... and the token naming the kind of oracle driver stays as well: thin against oci is a // first question of an oracle connect, and it stands in front of the credentials assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:scott/secret@//h:1521/svc"), "jdbc:oracle:thin:@//h:1521/svc"); assertEquals(CachedConnection.safeUrl("jdbc:mysql://u:secret@h:3306/db"), "jdbc:mysql://h:3306/db"); assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:@//h:1521/svc"), "jdbc:oracle:thin:@//h:1521/svc"); // a password holding the parameter separator of another dialect: ";" separates nothing on // an oracle url, so the credentials are cut in front of the "@" rather than inside them assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:scott/pa;ss@//h:1521/svc"), "jdbc:oracle:thin:@//h:1521/svc"); // ... and one holding a ":" is cut in front of it too: the token of the driver is the one // behind the subprotocol, not the last one standing in front of the "@" assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:scott/pa:ss@//h:1521/svc"), "jdbc:oracle:thin:@//h:1521/svc"); // ... and an "@" that stands inside a parameter is not the end of credentials: the host survives assertEquals(CachedConnection.safeUrl("jdbc:postgresql://h:5432/db?user=u@example.com&password=secret"), "jdbc:postgresql://h:5432/db"); // a password holding the parameter separator of its own dialect: on an oracle url the // parameters stand behind the descriptor, so a "?" in front of the "@" is part of the // password and cutting there would leave the start of it in the log assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:scott/pa?ss@//h:1521/svc"), "jdbc:oracle:thin:@//h:1521/svc"); // the same inside an authority, where the credentials end at the path rather than at a "?" assertEquals(CachedConnection.safeUrl("jdbc:mysql://u:sec?ret@h:3306/db"), "jdbc:mysql://h:3306/db"); // a descriptor of an oracle url carries no credentials and survives whole assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=h)(PORT=1521)))"), "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=h)(PORT=1521)))"); // a first host with nothing in front of the comma keeps the comma: what is left is the // hosts of a url, not one host of it assertEquals(CachedConnection.safeUrl("jdbc:mysql://,h2:3306/db"), "jdbc:mysql://,h2:3306/db"); // a password under a name of its own, and one numbered by the factor it belongs to assertEquals(CachedConnection.safeUrl("jdbc:mysql://(host=h,user=u,password2=secret)/db"), "jdbc:mysql://(host=h,user=u,password2=***)/db"); // a url of Connector/J gives every host of it credentials of its own, and every one of // them goes: the second used to stay in the message with the password of the failover host assertEquals(CachedConnection.safeUrl("jdbc:mysql://u:p@h1:3306,u2:p2@h2:3306/db"), "jdbc:mysql://h1:3306,h2:3306/db"); // ... including a url whose subprotocol names the kind of connection in front of the hosts assertEquals(CachedConnection.safeUrl("jdbc:mysql:replication://master:p1@h1:3306,slave:p2@h2:3306/db"), "jdbc:mysql:replication://h1:3306,h2:3306/db"); // the key-value host syntax of Connector/J puts the credentials inside the authority, where // neither the userinfo nor the parameters of a url stand assertEquals(CachedConnection.safeUrl("jdbc:mysql://address=(host=h)(port=3306)(user=u)(password=secret)/db"), "jdbc:mysql://address=(host=h)(port=3306)(user=u)(password=***)/db"); assertEquals(CachedConnection.safeUrl("jdbc:mysql://(host=h,port=3306,user=u,password=secret)/db"), "jdbc:mysql://(host=h,port=3306,user=u,password=***)/db"); assertEquals(CachedConnection.safeUrl("jdbc:sqlserver://h:1433;databaseName=db;PWD=secret"), "jdbc:sqlserver://h:1433;databaseName=db"); // a shape none of this took apart is not logged past its subprotocol: a password holding a // "/" ends the authority in front of the "@" that would have given the credentials away assertEquals(CachedConnection.safeUrl("jdbc:mysql://u:pa/ss@h:3306/db"), "jdbc:mysql:" + CachedConnection.CREDENTIALS_HIDDEN); // ... and so does a password that holds an "@" and was quoted for the driver assertEquals(CachedConnection.safeUrl("jdbc:oracle:thin:scott/\"pa@ss\"@//h:1521/svc"), "jdbc:oracle:" + CachedConnection.CREDENTIALS_HIDDEN); // a string that is no connection string of any driver carries nothing to the log either assertEquals(CachedConnection.safeUrl("h:5432/db?password=secret"), CachedConnection.CREDENTIALS_HIDDEN); } /** * A driver is free to quote the connection string it was handed back into the message of its * failure, and that message travels: RootContainer makes it the message of what it throws, * BackendConfigManager logs it at ERROR and answers a config change with it. So the message is * redacted rather than the two call sites that happen to log a url. */ @Test public void testAMessageOfADriverCarriesNoCredentials() throws Exception { final String url = "jdbc:postgresql://opendj:S3cret@h:5432/db"; assertEquals(CachedConnection.redact("No suitable driver found for " + url, url), "No suitable driver found for jdbc:postgresql://h:5432/db"); // a driver naming the credentials alone, without the url around them assertEquals(CachedConnection.redact("authentication of opendj:S3cret failed", url), "authentication of " + CachedConnection.CREDENTIALS_HIDDEN + " failed"); // ... and naming the password alone assertEquals(CachedConnection.redact("the password S3cret was not accepted", url), "the password " + CachedConnection.CREDENTIALS_HIDDEN + " was not accepted"); // the credentials of an oracle url stand in front of its descriptor final String oracle = "jdbc:oracle:thin:scott/S3cret@//h:1521/svc"; assertEquals(CachedConnection.redact("IO Error connecting to " + oracle, oracle), "IO Error connecting to jdbc:oracle:thin:@//h:1521/svc"); // a password of a parameter is blanked wherever the message carries it assertEquals(CachedConnection.redact("bad url jdbc:sqlserver://h:1433;password=S3cret", "jdbc:sqlserver://h:1433;password=S3cret"), "bad url jdbc:sqlserver://h:1433"); // a message naming nothing of the connection string is left as it stands assertEquals(CachedConnection.redact("Connection to h:5432 refused", url), "Connection to h:5432 refused"); assertNull(CachedConnection.redact(null, url)); // the stall report is the other way a driver's message reaches the log, and it carries the // url of the backend alongside it final String stall = CachedConnection.stallMessage(url, 3, 4000, new SQLException("FATAL: too many connections for " + url)); assertFalse(stall.contains("S3cret"), stall); assertTrue(stall.contains("jdbc:postgresql://h:5432/db"), stall); assertTrue(stall.contains("4000 ms") && stall.contains("(3 attempts)"), stall); // and so is the stall of a connect made outside the pool - the connection the tree catalog of a // backend is written on (#888) - which is under the same rule and describes the same url final String outside = CachedConnection.outsidePoolStallMessage(url, "tree catalog", 3, 4000, new SQLException("FATAL: too many connections for " + url)); assertFalse(outside.contains("S3cret"), outside); assertTrue(outside.contains("jdbc:postgresql://h:5432/db"), outside); assertTrue(outside.contains("4000 ms") && outside.contains("(3 attempts)"), outside); assertTrue(outside.contains("tree catalog"), outside); // and says what it is: a borrow of the pool is what this connect is not, and an operator // reading it must not be sent to the pool for a stall the pool has no part in assertFalse(outside.contains("pooled one"), outside); } /** * A password is free to be one character long, and a bare one of those stands inside half the * lines a driver writes. Replaced wherever it is found, it takes the diagnostic apart along * with the credential - and, since the walk looking for credentials asks the redaction whether * it changed anything, it makes every failure of that backend one whose chain is rebuilt. */ @Test public void testAShortPasswordDoesNotRewriteTheMessageOfADriver() throws Exception { final String url = "jdbc:oracle:thin:opendj/1@//h:1521/svc"; // the "1" of an ORA number is part of a number, not a credential of anybody assertEquals(CachedConnection.redact("ORA-12541: TNS:no listener", url), "ORA-12541: TNS:no listener"); // ... while the same password quoted back on its own is still taken out assertEquals(CachedConnection.redact("the password 1 was not accepted", url), "the password " + CachedConnection.CREDENTIALS_HIDDEN + " was not accepted"); assertEquals(CachedConnection.redact("IO Error connecting to " + url, url), "IO Error connecting to jdbc:oracle:thin:@//h:1521/svc"); } /** * A driver is free to name a parameter in the middle of a sentence. The value of one ends at * the first space: reaching to the end of the string, it would take the host, the port and the * cause of the failure into the blank along with the password - the very information the * redaction of a connection string goes out of its way to keep. */ @Test public void testAPasswordParameterDoesNotSwallowTheRestOfTheMessage() throws Exception { final String url = "jdbc:postgresql://h:5432/db?user=u&password=hunter2"; assertEquals(CachedConnection.redact("Connection refused: password=hunter2 for user u at h:5432", url), "Connection refused: password=*** for user u at h:5432"); } /** * The credentials of an oracle url are separated by a "/", so a password holding a "//" of * its own used to start an authority inside itself: what was taken off as a userinfo was the * tail of the password, the "@" the last guard of safeUrl() looks for went with it, and the * user name and the head of the password stayed in the message of a stall. */ @Test public void testAPasswordHoldingASlashPairIsNotLeftInTheLog() throws Exception { final String easyConnect = "jdbc:oracle:thin:opendj/pa//ss@//h:1521/svc"; assertEquals(CachedConnection.safeUrl(easyConnect), "jdbc:oracle:thin:@//h:1521/svc"); // ... and the same password in front of a host that names no "//" of its own final String sid = "jdbc:oracle:thin:opendj/pa//ss@h:1521:svc"; assertEquals(CachedConnection.safeUrl(sid), "jdbc:oracle:thin:@h:1521:svc"); // the message of a driver quoting the url back carries no more of it than the log does assertEquals(CachedConnection.redact("IO Error connecting to " + easyConnect, easyConnect), "IO Error connecting to jdbc:oracle:thin:@//h:1521/svc"); } /** * pgjdbc enforces loginTimeout out of process: Driver.connect hands the login to a daemon * thread of its own and gives up on the thread rather than on the login. Against the database * this bound exists for - one that completes the handshake and then says nothing - an * unbounded read there leaves that thread, and the socket it holds, behind on every borrow; * a few operations a second are enough to run the server out of threads and file descriptors. */ @Test(timeOut = 300000) public void testTheLoginThreadOfPostgresDoesNotOutliveTheBorrow() throws Exception { System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, Long.toString(BOUND_SECONDS)); // counted as a delta of this borrow rather than as a count of the jvm: three tests of this // file open a pgjdbc login against a socket that never answers, and the order they run in // is not contractual - a thread left by any of them would be reported here final int before = loginThreadsOfPostgres(); try (final ServerSocket blackhole = new ServerSocket(0, 50, InetAddress.getLoopbackAddress())) { final String url = "jdbc:postgresql://127.0.0.1:" + blackhole.getLocalPort() + "/opendj?user=opendj&password=opendj"; try { CachedConnection.getConnection(url); fail("a database that never answers must not hand out a connection"); } catch (SQLException expected) { // reported to the caller, as the bound of the attempt promises } final long giveUpAt = System.currentTimeMillis() + BOUND_SECONDS * 1000 + BOUND_MARGIN_MS; while (loginThreadsOfPostgres() > before && System.currentTimeMillis() < giveUpAt) { Thread.sleep(100); } assertTrue(loginThreadsOfPostgres() <= before, "the login thread pgjdbc abandoned outlived the borrow: the read of the login is not bounded"); } } private static int loginThreadsOfPostgres() { int alive = 0; for (final Thread thread : Thread.getAllStackTraces().keySet()) { if (thread.isAlive() && thread.getName().startsWith("PostgreSQL JDBC driver connection thread")) { alive++; } } return alive; } /** * The deadline of the borrow stands for the whole borrow, so it bounds the attempt inside it * even where the per-attempt property gives it no bound of its own: turning that property off * must not turn the bound of the borrow off with it. */ @Test(timeOut = 300000) public void testTheDeadlineBoundsAnAttemptTheConnectPropertyDoesNot() throws Exception { System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "0"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "2"); try (final ServerSocket blackhole = new ServerSocket(0, 50, InetAddress.getLoopbackAddress())) { final String url = "jdbc:postgresql://127.0.0.1:" + blackhole.getLocalPort() + "/opendj?user=opendj&password=opendj"; final long startedAt = System.currentTimeMillis(); try { CachedConnection.getConnection(url); fail("a database that never answers must not hand out a connection"); } catch (SQLException expected) { // bounded by what is left of the deadline of the borrow } // the wait is the point of this one as much as its end is: a borrow against a socket that // never answers cannot be over before the deadline unless something else ended it final long elapsed = System.currentTimeMillis() - startedAt; assertTrue(elapsed >= 1000, "the borrow was over after " + elapsed + " ms, before its deadline"); assertElapsedWithinBound(startedAt, 2000); } } /** * The deadline stops the drain of the pool; it does not throw away the connection in hand. A * database at its connection limit has no other source of connections than the ones coming * back to the pool, and closing one unvalidated takes it out of that source for good - while * the borrow that closed it fails with a timeout anyway. */ @Test(timeOut = 120000) public void testAPooledConnectionIsNotDiscardedUnvalidatedAtTheDeadline() throws Exception { final String url = StubDriver.PREFIX + "unvalidated-at-deadline"; final Connection stale = mock(Connection.class); when(stale.isValid(anyInt())).thenAnswer(invocation -> { Thread.sleep(1500); // a database that no longer answers: the validation waits out its bound return false; }); final Connection good = mock(Connection.class); when(good.isValid(anyInt())).thenReturn(true); seedPool(url, stale, good); final Connection fresh = mock(Connection.class); stub.answerWith(fresh); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh, "the drain must stop at the deadline"); verify(stale).close(); verify(good, never()).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "a connection the deadline was reached in front of was lost"); } /** * A connection the validation of which failed is on its way out, and its driver knows it: * Connector/J answers a failed validation by aborting the connection and the SQL Server driver * by terminating it. Putting the previous bound back on it fails, and warns about statements * of a connection that is being closed - over an idle connection the server reaped, which is * nobody's problem. */ @Test(timeOut = 120000) public void testAConnectionOnItsWayOutIsNotGivenItsBoundBack() throws Exception { final String url = StubDriver.PREFIX + "reaped-idle"; final Connection reaped = mock(Connection.class); when(reaped.getNetworkTimeout()).thenReturn(0); when(reaped.isValid(anyInt())).thenReturn(false); seedPool(url, reaped); final Connection fresh = mock(Connection.class); stub.answerWith(fresh); assertSame(((CachedConnection) CachedConnection.getConnection(url)).parent, fresh); verify(reaped).setNetworkTimeout(any(Executor.class), eq(CachedConnection.VALIDATION_TIMEOUT_SECONDS * 1000)); verify(reaped, never()).setNetworkTimeout(any(Executor.class), eq(0)); verify(reaped).close(); } /** * The read bound of the login is lifted once the login is through, because left in place it * fails every statement slower than it. A driver that will not take it back leaves a * connection that must not be pooled: it would carry that bound into every borrow the pool * hands it to, an import batch among them. */ @Test(timeOut = 120000) public void testAConnectionStillCarryingTheBoundOfItsLoginIsNotPooled() throws Exception { final String url = StubDriver.PREFIX + "unliftable-bound"; final Connection parent = mock(Connection.class); doThrow(new SQLException("setNetworkTimeout is not supported")) .when(parent).setNetworkTimeout(any(Executor.class), eq(0)); stub.answerWith(parent); final CachedConnection.Pool pool = CachedConnection.poolOf(url); final CachedConnection borrowed = CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30, pool, false); borrowed.close(); verify(parent).close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 0, "a connection still carrying the read bound of its login went back into the pool"); } /** * What a connection carries once the login is through, where a deployment asked for a read * bound of its own: that bound rather than the bound of the login, which is a value nothing * slower than a connect is meant to be measured against. Without one, this is the lift above - * the behaviour of every connection this backend established before the property existed. */ @Test(timeOut = 120000) public void testAnEstablishedConnectionCarriesTheReadBoundAskedFor() throws Exception { final String url = StubDriver.PREFIX + "standing-read-bound"; CachedConnection.readTimeoutMillis = 90000; final Connection parent = mock(Connection.class); stub.answerWith(parent); CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30, CachedConnection.poolOf(url), false); verify(parent).setNetworkTimeout(any(Executor.class), eq(90000)); verify(parent, never()).setNetworkTimeout(any(Executor.class), eq(0)); } /** * And it carries it whether or not the login had a bound of its own to lift. The read bound of * a login is only ever set where the connect is bounded, so a deployment that runs with * {@code connect.timeout=0} - the one setting that leaves a connect to the deadline of the * borrow alone - would otherwise set this property and get nothing for it. */ @Test(timeOut = 120000) public void testTheReadBoundIsSetWhereTheLoginHadNoneToLift() throws Exception { final String url = StubDriver.PREFIX + "read-bound-without-a-login-bound"; CachedConnection.readTimeoutMillis = 90000; final Connection parent = mock(Connection.class); stub.answerWith(parent); CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 0, CachedConnection.poolOf(url), false); verify(parent).setNetworkTimeout(any(Executor.class), eq(90000)); } /** * A read bound standing in the connection string is the deployment's own: the connect does not * replace it with this one, exactly as it does not set the read bound of a login on top of it. */ @Test(timeOut = 120000) public void testAReadBoundOfTheUrlIsNotReplacedByTheConfiguredOne() throws Exception { final String url = StubDriver.PREFIX + "own-read-bound?socketTimeout=1000"; CachedConnection.readTimeoutMillis = 90000; final Connection parent = mock(Connection.class); stub.answerWith(parent); CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30, CachedConnection.poolOf(url), false); verify(parent, never()).setNetworkTimeout(any(Executor.class), anyInt()); } /** * Which connections carry a read bound of this backend's own making, as the backstop of * {@code JDBCStorage} has to know it: a statement of an unbounded class takes that bound off * for as long as it runs, and it may only take off what this class put on. A bound of the url * is the deployment's, and a driver whose property names are not known here was never given * one - lifting either would leave the connection unbounded for the rest of its life. */ @Test(timeOut = 120000) public void testOnlyTheReadBoundThisClassSetsIsItsOwnToLift() { CachedConnection.readTimeoutMillis = 90000; assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db"), 90000, "the bound this class sets on a connection of a dialect it knows"); assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db?socketTimeout=1000"), 0, "a read bound of the url was reported as this backend's own"); assertEquals(CachedConnection.standingReadBoundMillis("jdbc:h2:mem:db"), 0, "a driver this class sets no read bound on was reported as bounded by it"); CachedConnection.readTimeoutMillis = 0; assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db"), 0, "a connection carries no standing bound where none is configured"); } /** * The wait between two attempts of a connect: a millisecond, doubling to the ceiling and staying * there, so that a database refusing connections for a moment is asked again at once and one * refusing them for a minute is asked once a second rather than in a spin. Pinned here because * both loops that retry a connect of this backend wait on this schedule - the borrow of this * class and the catalog connect of {@code JDBCStorage.newCatalogConnection} - and a change to it * is a change to the pair (#929). */ @Test(timeOut = 120000) public void testTheBackoffOfARetriedConnectDoublesToItsCeiling() { // the schedule itself and not a property of it: "grows until it reaches the ceiling" is // answered by every factor there is - a schedule tripling from 1 reaches 1000 in eight steps // and grows at every one of them - and the factor is what the two loops share long backoffMs = 0; for (final long expected : new long[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1000, 1000 }) { final long previous = backoffMs; backoffMs = CachedConnection.nextBackoffMs(backoffMs); assertEquals(backoffMs, expected, "the wait of a retry left the doubling schedule after " + previous + "ms"); } assertEquals(backoffMs, CachedConnection.MAX_BACKOFF_MS, "the wait of a retry is not at its ceiling"); } /** * The warning about a read bound a driver would not take is throttled per consequence and not * once for the JVM, so that one connection does not report for another. *

* The two connections this happens to do not share a fate: a connection of the pool is closed * rather than pooled, while the one catalog connection of a backend is kept and carries the * bound of its login for the rest of its life (#929). The pool meets the failure on every * connect and the catalog once per open of the backend, so a single timestamp has the pool * silence the line about the catalog connection - and the line that did come out says the * connection was closed, of a connection that is still being read from. */ @Test(timeOut = 120000) public void testTheReadBoundWarningOfOneConnectionDoesNotSilenceAnother() { final long now = System.currentTimeMillis(); final String pooled = "a connection of this pool of testTheReadBoundWarning"; final String catalog = "the catalog connection of backend testTheReadBoundWarning"; CachedConnection.lastReadBoundWarning.remove(pooled); CachedConnection.lastReadBoundWarning.remove(catalog); assertTrue(CachedConnection.readBoundWarningDue(pooled, now), "the first line about a connection was not due"); assertTrue(CachedConnection.readBoundWarningDue(catalog, now), "a line about one connection silenced the line about another, which has no other line about it"); assertFalse(CachedConnection.readBoundWarningDue(pooled, now + 1), "the same consequence was reported twice inside one interval"); assertTrue(CachedConnection.readBoundWarningDue(pooled, now + CachedConnection.STALL_WARNING_INTERVAL_MS), "a connection carrying a bound it was never meant to keep was reported once and never again"); } /** * The bound is configured in seconds and reaches the driver in milliseconds; 0, a negative * value and a value that is no number all leave a connection unbounded, which is what this * backend did before the property existed. A value past the ceiling of a socket read timeout is * taken down to it rather than left to overflow the {@code int} of setNetworkTimeout, where it * would arrive as a negative timeout - a value outside the contract, and one a driver is free * to read as anything at all. */ @Test(timeOut = 120000) public void testTheReadBoundIsConfiguredInSeconds() { assertEquals(CachedConnection.getReadTimeoutMillis(), 0, "a connection is unbounded by default"); System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "90"); assertEquals(CachedConnection.getReadTimeoutMillis(), 90000); System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "0"); assertEquals(CachedConnection.getReadTimeoutMillis(), 0); System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "-1"); assertEquals(CachedConnection.getReadTimeoutMillis(), 0, "a negative value was not read as no bound"); System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "a minute and a half"); assertEquals(CachedConnection.getReadTimeoutMillis(), 0, "a value that is no number was not ignored in favour of the default"); System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, Integer.toString(Integer.MAX_VALUE)); assertEquals(CachedConnection.getReadTimeoutMillis(), JDBCStorage.MAX_BOUND_SECONDS * 1000, "a value past the ceiling of a socket read timeout was not taken down to it"); } /** * A driver that will not take the standing bound leaves a connection with no bound of ours on * it, which is what every connection of this pool carried before the property existed and no * reason to keep this one out of the pool. The connection that must not be pooled is the one * still carrying the read bound of its login: there the call that failed was a call to take * something off, and the bound left on it fails every statement slower than a connect. */ @Test(timeOut = 120000) public void testAConnectionThatWouldNotTakeTheStandingBoundIsStillPooled() throws Exception { final String url = StubDriver.PREFIX + "unsettable-standing-bound"; CachedConnection.readTimeoutMillis = 90000; final Connection parent = mock(Connection.class); doThrow(new SQLException("setNetworkTimeout is not supported")) .when(parent).setNetworkTimeout(any(Executor.class), anyInt()); stub.answerWith(parent); final CachedConnection.Pool pool = CachedConnection.poolOf(url); // Metered, and holding a permit of the pool as a borrow does: an unmetered connection is // closed rather than pooled whatever bound it carries, which would answer this on the // accounting of the pool instead of on the bound the case is about. assertTrue(pool.tryReserve(), "the pool of this url would not reserve a place for the connection"); final CachedConnection borrowed = CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 0, pool, true); borrowed.close(); verify(parent, never()).close(); assertEquals(pool.idleCount(), 1, "a connection carrying no bound of ours was kept out of the pool"); } /** * A read parameter of the url set to 0 is no bound of the deployment's: 0 is what every one of * these drivers reads as "wait as long as it takes", which is the default this property exists * to replace. It tells the two bounds apart, and only on postgresql, where a parameter of the * url outranks the property this class supplies: the login there is left carrying no bound of * ours, and rightly so, while the bound of this property is no property of a connect at all - it * is a setNetworkTimeout of an established connection, which no url outranks. Read as a bound of * theirs, a "socketTimeout=0" - the default of pgjdbc, written out - would leave a deployment * that asked for this one with no bound and no report of why. */ @Test(timeOut = 120000) public void testAReadParameterOfTheUrlSetToZeroIsNoBoundOfTheDeployments() { CachedConnection.readTimeoutMillis = 90000; assertEquals(CachedConnection.standingReadBoundMillis("jdbc:postgresql://localhost/db?socketTimeout=0"), 90000, "a postgresql url turning the read bound off was read as a bound of the deployment's own"); assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db?socketTimeout=0"), 90000, "a mysql url turning the read bound off was read as a bound of the deployment's own"); assertEquals(CachedConnection.standingReadBoundMillis("jdbc:postgresql://localhost/db?socketTimeout=30"), 0, "a read bound of a postgresql url is the deployment's own and stands"); } /** * The predicate deciding whether a url bounds the read reads the same set of names as the one * deciding whether it declares it. It used to stop at the first name present even where the * value there was a zero, so a url naming the bound under both names of the oracle driver - the * dotted one turned off, the last segment set - was declared() and not bounds(): the login kept * the administrator's value, because a property of ours is not supplied over a declared one, and * a bound of ours then went on top of it with setNetworkTimeout. Contrived, but "the bound taken * off is the bound that was set" holds only while the two look at the same names. */ @Test(timeOut = 120000) public void testAReadBoundUnderEitherNameOfTheUrlIsTheDeploymentsOwn() { CachedConnection.readTimeoutMillis = 90000; assertEquals(CachedConnection.standingReadBoundMillis( "jdbc:oracle:thin:@//localhost:1521/db?oracle.jdbc.ReadTimeout=0&ReadTimeout=600"), 0, "a bound standing under the last segment of the name was read as no bound at all"); assertEquals(CachedConnection.standingReadBoundMillis( "jdbc:oracle:thin:@//localhost:1521/db?oracle.jdbc.ReadTimeout=0&ReadTimeout=0"), 90000, "a url turning the read bound off under both of its names is no bound of the deployment's"); } /** * With nothing configured this is the lift and nothing else - the read bound of the login comes * off and no bound of ours goes on top of it, which is what every connection of this pool * carried before the property existed. The default of this property is what makes the change * that introduced it no change at all for a deployment that does not ask for one. */ @Test(timeOut = 120000) public void testTheDefaultTakesTheBoundOfTheLoginOffAndPutsNothingOnTopOfIt() throws Exception { final String url = StubDriver.PREFIX + "default-read-bound"; CachedConnection.readTimeoutMillis = 0; final Connection parent = mock(Connection.class); stub.answerWith(parent); CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30, CachedConnection.poolOf(url), false); verify(parent).setNetworkTimeout(any(Executor.class), eq(0)); verify(parent, times(1)).setNetworkTimeout(any(Executor.class), anyInt()); } /** * A driver that would take neither the standing bound nor the lift leaves the connection that * must not be pooled: what it is left carrying is the read bound of a connect, and every borrow * after this one would meet it - which is the case above, reached by the other of the two paths * that call for a setNetworkTimeout once the login is through. */ @Test(timeOut = 120000) public void testAConnectionThatWouldTakeNeitherTheStandingBoundNorTheLiftIsNotPooled() throws Exception { final String url = StubDriver.PREFIX + "unsettable-over-a-login-bound"; CachedConnection.readTimeoutMillis = 90000; final Connection parent = mock(Connection.class); doThrow(new SQLException("setNetworkTimeout is not supported")) .when(parent).setNetworkTimeout(any(Executor.class), anyInt()); stub.answerWith(parent); final CachedConnection.Pool pool = CachedConnection.poolOf(url); // Metered, and holding a permit of the pool as a borrow does: an unmetered connection is // closed rather than pooled whatever bound it carries, which would answer this on the // accounting of the pool instead of on the bound the case is about. assertTrue(pool.tryReserve(), "the pool of this url would not reserve a place for the connection"); final CachedConnection borrowed = CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30, pool, true); borrowed.close(); verify(parent).close(); assertEquals(pool.idleCount(), 0, "a connection still carrying the read bound of its login went back into the pool"); } /** * The initializer reads this property the way it reads the two windows above - a value that is * no number, or a negative one, is reported once and ignored in favour of the default - and * reporting it has to leave the class usable: the set that report is deduplicated through is * declared above every field whose initializer can reach it (JLS 12.4.2), so a field of this * one moved above that set would turn a typo in a property into an ExceptionInInitializerError * that no test of a class already initialized would ever meet. */ @Test(timeOut = 120000, dataProvider = "readBoundsWorthWarningAbout") public void testAReadBoundWorthWarningAboutStillInitializesTheClass(String configured) throws Exception { System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, configured); final Class reloaded = loadedAfresh(CachedConnection.class); assertNotSame(reloaded, CachedConnection.class, "the class under test was not loaded afresh"); final Field field = reloaded.getDeclaredField("readTimeoutMillis"); field.setAccessible(true); assertEquals(field.getInt(null), 0, "the bound the reloaded class settled on"); } @DataProvider public Object[][] readBoundsWorthWarningAbout() { return new Object[][]{{"a minute and a half"}, {"-1"}}; } /** * The case the window exists for: the connection this borrow takes out answered the database a * moment ago, and asking it again costs the round trip the operation came to make. */ @Test(timeOut = 120000) public void testAConnectionProvenAliveIsNotValidatedAgainWithinTheWindow() throws Exception { final String url = StubDriver.PREFIX + "within-the-window"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); final Connection first = CachedConnection.getConnection(url); // established: it has just answered first.close(); final Connection second = CachedConnection.getConnection(url); assertSame(second, first, "the pooled connection was not the one handed back"); assertEquals(stub.attempts.get(), 1, "the pool established a second connection"); verify(parent, never()).isValid(anyInt()); } /** Past the window it is the connection the database or a firewall may have dropped meanwhile. */ @Test(timeOut = 120000) public void testAConnectionIsValidatedAgainOnceTheWindowHasPassed() throws Exception { final String url = StubDriver.PREFIX + "past-the-window"; CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); final Connection first = CachedConnection.getConnection(url); first.close(); Thread.sleep(20); final Connection second = CachedConnection.getConnection(url); assertSame(second, first); verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); } /** The window switched off validates every borrow, the way this pool did before it existed. */ @Test(timeOut = 120000) public void testAWindowOfZeroValidatesEveryBorrow() throws Exception { final String url = StubDriver.PREFIX + "window-of-zero"; CachedConnection.aliveBypassNanos = 0; final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); final Connection first = CachedConnection.getConnection(url); first.close(); final Connection second = CachedConnection.getConnection(url); assertSame(second, first); verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); } /** * A connection is trusted for the window that follows the last answer it gave, never for the * window that follows its return to the pool. pgjdbc short-circuits both rollback() and * commit() when the transaction state is IDLE, so a borrow that issued no statement - the open * of a backend, a configuration change that leaves the base DNs alone, an import of nothing - * puts a connection back without a byte reaching the server: stamping the return would mark a * connection the database dropped meanwhile as the freshest one in the pool. */ @Test(timeOut = 120000) public void testTheReturnToThePoolIsNotTakenForProofOfLife() throws Exception { final String url = StubDriver.PREFIX + "silent-return"; CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(50); final Connection dropped = mock(Connection.class); when(dropped.isValid(anyInt())).thenReturn(false); // dropped while it was out of the pool stub.answerWith(dropped); final Connection borrowed = CachedConnection.getConnection(url); Thread.sleep(80); // the answer of the login ages out of the window borrowed.close(); // and the rollback of this return never leaves the driver final Connection fresh = mock(Connection.class); when(fresh.isValid(anyInt())).thenReturn(true); stub.answerWith(fresh); final Connection next = CachedConnection.getConnection(url); verify(dropped).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); verify(dropped).close(); assertSame(((CachedConnection) next).parent, fresh, "a connection the database dropped was handed out on the strength of its return to the pool"); } /** * A proof taken while the database was going away does not outlive the distrust that reported it. *

* The stamp stands for the moment the connection was asked, not the moment its answer was filed: * a validation is given {@link CachedConnection#VALIDATION_TIMEOUT_SECONDS}, and one that started * before another operation reported a drop and returned after it would otherwise be the younger of * the two. The connection would then be handed out unvalidated for the rest of the window - and * LIFO puts it at the head of the deque, so it is the very one the next borrow takes - by the * check that exists to stop exactly that. */ @Test(timeOut = 120000) public void testAProofTakenWhileTheDatabaseWentAwayIsNotTrusted() throws Exception { final String url = StubDriver.PREFIX + "proof-across-a-drop"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); // nothing here ages out of the window final AtomicInteger validations = new AtomicInteger(); final Connection parent = mock(Connection.class); // the database goes away while this validation is in flight: another operation of the backend // reports the drop of its own connection before this one has answered when(parent.isValid(anyInt())).thenAnswer(invocation -> { CachedConnection.distrustPool(url); validations.incrementAndGet(); return true; }); stub.answerWith(parent); CachedConnection.getConnection(url).close(); // established and returned, proven by its login CachedConnection.distrustPool(url); // an operation reports a drop: what the pool holds predates it CachedConnection.getConnection(url).close(); // validated, and a second drop is reported while it is final Connection borrowed = CachedConnection.getConnection(url); assertEquals(validations.get(), 2, "a connection was trusted on a proof that started before the drop it is compared against"); assertSame(((CachedConnection) borrowed).parent, parent, "the connection answered and was still discarded"); } /** * The pool hands out the connection returned last. Without it the window would rarely apply: a * connection reached only after a whole cycle of the pool has been idle far longer than it. */ @Test(timeOut = 120000) public void testTheConnectionReturnedLastIsBorrowedFirst() throws Exception { final String url = StubDriver.PREFIX + "returned-last"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection older = mock(Connection.class); when(older.isValid(anyInt())).thenReturn(true); stub.answerWith(older); final Connection first = CachedConnection.getConnection(url); final Connection newer = mock(Connection.class); when(newer.isValid(anyInt())).thenReturn(true); stub.answerWith(newer); final Connection second = CachedConnection.getConnection(url); assertNotSame(second, first); first.close(); second.close(); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, newer, "the pool cycled round to its coldest connection"); } /** * Whatever dropped one connection - a restart, a failover, a network that went away - dropped * every connection established before it, and a borrow inside the window asks the database * nothing: so the operation that saw the failure tells the pool, and the rest of that * generation is validated once before it is trusted again. */ @Test(timeOut = 120000) public void testThePoolIsValidatedAgainAfterTheDatabaseDroppedAConnection() throws Exception { final String url = StubDriver.PREFIX + "distrusted-generation"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); CachedConnection.getConnection(url).close(); CachedConnection.distrustPool(url); final Connection next = CachedConnection.getConnection(url); next.close(); CachedConnection.getConnection(url).close(); // once for the generation the drop condemned, and not again for the borrow behind it verify(parent, times(1)).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); } /** * The whole of the trade the window makes, end to end: a connection the database dropped * inside the window is handed out unvalidated - that is the cost - the operation it broke * reports the drop, and from there the pool validates the generation the drop condemned * instead of handing out the rest of it the same way. */ @Test(timeOut = 120000) public void testAConnectionDroppedInsideTheWindowIsHandedOutOnceAndThenValidated() throws Exception { final String url = StubDriver.PREFIX + "dropped-inside-the-window"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); CachedConnection.getConnection(url).close(); when(parent.isValid(anyInt())).thenReturn(false); // the database dropped it where it lay final Connection dropped = CachedConnection.getConnection(url); assertSame(((CachedConnection) dropped).parent, parent, "the pooled connection was not the one handed back"); verify(parent, never()).isValid(anyInt()); // handed out on the strength of its last answer // the statement of the caller is where the drop surfaces, and the caller reports it CachedConnection.distrustPool(url); dropped.close(); final Connection fresh = mock(Connection.class); when(fresh.isValid(anyInt())).thenReturn(true); stub.answerWith(fresh); final Connection next = CachedConnection.getConnection(url); verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); verify(parent).close(); assertSame(((CachedConnection) next).parent, fresh, "the rest of the generation was handed out unvalidated"); } /** * Seeds the pool the way {@link CachedConnection#close()} fills it - at the end a borrow takes * from - so that the connection named first here is the one the next borrow gets. */ private static void seedPool(String url, Connection... parents) { final CachedConnection.Pool pool = CachedConnection.poolOf(url); for (int i = parents.length - 1; i >= 0; i--) { pool.addIdle(new CachedConnection(url, parents[i])); } } /** * The borrows nothing compensates a dropped connection on - the open of a backend, the removal * of its files, the start of an import - ask for a connection the pool validates whatever the * window says. Each of them is one borrow of a cold path, and the one that opens a backend * issues no statement at all: a connection dropped inside the window would surface there out of * the rollback that releases it, with no statement to replay and nothing to tell the pool. */ @Test(timeOut = 120000) public void testTheBorrowsNothingCompensatesAreValidated() throws Exception { final String url = StubDriver.PREFIX + "validated-borrow"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); CachedConnection.getConnection(url).close(); final Connection borrowed = CachedConnection.getConnection(url, false); assertSame(((CachedConnection) borrowed).parent, parent, "the pooled connection was not the one handed back"); verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS); } /** * A connection closed under the borrow is not handed out on the strength of its last answer: * the removal listener of the pool closes every connection it finds in the deque when the pool * expires, and it iterates a weakly consistent view. The validation the window replaces * answered that as well, out of a flag of the driver rather than out of a round trip. */ @Test(timeOut = 120000) public void testAConnectionThePoolClosedIsNotHandedOut() throws Exception { final String url = StubDriver.PREFIX + "closed-inside-the-window"; CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); final Connection parent = mock(Connection.class); when(parent.isValid(anyInt())).thenReturn(true); stub.answerWith(parent); CachedConnection.getConnection(url).close(); // closed where it lay, by the expiry of the pool: a closed connection answers isValid() with // false as well, which is what discards it once the window stops trusting it when(parent.isClosed()).thenReturn(true); when(parent.isValid(anyInt())).thenReturn(false); final Connection fresh = mock(Connection.class); when(fresh.isValid(anyInt())).thenReturn(true); stub.answerWith(fresh); final Connection borrowed = CachedConnection.getConnection(url); assertSame(((CachedConnection) borrowed).parent, fresh, "a closed connection was handed out on its last answer"); } /** * The window is clamped twice: to the idle time the pool keeps a connection for, since a window * longer than that is one the pool can never back - the connection it was meant for is gone * before it closes - and to {@link CachedConnection#MAX_ALIVE_BYPASS_MS} behind it, since the * ttl has no upper bound of its own and a value the unit conversion saturates on would leave * every connection of the pool trusted for the life of the server. *

* About the value the class settles on at initialization: the field the pool reads is assigned * once, so a ttl set after that changes neither it nor the idle time it was clamped to. */ @Test(timeOut = 120000) public void testTheWindowIsClampedToTheIdleTimeOfThePool() { System.setProperty(CachedConnection.TTL_PROPERTY, "15000"); System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, "500"); assertEquals(CachedConnection.getAliveBypassMillis(), 500L, "a window inside the ttl was not left alone"); System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE)); assertEquals(CachedConnection.getAliveBypassMillis(), 15000L, "a window of Long.MAX_VALUE was not clamped"); System.setProperty(CachedConnection.TTL_PROPERTY, "100"); assertEquals(CachedConnection.getAliveBypassMillis(), 100L, "the clamp is the configured ttl, not the default"); // the ttl has no upper bound of its own, so the clamp to it does not bound the window either: both set // to a value the conversion to nanoseconds saturates on would leave every connection of the pool // trusted for the life of the server, which is the outcome this javadoc says the clamp rules out System.setProperty(CachedConnection.TTL_PROPERTY, Long.toString(Long.MAX_VALUE)); System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE)); assertEquals(CachedConnection.getAliveBypassMillis(), CachedConnection.MAX_ALIVE_BYPASS_MS, "a window the ttl did not bound was left to saturate"); } /** * A setting worth warning about must leave the class usable. Both properties are read by the * initializer of {@code aliveBypassNanos}, and both report an unusable value through the set of * what has been said once already - which, declared below that initializer, would still be null * when the initializer reaches it (JLS 12.4.2). A window longer than the ttl is exactly the * tuning the javadoc of the property invites, and it would turn a log line into an * ExceptionInInitializerError on the first borrow and a causeless NoClassDefFoundError on every * one after it: no connection can be borrowed, so the backend cannot open at all. *

* Asserted on the class loaded afresh rather than on this one, which was initialized long * before the property was set. */ @Test(timeOut = 120000, dataProvider = "settingsWorthWarningAbout") public void testASettingWorthWarningAboutStillInitializesTheClass(String ttl, String window, long expectedMillis) throws Exception { System.setProperty(CachedConnection.TTL_PROPERTY, ttl); System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, window); final Class reloaded = loadedAfresh(CachedConnection.class); assertNotSame(reloaded, CachedConnection.class, "the class under test was not loaded afresh"); final Field field = reloaded.getDeclaredField("aliveBypassNanos"); field.setAccessible(true); assertEquals(field.getLong(null), TimeUnit.MILLISECONDS.toNanos(expectedMillis), "the window the reloaded class settled on"); } @DataProvider public Object[][] settingsWorthWarningAbout() { return new Object[][]{ // a window longer than the ttl: reported once and used as the ttl {"15000", "60000", 15000L}, // not a number, and negative: reported once and ignored in favour of the default {"15000", "half a second", CachedConnection.DEFAULT_ALIVE_BYPASS_MS}, {"15000", "-1", CachedConnection.DEFAULT_ALIVE_BYPASS_MS}, // the ttl is read by the same initializer, and reports its own value the same way {"30s", "500", 500L} }; } /** * The class again, defined by a loader of this test rather than taken from the one that has * already initialized it: a static initializer runs once per loader, and this is about what it * does. Every other class is delegated to the parent, so the reloaded one shares the types it * is written against. */ private static Class loadedAfresh(Class type) throws Exception { final String name = type.getName(); final ClassLoader parent = type.getClassLoader(); final ClassLoader loader = new ClassLoader(parent) { @Override protected Class loadClass(String candidate, boolean resolve) throws ClassNotFoundException { if (!name.equals(candidate)) { return super.loadClass(candidate, resolve); } Class defined = findLoadedClass(candidate); if (defined == null) { final byte[] bytecode = bytecodeOf(candidate, parent); defined = defineClass(candidate, bytecode, 0, bytecode.length); } if (resolve) { resolveClass(defined); } return defined; } }; return Class.forName(name, true, loader); } private static byte[] bytecodeOf(String name, ClassLoader from) throws ClassNotFoundException { try (final InputStream in = from.getResourceAsStream(name.replace('.', '/') + ".class")) { if (in == null) { throw new ClassNotFoundException(name); } final ByteArrayOutputStream bytecode = new ByteArrayOutputStream(); final byte[] chunk = new byte[8192]; for (int read; (read = in.read(chunk)) >= 0; ) { bytecode.write(chunk, 0, read); } return bytecode.toByteArray(); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } private static SQLException tooManyConnections() { // 53300, too_many_connections, of the insufficient_resources class return new SQLException("sorry, too many clients already", "53300"); } /** A connection string of every dialect pointing at one host and port. */ private static String[] urlsOf(int port) { return new String[]{ "jdbc:postgresql://127.0.0.1:" + port + "/opendj?user=opendj&password=opendj", "jdbc:mysql://127.0.0.1:" + port + "/opendj?user=opendj&password=opendj", "jdbc:oracle:thin:opendj/opendj@//127.0.0.1:" + port + "/free", "jdbc:sqlserver://127.0.0.1:" + port + ";databaseName=opendj;user=opendj;password=opendj;encrypt=false" }; } private static int closedPort() throws Exception { try (final ServerSocket socket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) { return socket.getLocalPort(); } // closed again: nothing listens there any more } /** * A socket that answers a connect and closes it at once. A port bound and released is the * shape of a refused connect a test would reach for, but it races whatever else on the host * may take that port; this one is the failure it stands for and belongs to nobody else. */ private static ServerSocket rejectingSocket() throws Exception { final ServerSocket socket = new ServerSocket(0, 50, InetAddress.getLoopbackAddress()); final Thread accepting = new Thread(() -> { while (!socket.isClosed()) { try { socket.accept().close(); } catch (Exception closed) { return; } } }, "opendj-test-rejecting-socket"); accepting.setDaemon(true); accepting.start(); return socket; } // The margin grows with the bound instead of dwarfing it: what this has to catch is a bound // that is not in force, and a bound of 2 s that is not in force is not a wait of 12 s - it is // the 30 s of the connect property, the 600 s of a driver, or no end at all. private static void assertElapsedWithinBound(long startedAt, long boundMs) { final long elapsed = System.currentTimeMillis() - startedAt; final long margin = Math.max(BOUND_MARGIN_MS, boundMs); assertTrue(elapsed < boundMs + margin, "gave up only after " + elapsed + " ms, past the " + boundMs + " ms it was bounded by"); } /** * Stands in for a database whose answer to a connect is the point of the test: the vendor * codes and SQL states below are what the retry has to tell apart, and no engine is needed to * produce them. */ private static final class StubDriver implements Driver { static final String PREFIX = "jdbc:opendj-stub:"; static final int ALWAYS = -1; final AtomicInteger attempts = new AtomicInteger(); /** A SQLException, or the unchecked failure a driver is free to throw at DriverManager instead. */ private volatile Throwable failure; private volatile int failuresLeft; private volatile Connection answer; void failWith(Throwable failure, int times) { this.failure = failure; this.failuresLeft = times; this.answer = null; this.attempts.set(0); } void answerWith(Connection answer) { this.failure = null; this.failuresLeft = 0; this.answer = answer; this.attempts.set(0); } @Override public Connection connect(String url, Properties info) throws SQLException { if (!acceptsURL(url)) { return null; } attempts.incrementAndGet(); if (failuresLeft != 0) { if (failuresLeft > 0) { failuresLeft--; } if (failure instanceof SQLException) { throw (SQLException) failure; } throw (RuntimeException) failure; } if (answer != null) { return answer; } final Connection con = mock(Connection.class); when(con.isValid(anyInt())).thenReturn(true); return con; } @Override public boolean acceptsURL(String url) { return url != null && url.startsWith(PREFIX); } @Override public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { return new DriverPropertyInfo[0]; } @Override public int getMajorVersion() { return 1; } @Override public int getMinorVersion() { return 0; } @Override public boolean jdbcCompliant() { return false; } @Override public Logger getParentLogger() { return Logger.getLogger(StubDriver.class.getName()); } } }