/* * 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
* 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
* 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
* 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
* 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
* 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