Skip to content

Commit

Permalink
Adds pluggable wait strategy, to enable containers to be checked for …
Browse files Browse the repository at this point in the history
…readiness using arbitrary logic. Adds HTTP(S) wait strategy to wait for a particular endpoint to be available.
  • Loading branch information
outofcoffee committed Apr 28, 2016
1 parent e1c96be commit 1613efe
Show file tree
Hide file tree
Showing 11 changed files with 589 additions and 39 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Matthieu Baechler <[email protected]>
Krystian Nowak <[email protected]>
Viktor Schulz <[email protected]>
Asaf Mesika <[email protected]>
Sergei Egorov <[email protected]>
Sergei Egorov <[email protected]>
Pete Cornish <[email protected]>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* AN exception that may be raised during launch of a container.
*/
class ContainerLaunchException extends RuntimeException {
public class ContainerLaunchException extends RuntimeException {

public ContainerLaunchException(String message) {
super(message);
Expand Down
132 changes: 95 additions & 37 deletions core/src/main/java/org/testcontainers/containers/GenericContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import org.jetbrains.annotations.Nullable;
import org.junit.runner.Description;
import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
Expand All @@ -28,12 +27,13 @@
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.containers.wait.Wait;
import org.testcontainers.containers.wait.WaitStrategy;
import org.testcontainers.images.RemoteDockerImage;
import org.testcontainers.utility.*;

import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
Expand Down Expand Up @@ -63,7 +63,6 @@ public class GenericContainer extends FailureDetectingExternalResource implement

public static final int CONTAINER_RUNNING_TIMEOUT_SEC = 30;


/*
* Default settings
*/
Expand Down Expand Up @@ -91,9 +90,6 @@ public class GenericContainer extends FailureDetectingExternalResource implement
@NonNull
private Map<String, LinkableContainer> linkedContainers = new HashMap<>();

@NonNull
private Duration startupTimeout = Duration.ofSeconds(60);

@NonNull
private Duration minimumRunningDuration = null;

Expand All @@ -113,6 +109,12 @@ public class GenericContainer extends FailureDetectingExternalResource implement
protected String containerId;
protected String containerName;

/**
* The approach to determine if the container is ready.
*/
@NonNull
protected WaitStrategy waitStrategy = Wait.defaultWaitStrategy();

@Nullable
private InspectContainerResponse containerInfo;

Expand Down Expand Up @@ -297,6 +299,9 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
protected void containerIsStarted(InspectContainerResponse containerInfo) {
}

/**
* @return the port on which to check if the container is ready
*/
protected Integer getLivenessCheckPort() {
if (exposedPorts.size() > 0) {
return getMappedPort(exposedPorts.get(0));
Expand Down Expand Up @@ -349,42 +354,37 @@ private void applyConfiguration(CreateContainerCmd createCommand) {
}

/**
* Wait until the container has started. The default implementation simply
* waits for a port to start listening; subclasses may override if more
* sophisticated behaviour is required.
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
*
* @see Wait#defaultWaitStrategy()
* @param waitStrategy the WaitStrategy to use
* @return this
*/
protected void waitUntilContainerStarted() {
waitForListeningPort(DockerClientFactory.instance().dockerHostIpAddress(), getLivenessCheckPort());
public GenericContainer waitingFor(@NonNull WaitStrategy waitStrategy) {
this.waitStrategy = waitStrategy;
return this;
}

/**
* Waits for a port to start listening for incoming connections.
* The {@link WaitStrategy} to use to determine if the container is ready.
* Defaults to {@link Wait#defaultWaitStrategy()}.
*
* @param ipAddress the IP address to attempt to connect to
* @param port the port which will start accepting connections
* @return the {@link WaitStrategy} to use
*/
protected void waitForListeningPort(String ipAddress, Integer port) {

if (port == null) {
return;
}

try {
Unreliables.retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
DOCKER_CLIENT_RATE_LIMITER.doWhenReady(() -> {
try {
new Socket(ipAddress, port).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return true;
});
} catch (TimeoutException e) {
throw new ContainerLaunchException("Timed out waiting for container port to open (" + ipAddress + ":" + port + " should be listening)");
}
protected WaitStrategy getWaitStrategy() {
return waitStrategy;
}

/**
* Wait until the container has started. The default implementation simply
* waits for a port to start listening; other implementations are available
* as implementations of {@link WaitStrategy}
*
* @see #waitingFor(WaitStrategy)
*/
protected void waitUntilContainerStarted() {
getWaitStrategy().waitUntilReady(this);
}

/**
* Set the command that should be run in the container
Expand Down Expand Up @@ -539,13 +539,13 @@ public GenericContainer withClasspathResourceMapping(String resourcePath, String

/**
* Set the duration of waiting time until container treated as started.
* @see GenericContainer#waitForListeningPort(String, Integer)
* @see WaitStrategy#waitUntilReady(GenericContainer)
*
* @param startupTimeout timeout
* @return this
*/
public GenericContainer withStartupTimeout(Duration startupTimeout) {
this.setStartupTimeout(startupTimeout);
getWaitStrategy().withStartupTimeout(startupTimeout);
return this;
}

Expand All @@ -562,7 +562,6 @@ public String getContainerIpAddress() {
* Only consider a container to have successfully started if it has been running for this duration. The default
* value is null; if that's the value, ignore this check.
*/

public GenericContainer withMinimumRunningDuration(Duration minimumRunningDuration) {
this.setMinimumRunningDuration(minimumRunningDuration);
return this;
Expand Down Expand Up @@ -794,4 +793,63 @@ public String getStderr() {
return stderr;
}
}

/**
* Convenience class with access to non-public members of GenericContainer.
*/
public static abstract class AbstractWaitStrategy implements WaitStrategy {
protected GenericContainer container;

@NonNull
protected Duration startupTimeout = Duration.ofSeconds(60);

/**
* Wait until the container has started.
*
* @param container the container for which to wait
*/
@Override
public void waitUntilReady(GenericContainer container) {
this.container = container;
waitUntilReady();
}

/**
* Wait until {@link #container} has started.
*/
protected abstract void waitUntilReady();

/**
* Set the duration of waiting time until container treated as started.
*
* @param startupTimeout timeout
* @return this
* @see WaitStrategy#waitUntilReady(GenericContainer)
*/
public WaitStrategy withStartupTimeout(Duration startupTimeout) {
this.startupTimeout = startupTimeout;
return this;
}

/**
* @return the container's logger
*/
protected Logger logger() {
return container.logger();
}

/**
* @return the port on which to check if the container is ready
*/
protected Integer getLivenessCheckPort() {
return container.getLivenessCheckPort();
}

/**
* @return the rate limiter to use
*/
protected RateLimiter getRateLimiter() {
return DOCKER_CLIENT_RATE_LIMITER;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.testcontainers.containers.wait;

import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;

import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

/**
* Waits until a socket connection can be established on a port exposed or mapped by the container.
*
* @author richardnorth
*/
public class HostPortWaitStrategy extends GenericContainer.AbstractWaitStrategy {
@Override
protected void waitUntilReady() {
final Integer port = getLivenessCheckPort();
if (null == port) {
return;
}

final String ipAddress = DockerClientFactory.instance().dockerHostIpAddress();
try {
Unreliables.retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
getRateLimiter().doWhenReady(() -> {
try {
new Socket(ipAddress, port).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return true;
});

} catch (TimeoutException e) {
throw new ContainerLaunchException("Timed out waiting for container port to open (" +
ipAddress + ":" + port + " should be listening)");
}
}
}
Loading

0 comments on commit 1613efe

Please sign in to comment.