/* * 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.AccessMode; import org.opends.server.backends.pluggable.spi.Importer; import org.opends.server.backends.pluggable.spi.StorageRuntimeException; import org.opends.server.backends.pluggable.spi.TreeName; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.DriverPropertyInfo; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.logging.Logger; import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; 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.assertTrue; import static org.testng.Assert.fail; /** * The {@link Importer} of the JDBC backend is used by several threads at once - phase two of an * import runs one thread per tree - and {@code java.sql.Connection} is not thread-safe. Sharing * one connection between those threads corrupts the driver rather than merely serializing the * work: the sql server driver keeps the reconnect listeners of a connection in a plain * {@code ArrayList} that every {@code prepareStatement()} and every statement {@code close()} * mutates, and two import threads on one connection walked it past the end of its array * (issue #891). *

* Needs no database: the connections are handed out by a driver of this test, which records every * thread inside a statement of a connection - and which thread commits, rolls back or returns one * while another is in there - so what it records is the defect itself, two threads on one * connection at the same time, rather than an exception of one driver. It therefore holds for every * dialect this backend takes. *

* Two ways of getting the threads to overlap, because trees that get connections of their own and * trees that share one cannot be asked the same question. Where the connections differ, the driver * holds the first statement of each thread at a rendezvous until its peers have one in flight of * their own. Where they do not, a second thread never reaches such a rendezvous - the monitor of * the connection is what this is about - so the threads line up before they write instead, and the * first one in holds the connection for a moment to see whether the other turns up beside it. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "jdbc" }, sequential = true) public class ImportConnectionsTestCase extends DirectoryServerTestCase { /** Long enough for a peer thread to reach a statement on a loaded machine, and no longer. */ private static final long RENDEZVOUS_SECONDS = 30; /** * How long the first thread inside a statement holds the connection it is on. Paid in full by * every run where the importer serializes its threads as it should - which is the point: a peer * that is let in arrives at once, having been waiting on the monitor since the starting line. */ private static final long HOLD_SECONDS = 2; /** How long a close of an import may take before a thread of the test is taken to be stuck. */ private static final long CLOSE_SECONDS = 30; /** * How long a thread parked inside a statement stays in there before it gives up on the test * that parked it. Paid by no passing run - the test lets it go as soon as it has what it came * for - and it is bounded so that a run which never gets there is a red test rather than a * build that hangs on a monitor nobody is going to release. */ private static final long PARK_SECONDS = 30; private static final TreeName ID2ENTRY = new TreeName("dc=example,dc=com", "id2entry"); private static final TreeName DN2ID = new TreeName("dc=example,dc=com", "dn2id"); private static final TreeName STATE = new TreeName("dc=example,dc=com", "state"); private final StubDriver stub = new StubDriver(); /** * What the driver of this test answers a connect with, or null for a connection of its own. * A failure per connect rather than one instance thrown over and over: a caller is free to * give what it caught a cause and a suppressed exception of its own. */ private final AtomicReference> refusal = new AtomicReference<>(); /** The connections an import issued a statement on, in no particular order. */ private final Set used = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap())); /** The same, in the order the import first issued a statement on them. */ private final List usedInOrder = Collections.synchronizedList(new ArrayList()); /** Every commit of the import, in the order the connections took them. */ private final List commits = Collections.synchronizedList(new ArrayList()); /** * The threads inside a statement of a connection right now - every one of them, rather than the * first to arrive: a thread that finds a peer there records the pair and stays out of the map, * and the peer's exit would then leave the map saying the connection is free while it is not. * What comes after - another statement, a commit, the rollback of a return - would be recorded * against nobody, which is the one thing this suite is here to notice. */ private final Map> inStatement = Collections.synchronizedMap(new IdentityHashMap>()); /** Every pair of threads that was inside a statement of one connection at the same time. */ private final List shared = Collections.synchronizedList(new ArrayList()); /** Where the threads of a test meet, so that their statements are in flight at the same time. */ private volatile CyclicBarrier rendezvous; /** Whether this thread has already met its peers: a thread waits there once, whatever it issues after. */ private final ThreadLocal met = ThreadLocal.withInitial(() -> Boolean.FALSE); /** * Where the threads of a test line up before they write, which is in front of every monitor the * importer takes - unlike {@link #rendezvous}, which they reach once they are already inside a * statement, and which two threads sharing a connection therefore cannot both reach. */ private volatile CyclicBarrier startingLine; /** * Counted down by every thread that gets inside a statement, and waited on by the first of them: * it holds the connection long enough for a peer to get in there beside it, if anything lets it. */ private volatile CountDownLatch insideAStatement; /** Whether this thread has already held a connection: it holds the first statement it issues. */ private final ThreadLocal held = ThreadLocal.withInitial(() -> Boolean.FALSE); /** * Counted down by the first thread of a test to get inside a statement, and awaited by the test * itself: what a write in flight looks like to the rest of the import - a connection whose * statement has not come back, and which therefore has nothing recorded against it yet. */ private volatile CountDownLatch insideAStatementNow; /** Counted down by the test to let the parked write finish, once it has seen what it came to see. */ private volatile CountDownLatch letTheStatementFinish; /** Whether this thread has already been parked: a thread is parked in the first statement it issues. */ private final ThreadLocal parked = ThreadLocal.withInitial(() -> Boolean.FALSE); private ExecutorService threads; /** The pool bound and its borrow deadline as this JVM had them, put back after every test. */ private String poolMaxOfTheJvm; private String poolTimeoutOfTheJvm; @BeforeClass public void registerStubDriver() throws Exception { DriverManager.registerDriver(stub); } /** * The bound of the pools of this suite, pinned: how many connections an import takes is clamped * to it, so a value another suite of this package left set - CachedConnectionTestCase varies it * - would decide the counts asserted here. A pool reads it once, when the first borrow of a * connection string creates it, so it has to stand before the storage of a test is opened. */ @BeforeMethod public void pinThePoolBound() { poolMaxOfTheJvm = System.getProperty(CachedConnection.POOL_MAX_PROPERTY); poolTimeoutOfTheJvm = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY); System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "16"); } @AfterClass public void deregisterStubDriver() throws Exception { DriverManager.deregisterDriver(stub); } @AfterMethod public void forgetWhatTheTestRecorded() { System.clearProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY); // put back rather than cleared: a sibling suite of this package varies this property, and a // pool is created with the value that stands when its connection string is first borrowed on putBack(CachedConnection.POOL_MAX_PROPERTY, poolMaxOfTheJvm); putBack(CachedConnection.POOL_TIMEOUT_PROPERTY, poolTimeoutOfTheJvm); // the rendezvous of the next test is not the one this thread already went to met.remove(); held.remove(); parked.remove(); // let go of anything this test left parked before its threads are shut down: a write held // inside a statement holds the monitor of a connection, and the next test of this suite // starts by opening a storage on that pool if (letTheStatementFinish != null) { letTheStatementFinish.countDown(); } if (threads != null) { threads.shutdownNow(); threads = null; } refusal.set(null); rendezvous = null; startingLine = null; insideAStatement = null; insideAStatementNow = null; letTheStatementFinish = null; forgetTheConnections(); inStatement.clear(); shared.clear(); } private static void putBack(String property, String value) { if (value == null) { System.clearProperty(property); } else { System.setProperty(property, value); } } private void forgetTheConnections() { used.clear(); usedInOrder.clear(); commits.clear(); } /** * Two trees written at the same time are written through connections of their own. This is the * contract {@code Importer} states and the defect of #891: every thread of phase two wrote * through the one connection the importer borrowed in its constructor. */ @Test(timeOut = 120000) public void testTwoTreesAreWrittenThroughConnectionsOfTheirOwn() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-parallel"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { // both threads have to be inside a statement at once for the record below to say // anything: a run where one finished before the other started shares no connection // however the importer borrows them rendezvous = new CyclicBarrier(2); threads = daemonThreads(2, "import-writer"); final Future id2entry = put(importer, ID2ENTRY); final Future dn2id = put(importer, DN2ID); id2entry.get(RENDEZVOUS_SECONDS * 2, TimeUnit.SECONDS); dn2id.get(RENDEZVOUS_SECONDS * 2, TimeUnit.SECONDS); // belt and braces here rather than the cover of this suite: with a connection each // the record below is empty however the import behaves, and a run where the two // trees did share one fails at the rendezvous instead - the second thread would be // on the monitor of the connection and never reach it. What pins the monitor is // testTwoTreesOnOneConnectionAreNotWrittenAtTheSameTime assertEquals(shared, Collections.emptyList(), "two import threads issued a statement on one connection at the same time"); assertEquals(used.size(), 2, "the two trees of the import did not get a connection each"); }); } finally { storage.close(); } } /** * Two trees that share one connection are not written at the same time. The bound of an import, * and a pool with nothing to spare, both put several trees on one connection - and what #891 is * about is two threads issuing statements on one connection, not how many connections an import * holds: the monitor of the connection is what has to keep the second thread out. *

* The two threads cannot be brought together at a barrier inside their statements the way the * test above does it - the second one never gets in to reach it - so the first one in holds the * connection for {@link #HOLD_SECONDS} instead, having lined up with its peer beforehand. A * second thread let in beside it arrives at once, since it has been waiting on that monitor * since the starting line, and both are then recorded in {@link #shared}. */ @Test(timeOut = 120000) public void testTwoTreesOnOneConnectionAreNotWrittenAtTheSameTime() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "1"); // both trees on one connection final String url = StubDriver.PREFIX + "importer-serialized"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { startingLine = new CyclicBarrier(2); insideAStatement = new CountDownLatch(2); threads = daemonThreads(2, "import-writer"); final Future id2entry = put(importer, ID2ENTRY); final Future dn2id = put(importer, DN2ID); id2entry.get(RENDEZVOUS_SECONDS * 2, TimeUnit.SECONDS); dn2id.get(RENDEZVOUS_SECONDS * 2, TimeUnit.SECONDS); assertEquals(used.size(), 1, "the two trees of this import did not share one connection"); assertEquals(shared, Collections.emptyList(), "two import threads issued a statement on one connection at the same time"); }); } finally { storage.close(); } } /** * ... and no more connections than that: an import takes them for its whole duration, and the * pool they come from is bounded and shared with the operations of every other backend on that * database (#878). One thread per tree is what phase two runs, and a default backend has trees * enough to empty a default pool. */ @Test(timeOut = 120000) public void testTreesShareTheConnectionsOfABoundAboveOne() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-bounded-two"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { // five trees over two connections: the bound is what the round robin wraps at importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(new TreeName("dc=example,dc=com", "id2childrencount"), ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(new TreeName("dc=example,dc=com", "referral"), ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); assertEquals(used.size(), 2, "the import did not spread its trees over the connections of its bound"); } finally { importer.close(); storage.close(); } } /** * The bound an import takes where the operator set none, which is the branch every real install * takes: half the bound of the pool, and no more than half of what the default bound would be. * An import holds what it takes for its whole duration out of a pool that is shared with the * LDAP traffic of every backend on that database (#878), so the deployment that raised the * bound of its pool did not thereby ask for an import to take half of it. */ @Test(timeOut = 120000) public void testTheBoundOfAnImportDefaultsToHalfThePool() throws Exception { // set by no test of this suite, and cleared after every one of them: this is the default System.clearProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY); // a bound of the pool that is not the default one: DEFAULT_POOL_MAX is // max(16, processors * 2), so the 16 pinThePoolBound() puts on the pools of this suite is // the default itself on a machine of eight cores or fewer - and half the pool and half the // default are then the same number, which is no test of which of the two an import halves System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "6"); final JDBCStorage half = importingStorage(StubDriver.PREFIX + "importer-default-bound"); try { assertEquals(half.importConnections(), 3, "an import did not default to half the bound of the pool"); } finally { half.close(); } System.setProperty(CachedConnection.POOL_MAX_PROPERTY, String.valueOf(CachedConnection.DEFAULT_POOL_MAX * 4)); final JDBCStorage capped = importingStorage(StubDriver.PREFIX + "importer-default-bound-large-pool"); try { assertEquals(capped.importConnections(), CachedConnection.DEFAULT_POOL_MAX / 2, "an import of a pool larger than the default took more than half of what the default bound would be"); } finally { capped.close(); } } /** ... and a bound the pool cannot honour is the pool's, not the property's. */ @Test(timeOut = 120000) public void testTheBoundOfAnImportIsClampedToTheBoundOfThePool() throws Exception { System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "2"); System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "8"); final String url = StubDriver.PREFIX + "importer-clamped"; final JDBCStorage storage = importingStorage(url); try { assertEquals(storage.importConnections(), 2, "the bound of an import was not clamped to the pool"); } finally { storage.close(); } } @Test(timeOut = 120000) public void testTheConnectionsOfAnImportAreBounded() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "1"); final String url = StubDriver.PREFIX + "importer-bounded"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); assertEquals(used.size(), 1, "the import took more connections than the bound allows"); } finally { importer.close(); storage.close(); } } /** * The trees decide which connection a write goes through, not the threads that write them. The * connections of an import are transactions of their own, so two of them writing one row would * have the second wait for the first to commit - which an import does in {@code close()}, when * the thread that would have to release the row is long done, and the bulk class carries no * bound to break the wait. {@code setTrust()} writes the state tree of a container from an * import thread in {@code beforePhaseOne} and from the closing thread in {@code afterPhaseTwo}, * which is exactly that shape. */ @Test(timeOut = 120000) public void testOneTreeIsWrittenThroughOneConnectionWhoeverWritesIt() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "4"); final String url = StubDriver.PREFIX + "importer-one-tree"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { // three threads, one tree: an import thread of phase one, another of phase two, and // the thread that closes the importer, which is the one afterPhaseTwo runs on putOnAThreadOfItsOwn(importer, STATE); putOnAThreadOfItsOwn(importer, STATE); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); assertEquals(used.size(), 1, "one tree was written through more than one connection"); }); } finally { storage.close(); } } /** * Every connection an import took is committed and given back, not only the one its * constructor borrowed: what a connection left behind holds is a transaction of the import and * a permit of the pool, and a pool is never removed from the map - so a permit lost to an * import is lost for the life of the server (#878). */ @Test(timeOut = 120000) public void testEveryConnectionOfAnImportIsCommittedAndReturned() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "3"); final String url = StubDriver.PREFIX + "importer-returned"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.close(); assertEquals(used.size(), 3, "the three trees of the import did not get a connection each"); for (final Connection con : connectionsUsed()) { verify(con, times(1)).commit(); } assertEquals(CachedConnection.poolOf(url).idleCount(), 3, "an import kept a connection of the pool"); } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "an import kept a permit of the pool"); } /** * A clear commits every connection of the import, not only the one whose tree it clears, and it * does so before it empties anything. The one connection an import used to hold made that so of * its own accord - {@code clearTree()} ends in a commit, and that commit made durable every * write the import had made so far. What that is worth to a {@code rebuild-index} is the * {@code setTrust(false)} of {@code RebuildIndexStrategy.beforePhaseOne}, which is written just * in front of the clear of the tree it describes: left to {@code close()} that flag would still * be uncommitted while the table was emptied and committed, and a server that stopped in * between would come back to an index that is empty and marked trusted. */ @Test(timeOut = 120000) public void testAClearCommitsTheOtherConnectionsOfTheImport() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-clear-commits"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { // the state tree stands for the trust flag: written, and left uncommitted until // something commits the connection it went to importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.clearTree(ID2ENTRY); // asserted in order rather than counted: the destructive half of a clear is the one // thing that must not be durable while a write it is meant to invalidate is not, so the // commit of the peer belongs in front of the one the clear itself ends in - a count of // two is the same whichever way round they happened assertEquals(commits, Arrays.asList(usedInOrder.get(0), usedInOrder.get(1)), "the clear did not commit the other connection of the import before it emptied its own tree"); } finally { importer.close(); storage.close(); } } /** * ... including a peer whose first write is still in flight, which is not a connection with * nothing to commit but one whose write has not come back yet. *

* An import counts a write once its statement returns, so a connection written for the first * time has nothing recorded against it for exactly as long as that statement takes. That window * is reached by more than one thread at once: {@code beforePhaseOne} runs on the import threads * - {@code OnDiskMergeImporter.processEntry} guards it with a latch per container, not one for * all of them - so on an {@code import-ldif} of two base DNs the {@code setTrust(false)} of one * container is written while the clears of the next are running. The one connection an import * held before #891 carried such a write into the commit of the clear, being the same * transaction and issued in front of it in the driver, so a clear that passed over it here * would be a commit point the single connection did not have. */ @Test(timeOut = 120000) public void testAClearCommitsAPeerWhoseWriteIsStillInFlight() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-clear-commits-in-flight"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { insideAStatementNow = new CountDownLatch(1); letTheStatementFinish = new CountDownLatch(1); // the state tree stands for the trust flag of beforePhaseOne: written on a thread of // the import, and held inside the statement it issues final AtomicReference flagFailure = new AtomicReference<>(); final Thread writing = daemon(() -> { try { importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); } catch (Throwable t) { flagFailure.set(t); } }, "import-flag"); // the clear of the next container, on a connection of its own final AtomicReference clearFailure = new AtomicReference<>(); final Thread clearing = daemon(() -> { try { importer.clearTree(ID2ENTRY); } catch (Throwable t) { clearFailure.set(t); } }, "import-clear"); try { writing.start(); assertTrue(insideAStatementNow.await(RENDEZVOUS_SECONDS, TimeUnit.SECONDS), "the write of the trust flag never got inside a statement"); // where the clear gets to on its own: waiting on the monitor of the peer, which // is the commit point being paid for - a clear that decided the peer had nothing // to commit runs through to the end instead clearing.start(); untilBlockedOrDone(clearing); } finally { letTheStatementFinish.countDown(); } writing.join(TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS)); clearing.join(TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS)); reportFailureOf("the write of the trust flag", flagFailure); reportFailureOf("the clear", clearFailure); assertFalse(writing.isAlive(), "the write of the trust flag did not finish"); assertFalse(clearing.isAlive(), "the clear did not finish"); assertEquals(usedInOrder.size(), 2, "the two trees of this import did not get a connection each"); assertEquals(commits, Arrays.asList(usedInOrder.get(0), usedInOrder.get(1)), "the clear passed over a peer whose write was still in flight"); }); } finally { storage.close(); } } /** * The connection of the last write is committed last. The connections of an import are * transactions of their own, so {@code close()} cannot commit them as one - and the last thing * an import writes is the flag that says the rest of it is good ({@code setTrust(true)} of * {@code afterPhaseTwo}). Committed last, that flag is rolled back with the connection it is on * whenever an earlier commit fails, so a failed import cannot leave an index marked trusted * over data that never got there. */ @Test(timeOut = 120000) public void testTheConnectionsAreCommittedInTheOrderTheyWereLastWrittenTo() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "3"); final String url = StubDriver.PREFIX + "importer-last-write"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); // the two writes that stand for the trust flags afterPhaseTwo writes, one per base DN: // both have to be committed after the connection that holds nothing but data importer.put(ID2ENTRY, ByteString.valueOfUtf8("k2"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k2"), ByteString.valueOfUtf8("v")); importer.close(); assertEquals(usedInOrder.size(), 3, "the three trees of the import did not get a connection each"); assertEquals(commits, Arrays.asList(usedInOrder.get(2), usedInOrder.get(0), usedInOrder.get(1)), "the connections were not committed in the order they were last written to"); } finally { storage.close(); } } /** * {@code close()} waits for a statement of a straggler to finish rather than committing the * connection beside it. *

* Phase two gives its threads five seconds to answer an interrupt and closes the importer * whether they answered or not ({@code OnDiskMergeImporter.invokeParallel}), so a write of an * import can still be in flight when its connections are committed and handed back. A commit * issued beside that statement is two threads on one connection - the whole of #891, with the * closing thread on one side of it. Every other test of this suite joins its writers before it * closes, so this is the only one where anything of a close happens beside a statement, and it * is what the other half of the detector is for: the one that watches a commit, a rollback or a * return land while a thread is inside a statement. *

* The straggler is let go once the close has got as far as it can on its own, and its write is * then committed rather than lost: a thread that was inside the import when {@code close()} * began is part of it. That is the assertion which bites - a close that reads the state of a * connection without waiting for the statement in flight on it finds nothing written and * commits nothing, and the write of the straggler is rolled back by the return instead. */ @Test(timeOut = 120000) public void testACloseWaitsForAStatementOfAStragglerRatherThanCommittingBesideIt() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-close-waits"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { insideAStatementNow = new CountDownLatch(1); letTheStatementFinish = new CountDownLatch(1); final AtomicReference stragglerFailure = new AtomicReference<>(); final Thread straggling = daemon(() -> { try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); } catch (Throwable t) { stragglerFailure.set(t); } }, "import-straggler"); final AtomicReference closeFailure = new AtomicReference<>(); final Thread closing = daemon(() -> { try { importer.close(); } catch (Throwable t) { closeFailure.set(t); } }, "import-close-beside-a-statement"); try { straggling.start(); assertTrue(insideAStatementNow.await(RENDEZVOUS_SECONDS, TimeUnit.SECONDS), "the write of the straggler never got inside a statement"); // where the close gets to on its own: waiting on the monitor of the connection // the straggler is writing through closing.start(); untilBlockedOrDone(closing); } finally { letTheStatementFinish.countDown(); } straggling.join(TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS)); closing.join(TimeUnit.SECONDS.toMillis(CLOSE_SECONDS)); reportFailureOf("the write of the straggler", stragglerFailure); reportFailureOf("the close of the import", closeFailure); assertFalse(straggling.isAlive(), "the write of the straggler did not finish"); assertFalse(closing.isAlive(), "the import did not close"); assertEquals(shared, Collections.emptyList(), "the close touched a connection while a thread of the import was inside a statement of it"); assertEquals(usedInOrder.size(), 1, "the write of the straggler did not reach a connection"); verify(usedInOrder.get(0), times(1)).commit(); }); } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "the import kept a permit of the pool"); } /** * A write that arrives after {@code close()} takes no connection. Phase two gives its threads * five seconds to answer an interrupt and then closes the importer whether they did or not, so * a write can reach it once its connections are back in the pool - and one that borrowed there * would take a permit nothing is left to give back, out of a pool that is never removed from * the map (#878). */ @Test(timeOut = 120000) public void testAWriteAfterCloseTakesNoConnection() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "4"); final String url = StubDriver.PREFIX + "importer-after-close"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.close(); try { importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); fail("a write that arrives after the import is closed must be refused"); } catch (StorageRuntimeException expected) { // the import is over: there is no transaction left for this write to belong to } try { // the same for a clear, which commits the other connections of the import before it // empties a tree: those are back in the pool, serving whoever borrowed them next importer.clearTree(ID2ENTRY); fail("a clear that arrives after the import is closed must be refused"); } catch (StorageRuntimeException expected) { // as above } assertEquals(used.size(), 1, "a write after close() took a connection of the pool"); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "a write after close() kept a connection"); } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "a write after close() kept a permit"); } /** * A commit that fails still gives every connection of the import back, and takes the ones after * it down with it. What a connection left behind holds is a permit of a pool that is never * removed from the map, so a permit lost to a failed import is lost for the life of the server * (#878) - and what a connection committed after the failure of an earlier one would hold is * the trust flags of {@code afterPhaseTwo}, which {@code close()} commits last for exactly this * reason: a failed import must not leave an index marked trusted over data that never got there. */ @Test(timeOut = 120000) public void testAFailedCommitStillReturnsEveryConnection() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "3"); final String url = StubDriver.PREFIX + "importer-failed-commit"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(STATE, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); doThrow(new SQLException("the socket went away")).when(usedInOrder.get(1)).commit(); try { importer.close(); fail("the failure of the commit was not reported"); } catch (StorageRuntimeException expected) { assertEquals(expected.getCause().getMessage(), "the socket went away"); } // the connections are committed in the order they were last written to, so this one // stands where the trust flags of afterPhaseTwo stand: behind the connection that just // failed, and rolled back by the return rather than committed on top of a failed import verify(usedInOrder.get(2), never()).commit(); assertEquals(CachedConnection.poolOf(url).idleCount(), 3, "a failed import kept a connection of the pool"); } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "a failed import kept a permit of the pool"); } /** * A tree that cannot be given a connection of its own is written through one the import already * holds, rather than failing the import. The connections are taken as the trees are first * touched, so a pool at its bound would otherwise stop an import halfway through - with the * clears of {@code beforePhaseOne} already committed - where the one connection an import held * before #891 would have carried it to the end. */ @Test(timeOut = 120000) public void testATreeSharesAConnectionWhenThePoolHasNoneToSpare() throws Exception { System.setProperty(CachedConnection.POOL_MAX_PROPERTY, "2"); System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1"); // nothing is going to be returned System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-pool-full"; final JDBCStorage storage = importingStorage(url); try { final Importer importer = storage.startImport(); // one of the two connections of the pool // and the other one to an operation of the server, so that the pool has none to spare final Connection heldByAnOperation = CachedConnection.getConnection(url); try { importing(importer, () -> { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); // on a thread of its own: a thread that already holds a connection of the pool is // exempt from waiting at its bound, and every thread of phase two starts holding // none putOnAThreadOfItsOwn(importer, DN2ID); assertEquals(used.size(), 1, "the tree the pool had no connection for did not share one"); }); } finally { heldByAnOperation.close(); } } finally { storage.close(); } } /** * A second {@code close()} touches nothing: the connections of the import are back in the pool * by then, and the pool hands them out again - so a close that walked its map a second time * would roll back and re-pool a connection another borrower is holding. */ @Test(timeOut = 120000) public void testASecondCloseTouchesNothingTheImportGaveBack() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-second-close"; final JDBCStorage storage = importingStorage(url); try { final Importer importer = storage.startImport(); importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); importer.close(); // the pool hands one of them straight back out, the way an LDAP operation would final Connection borrowedAgain = CachedConnection.getConnection(url); try { importer.close(); assertEquals(CachedConnection.poolOf(url).idleCount(), 1, "a second close() returned a connection the pool had already handed to somebody else"); } finally { borrowedAgain.close(); } } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "a connection of the import was lost"); } /** * A tree the database refuses a connection for is written through one the import already holds, * the way a tree the pool has nothing to spare for is (#1013). *

