diff --git a/core/build.gradle b/core/build.gradle index a77720d66df..870429172f1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -66,6 +66,7 @@ dependencies { compile ('org.rnorth.duct-tape:duct-tape:1.0.7') { exclude(group: 'org.jetbrains', module: 'annotations') } + shaded 'org.awaitility:awaitility:3.1.6' compile 'org.rnorth.visible-assertions:visible-assertions:2.1.2' diff --git a/core/src/main/java/org/testcontainers/containers/ContainerLaunchException.java b/core/src/main/java/org/testcontainers/containers/ContainerLaunchException.java index 9943d9f612b..bf407c16808 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerLaunchException.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerLaunchException.java @@ -9,7 +9,7 @@ public ContainerLaunchException(String message) { super(message); } - public ContainerLaunchException(String message, Exception exception) { - super(message, exception); + public ContainerLaunchException(String message, Throwable cause) { + super(message, cause); } } diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index e480070168f..39ac0839039 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -1,11 +1,9 @@ package org.testcontainers.containers; -import lombok.NonNull; import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.NonNull; +import org.awaitility.core.ConditionTimeoutException; import org.jetbrains.annotations.NotNull; -import org.rnorth.ducttape.ratelimits.RateLimiter; -import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; -import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.delegate.DatabaseDelegate; import org.testcontainers.ext.ScriptUtils; @@ -19,7 +17,9 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; /** * Base class for containers that expose a JDBC connection @@ -33,11 +33,6 @@ public abstract class JdbcDatabaseContainer parameters = new HashMap<>(); - private static final RateLimiter DB_CONNECT_RATE_LIMIT = RateLimiterBuilder.newBuilder() - .withRate(10, TimeUnit.SECONDS) - .withConstantThroughput() - .build(); - private int startupTimeoutSeconds = 120; private int connectTimeoutSeconds = 120; @@ -126,23 +121,10 @@ protected void waitUntilContainerStarted() { // Repeatedly try and open a connection to the DB and execute a test query logger().info("Waiting for database connection to become available at {} using query '{}'", getJdbcUrl(), getTestQueryString()); - Unreliables.retryUntilSuccess(getStartupTimeoutSeconds(), TimeUnit.SECONDS, () -> { - - if (!isRunning()) { - throw new ContainerLaunchException("Container failed to start"); - } - - try (Connection connection = createConnection("")) { - boolean success = connection.createStatement().execute(JdbcDatabaseContainer.this.getTestQueryString()); - if (success) { - logger().info("Obtained a connection to container ({})", JdbcDatabaseContainer.this.getJdbcUrl()); - return null; - } else { - throw new SQLException("Failed to execute test query"); - } - } - }); + await().ignoreExceptionsMatching(e -> ! (e instanceof NoDriverFoundException)) + .timeout(startupTimeoutSeconds, SECONDS) + .until(this::isConnectable); } @Override @@ -155,14 +137,14 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * * @return a JDBC Driver */ - public Driver getJdbcDriverInstance() { + public Driver getJdbcDriverInstance() throws NoDriverFoundException { synchronized (DRIVER_LOAD_MUTEX) { if (driver == null) { try { driver = (Driver) Class.forName(this.getDriverClassName()).newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { - throw new RuntimeException("Could not get Driver", e); + throw new NoDriverFoundException("Could not get Driver", e); } } } @@ -178,7 +160,7 @@ public Driver getJdbcDriverInstance() { * @return a Connection * @throws SQLException if there is a repeated failure to create the connection */ - public Connection createConnection(String queryString) throws SQLException { + public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException { final Properties info = new Properties(); info.put("user", this.getUsername()); info.put("password", this.getPassword()); @@ -187,10 +169,13 @@ public Connection createConnection(String queryString) throws SQLException { final Driver jdbcDriverInstance = getJdbcDriverInstance(); try { - return Unreliables.retryUntilSuccess(getConnectTimeoutSeconds(), TimeUnit.SECONDS, () -> - DB_CONNECT_RATE_LIMIT.getWhenReady(() -> - jdbcDriverInstance.connect(url, info))); - } catch (Exception e) { + return await() + .ignoreExceptions() + .atMost(connectTimeoutSeconds, SECONDS) + .pollDelay(0, SECONDS) + .pollInterval(5, SECONDS) + .until(() -> jdbcDriverInstance.connect(url, info), __ -> true); + } catch (ConditionTimeoutException e) { throw new SQLException("Could not create new connection", e); } } @@ -256,4 +241,27 @@ protected int getConnectTimeoutSeconds() { protected DatabaseDelegate getDatabaseDelegate() { return new JdbcDatabaseDelegate(this, ""); } + + private boolean isConnectable() throws SQLException { + if (!isRunning()) { + return false; // Don't attempt to connect + } + + try (Connection connection = createConnection("")) { + boolean success = connection.createStatement().execute(JdbcDatabaseContainer.this.getTestQueryString()); + + if (success) { + logger().info("Obtained a connection to container ({})", JdbcDatabaseContainer.this.getJdbcUrl()); + return true; + } else { + return false; + } + } + } + + public static class NoDriverFoundException extends RuntimeException { + public NoDriverFoundException(String message, Throwable e) { + super(message, e); + } + } } diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java new file mode 100644 index 00000000000..09aed9ab6f0 --- /dev/null +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java @@ -0,0 +1,69 @@ +package org.testcontainers.jdbc; + +import com.google.common.base.Throwables; +import org.junit.Test; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; +import static org.rnorth.visibleassertions.VisibleAssertions.fail; + +public class MissingJdbcDriverTest { + + @Test + public void shouldFailFastIfNoDriverFound() { + + AtomicInteger connectionAttempts = new AtomicInteger(); + + // Anonymous inner class for the purposes of testing, with a known non-existent driver testFailFastIfNoDriverFound + final JdbcDatabaseContainer container = new JdbcDatabaseContainer("mysql:5.7.22") { + + @Override + public String getDriverClassName() { + return "nonexistent.ClassName"; + } + + @Override + public String getJdbcUrl() { + return ""; + } + + @Override + public String getUsername() { + return ""; + } + + @Override + public String getPassword() { + return ""; + } + + @Override + protected String getTestQueryString() { + return ""; + } + + @Override + public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException { + connectionAttempts.incrementAndGet(); // test window: so we know how many times a connection was attempted + return super.createConnection(queryString); + } + }; + + try { + container.start(); + fail("The container is expected to fail to start"); + } catch (Exception e) { + final Throwable rootCause = Throwables.getRootCause(e); + assertTrue("ClassNotFoundException is the root cause", rootCause instanceof ClassNotFoundException); + } finally { + container.stop(); + } + + assertEquals("only one connection attempt should have been made", 1, connectionAttempts.get()); + } +} diff --git a/modules/jdbc/src/test/resources/logback-test.xml b/modules/jdbc/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..b0f6b00e3e0 --- /dev/null +++ b/modules/jdbc/src/test/resources/logback-test.xml @@ -0,0 +1,25 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + +