Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No jdbc driver #1590

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -33,11 +33,6 @@ public abstract class JdbcDatabaseContainer<SELF extends JdbcDatabaseContainer<S
private String initScriptPath;
protected Map<String, String> 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;

Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand All @@ -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());
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
25 changes: 25 additions & 0 deletions modules/jdbc/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.apache.http" level="WARN"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="org.zeroturnaround.exec" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.rnorth.tcpunixsocketproxy" level="INFO" />
<logger name="io.netty" level="WARN" />
<logger name="org.mongodb" level="INFO" />
<logger name="org.testcontainers.shaded" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>

</configuration>