Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <matthieu.baechler@gmail.com>
Krystian Nowak <krystian.nowak@gmail.com>
Viktor Schulz <vschulz@mail.uni-mannheim.de>
Asaf Mesika <asaf.mesika@gmail.com>
Sergei Egorov <bsideup@gmail.com>
Sergei Egorov <bsideup@gmail.com>
Pete Cornish <outofcoffee@gmail.com>
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();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above - I think this could maybe be a bit more readable by taking a less clever approach!

@outofcoffee outofcoffee Apr 18, 2016 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Have moved this method into GenericContainer anyway.


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