* The two ends of a borrow that can run out are not reported alike: the bound of the pool is * answered with a {@code SQLTimeoutException}, while a database at a limit of its own is * answered with whatever its driver says - and a code {@code CachedConnection.isWorthRetrying} * does not recognize is raised at once rather than waited out. A mysql account with a * {@code MAX_USER_CONNECTIONS} of its own answers 1226 on SQLState 42000, which is such a code * (#1011). An import that failed on it would stop halfway through, with the clears of * {@code beforePhaseOne} already committed, while every connection it holds still works. */ @Test(timeOut = 120000) public void testATreeSharesAConnectionWhenTheDatabaseRefusesOne() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-refused"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { // the first tree takes the connection the constructor borrowed, so that the second is // one the import has to go to the database for importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); // armed only now: what this is about is the borrows an import makes as it goes, not // the one it is started with - an import of a database that takes no connection at // all is refused where it is started refuseFurtherConnects("User 'opendj' has exceeded the 'max_user_connections' resource" + " (current value: 1)", "42000", 1226); // on a thread of its own, the way every thread of phase two starts: one that already // holds a connection of the pool takes another path through the borrow putOnAThreadOfItsOwn(importer, DN2ID); assertEquals(used.size(), 1, "the tree the database refused a connection for did not share one"); }); } finally { storage.close(); } assertEquals(CachedConnection.poolOf(url).meteredCount(), 0, "a refused borrow kept a permit of the pool"); } /** * ... and an import whose borrow is interrupted fails instead, keeping the interrupt. *

* The connection this import cannot have is the loss of a parallelism it asked for, and that is * what sharing is the answer to. An interrupt is not that: phase two interrupts the threads of * an import to stop it ({@code OnDiskMergeImporter.invokeParallel} gives them five seconds to * answer), and a thread that answered by writing the tree through another connection would be * carrying on with the work it was told to drop. The interrupt goes back on the thread as well: * a borrow that throws {@code InterruptedException} has cleared it, and what reads it next is * the executor of phase two. */ @Test(timeOut = 120000) public void testAnInterruptedBorrowFailsTheImportRatherThanSharingAConnection() throws Exception { System.setProperty(JDBCStorage.IMPORT_CONNECTIONS_PROPERTY, "2"); final String url = StubDriver.PREFIX + "importer-interrupted"; final JDBCStorage storage = importingStorage(url); final Importer importer = storage.startImport(); try { importing(importer, () -> { importer.put(ID2ENTRY, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); final AtomicReference failure = new AtomicReference<>(); final AtomicBoolean interruptKept = new AtomicBoolean(); final Thread writer = daemon(() -> { // set before the write rather than raced with it: the pool waits for an idle // connection interruptibly, so a thread that carries an interrupt into the borrow // gets the same InterruptedException as one interrupted while parked there Thread.currentThread().interrupt(); try { importer.put(DN2ID, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); } catch (Throwable t) { failure.set(t); } finally { interruptKept.set(Thread.currentThread().isInterrupted()); } }, "import-interrupted"); writer.start(); writer.join(TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS)); assertFalse(writer.isAlive(), "the interrupted write did not finish"); final Throwable reported = failure.get(); assertTrue(reported instanceof StorageRuntimeException, "an interrupted borrow was not reported as a failure of the import: " + reported); assertTrue(reported.getCause() instanceof InterruptedException, "an interrupted borrow was reported as something else: " + reported.getCause()); assertTrue(interruptKept.get(), "the borrow swallowed the interrupt of an import thread"); }); } finally { storage.close(); } } /** A storage open for writing over the driver of this test, which is what an import needs. */ private JDBCStorage importingStorage(String url) throws Exception { // mockCfg rather than a bare mock: it answers every getter with the value declared in // JDBCBackendConfiguration.xml, so a storage that comes to read a setting this test never // thought of gets the default instead of a null - the way the sibling suites of this package // build their configurations final JDBCBackendCfg cfg = mockCfg(JDBCBackendCfg.class); when(cfg.getDBDirectory()).thenReturn(url); final JDBCStorage storage = new JDBCStorage(cfg, null); storage.open(AccessMode.READ_WRITE); // the connection the open borrowed and gave back is not one of the import's: only what a // statement was issued on is recorded, and the open issues none forgetTheConnections(); return storage; } /** * Makes the driver of this test answer every further connect with a failure of the given shape, * the way a database at a limit of its own does. */ private void refuseFurtherConnects(final String message, final String sqlState, final int vendorCode) { refusal.set(() -> new SQLException(message, sqlState, vendorCode)); } private Future put(final Importer importer, final TreeName treeName) { return threads.submit(() -> { toTheStartingLine(); importer.put(treeName, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); }); } /** A write of a thread that has written nothing before, the way every thread of an import starts. */ private void putOnAThreadOfItsOwn(final Importer importer, final TreeName treeName) throws Exception { final AtomicReference failure = new AtomicReference<>(); final Thread thread = daemon(() -> { try { importer.put(treeName, ByteString.valueOfUtf8("k"), ByteString.valueOfUtf8("v")); } catch (Throwable t) { failure.set(t); } }, "import-" + treeName.getIndexId()); thread.start(); thread.join(TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS)); if (failure.get() != null) { throw new IllegalStateException("the write of " + treeName + " failed", failure.get()); } // asserted rather than left to the join: a write still blocked - on the monitor of a // connection, or on a borrow - would otherwise leave the assertions of the test standing on // a run that never made the state they are about assertFalse(thread.isAlive(), "the write of " + treeName + " did not finish"); } /** Raises what a thread of this test caught, under a name that says which thread it was. */ private static void reportFailureOf(String what, AtomicReference failure) { final Throwable caught = failure.get(); if (caught != null) { throw new IllegalStateException(what + " failed", caught); } } /** * A thread of this suite. Daemon, every one of them: a write left standing on the monitor of a * connection by a failing run must not keep the JVM of the build alive once the test that * started it has been reported. */ private static Thread daemon(Runnable body, String name) { final Thread thread = new Thread(body, name); thread.setDaemon(true); return thread; } /** The threads a test writes through, so that a stuck one is not a build that never ends. */ private static ExecutorService daemonThreads(int count, final String name) { final AtomicInteger numbered = new AtomicInteger(); return Executors.newFixedThreadPool(count, runnable -> daemon(runnable, name + "-" + numbered.incrementAndGet())); } /** The body of a test that writes through an import - see {@link #importing(Importer, ImportBody)}. */ private interface ImportBody { void run() throws Exception; } /** * Runs the body of a test against an import and closes that import afterwards, whatever the * body did - without the close having the last word on what went wrong. *

* The close used to stand in a {@code finally} of the test, and it is here instead because the * two things that can go wrong there are not disjoint: {@code close()} commits and returns every * connection under the monitor of that connection, so a write of this test left standing on one * is exactly what makes the close wait - and a close that gave up waiting and threw out of that * {@code finally} replaced the failure the test came for with one naming the monitor. Here the * body has the first say and the trouble of the close rides along with it as suppressed. */ private void importing(Importer importer, ImportBody body) throws Exception { Throwable found = null; try { body.run(); } catch (Throwable t) { found = t; } final Throwable reported = closeWithoutHanging(importer, found); if (reported instanceof Error) { throw (Error) reported; } if (reported instanceof Exception) { throw (Exception) reported; } if (reported != null) { throw new IllegalStateException("the import failed to close", reported); } } /** * Closes an import without waiting for a straggling thread of the test forever, and returns * what is to be reported: what the body of the test found where it found something, with the * trouble of the close - a close that never finished, or one that failed - suppressed into it. */ private Throwable closeWithoutHanging(Importer importer, Throwable found) { final AtomicReference failure = new AtomicReference<>(); final Thread closing = daemon(() -> { try { importer.close(); } catch (Throwable t) { failure.set(t); } }, "import-close"); closing.start(); Throwable trouble; try { closing.join(TimeUnit.SECONDS.toMillis(CLOSE_SECONDS)); trouble = closing.isAlive() ? new IllegalStateException("the import did not close within " + CLOSE_SECONDS + "s: a thread of this test is holding the monitor of one of its connections") : failure.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); trouble = new IllegalStateException("interrupted while closing the import", e); } if (found == null) { return trouble; } if (trouble != null) { found.addSuppressed(trouble); } return found; } private List connectionsUsed() { synchronized (used) { return new ArrayList<>(used); } } /** * A connection of this test: it answers every statement with one mock of its own and records * the thread that asked for it while that thread is inside the call. */ private Connection newConnection() throws SQLException { final Connection con = org.mockito.Mockito.mock(Connection.class); final PreparedStatement statement = org.mockito.Mockito.mock(PreparedStatement.class); when(con.isValid(anyInt())).thenReturn(true); // the statement names the connection it runs on, as CachedConnection.prepareStatement() has // it: the bound of a statement is arbitrated on the connection, which is read off the statement when(statement.getConnection()).thenReturn(con); // a thread is inside a statement of a connection for longer than the prepareStatement that // hands it one: what the sql server driver walks past the end of its array is the list of // listeners that the execution and the close of a statement mutate as readily (#891), so // those are recorded here too - the window this suite watches is the whole of a statement doAnswer(invocation -> { enterAStatement(con); try { return 0; // what a mock of this returns anyway: no row of this test is counted } finally { leaveAStatement(con); } }).when(statement).executeUpdate(); doAnswer(invocation -> { enterAStatement(con); try { return Boolean.FALSE; // as above: nothing of this test reads a result set } finally { leaveAStatement(con); } }).when(statement).execute(); doAnswer(invocation -> { enterAStatement(con); try { return null; } finally { leaveAStatement(con); } }).when(statement).close(); // the commits of the import in the order they happen: which connection is committed when is // what keeps a failed import from leaving an index marked trusted over data that is not there doAnswer(invocation -> { recordAThreadInsideAStatement(con, "commit"); commits.add(con); return null; }).when(con).commit(); // the return of a connection rolls it back and hands it to the next borrower, so neither may // happen while a statement of an import thread is in flight on it doAnswer(invocation -> { recordAThreadInsideAStatement(con, "rollback"); return null; }).when(con).rollback(); doAnswer(invocation -> { recordAThreadInsideAStatement(con, "close"); return null; }).when(con).close(); when(con.prepareStatement(anyString())).thenAnswer(invocation -> { if (used.add(con)) { usedInOrder.add(con); } enterAStatement(con); try { // the hold outside the monitor of the map above, and after this thread has entered // it: what it is waiting for is a peer thread getting in here and recording itself parkInsideTheStatement(); holdTheConnection(); meetPeers(); return statement; } finally { leaveAStatement(con); } }); return con; } /** Records this thread as inside a statement of the given connection, with whoever is already there. */ private void enterAStatement(Connection con) { synchronized (inStatement) { final Set inside = inStatement.get(con); final Set threadsInside = inside != null ? inside : new LinkedHashSet(); for (final Thread peer : threadsInside) { if (peer != Thread.currentThread()) { shared.add(peer.getName() + " and " + Thread.currentThread().getName()); } } threadsInside.add(Thread.currentThread()); inStatement.put(con, threadsInside); } } private void leaveAStatement(Connection con) { synchronized (inStatement) { final Set threadsInside = inStatement.get(con); if (threadsInside != null && threadsInside.remove(Thread.currentThread()) && threadsInside.isEmpty()) { inStatement.remove(con); } } } /** Records a connection touched from one thread while another is inside a statement on it. */ private void recordAThreadInsideAStatement(Connection con, String what) { synchronized (inStatement) { final Set threadsInside = inStatement.get(con); if (threadsInside == null) { return; } for (final Thread inside : threadsInside) { if (inside != Thread.currentThread()) { shared.add(what + " of " + Thread.currentThread().getName() + " while " + inside.getName() + " was inside a statement"); } } } } /** * Parks a thread inside the first statement it issues and tells the test it is in there, until * the test lets it go. *

* A write in flight, as the rest of the import sees one: the connection holds a statement that * has not come back, so nothing is recorded against it yet - {@code put()} counts a write once * the statement returns - and its monitor is held for as long as this lasts. What a test does * from there is ask another thread of the import a question about that connection. */ private void parkInsideTheStatement() { final CountDownLatch letGo = letTheStatementFinish; if (letGo == null || parked.get()) { return; } parked.set(Boolean.TRUE); insideAStatementNow.countDown(); try { letGo.await(PARK_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** * Waits until a thread of this test has got as far as it is going to get on its own: either it * is blocked on a monitor - the thing this suite is about - or it is done. *

* Waited for rather than slept past: what the tests below ask is whether a thread stops at a * monitor or walks through it, and a fixed pause would answer that with the load of the machine * on a slow run. */ private static void untilBlockedOrDone(Thread thread) throws InterruptedException { final long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(RENDEZVOUS_SECONDS); while (System.currentTimeMillis() < deadline) { final Thread.State state = thread.getState(); if (state == Thread.State.BLOCKED || state == Thread.State.TERMINATED) { return; } Thread.sleep(10); } } /** * Holds the first statement of a thread on the connection it is on, until a peer thread has one * of its own in flight there or the hold runs out. *

* How a test asks whether two threads can be inside a statement of one connection at the same * time: they cannot be brought together at a barrier the way the threads of two connections are * - a second thread held on the monitor of the connection never reaches one - so the first * thread in waits there instead, and the peer either turns up beside it, which is #891, or does * not, which is the hold being paid in full. */ private void holdTheConnection() { final CountDownLatch inside = insideAStatement; if (inside == null || held.get()) { return; } held.set(Boolean.TRUE); inside.countDown(); try { inside.await(HOLD_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** Where the threads of a test start together, in front of the monitors of the importer. */ private void toTheStartingLine() { final CyclicBarrier line = startingLine; if (line == null) { return; } try { line.await(RENDEZVOUS_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException | TimeoutException | BrokenBarrierException e) { throw new IllegalStateException("the threads of this test did not reach the starting line", e); } } /** * Holds the first statement of a thread until every peer of the rendezvous has one in flight * too. Reported as a failure of the statement rather than waited out: a rendezvous nobody else * reaches is an import that serialized its threads, which is what the parallel test is about. */ private void meetPeers() throws SQLException { final CyclicBarrier barrier = rendezvous; if (barrier == null || met.get()) { return; } met.set(Boolean.TRUE); try { barrier.await(RENDEZVOUS_SECONDS, TimeUnit.SECONDS); } catch (TimeoutException | BrokenBarrierException e) { throw new SQLException("no peer thread had a statement of its own in flight within " + RENDEZVOUS_SECONDS + "s: the import did not let its threads write at the same time", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException("interrupted while waiting for the peer threads of the import", e); } } /** A driver of this test, so that the connections of an import need no database behind them. */ private final class StubDriver implements Driver { static final String PREFIX = "jdbc:opendj-import-stub:"; @Override public Connection connect(String url, Properties info) throws SQLException { if (!acceptsURL(url)) { return null; } final Supplier refused = refusal.get(); if (refused != null) { throw refused.get(); } return newConnection(); } @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()); } } }