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

Add DockerHealthcheckWaitStrategy #618

Merged
merged 9 commits into from
Apr 3, 2018
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
- Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))
- Added `ContainerState` interface representing the state of a started container ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))
- Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))
- Added `DockerHealthcheckWaitStrategy` that is based on Docker's built-in [healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) ([\#618](https://github.com/testcontainers/testcontainers-java/pull/618)).

## [1.6.0] - 2018-01-28

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

public interface ContainerState {

String STATE_HEALTHY = "healthy";

/**
* Get the IP address that this container may be reached on (may not be the local machine).
*
Expand All @@ -27,14 +29,41 @@ default String getContainerIpAddress() {
/**
* @return is the container currently running?
*/
default Boolean isRunning() {
default boolean isRunning() {
if (getContainerId() == null) {
return false;
}

try {
Boolean running = getCurrentContainerInfo().getState().getRunning();
return Boolean.TRUE.equals(running);
} catch (DockerException e) {
return false;
}
}

/**
* @return has the container health state 'healthy'?
*/
default boolean isHealthy() {
if (getContainerId() == null) {
return false;
}

try {
return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning();
InspectContainerResponse inspectContainerResponse = getCurrentContainerInfo();
String healthStatus = inspectContainerResponse.getState().getHealth().getStatus();

return healthStatus.equals(STATE_HEALTHY);
} catch (DockerException e) {
return false;
}
}

default InspectContainerResponse getCurrentContainerInfo() {
return DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec();
}

/**
* Get the actual mapped port for a first port exposed by the container.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.testcontainers.containers.wait.strategy;

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

import java.util.concurrent.TimeUnit;

/**
* Wait strategy leveraging Docker's built-in healthcheck mechanism.
*
* @see <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">https://docs.docker.com/engine/reference/builder/#healthcheck</a>
*/
public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy {

@Override
protected void waitUntilReady() {

try {
Unreliables.retryUntilTrue((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, waitStrategyTarget::isHealthy);
} catch (TimeoutException e) {
throw new ContainerLaunchException("Timed out waiting for container to become healthy");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,13 @@ public static HttpWaitStrategy forHttps(String path) {
public static LogMessageWaitStrategy forLogMessage(String regex, int times) {
return new LogMessageWaitStrategy().withRegEx(regex).withTimes(times);
}

/**
* Convenience method to return a WaitStrategy leveraging Docker's built-in healthcheck.
*
* @return DockerHealthcheckWaitStrategy
*/
public static DockerHealthcheckWaitStrategy forHealthcheck() {
return new DockerHealthcheckWaitStrategy();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.testcontainers.containers.wait.strategy;

import org.junit.Before;
import org.junit.Test;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;

import java.time.Duration;

import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows;

public class DockerHealthcheckWaitStrategyTest {

private GenericContainer container;

@Before
public void setUp() {
// Using a Dockerfile here, since Dockerfile builder DSL doesn't support HEALTHCHECK
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should add HEALTHCHECK support to the DSL?

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitly, but another PR I assume?

Copy link
Member

Choose a reason for hiding this comment

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

sure, up to you

container = new GenericContainer(new ImageFromDockerfile()
.withFileFromClasspath("write_file_and_loop.sh", "health-wait-strategy-dockerfile/write_file_and_loop.sh")
.withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile"))
.waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3)));
}

@Test
public void startsOnceHealthy() {
container.start();
}

@Test
public void containerStartFailsIfContainerIsUnhealthy() {
container.withCommand("tail", "-f", "/dev/null");
assertThrows("Container launch fails when unhealthy", ContainerLaunchException.class, container::start);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM alpine:3.7

HEALTHCHECK --interval=1s CMD test -e /testfile

COPY write_file_and_loop.sh write_file_and_loop.sh
RUN chmod +x write_file_and_loop.sh

CMD ["/write_file_and_loop.sh"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/ash

echo sleeping
sleep 2
echo writing file
touch /testfile

while true; do sleep 1; done
8 changes: 8 additions & 0 deletions docs/usage/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ public static GenericContainer elasticsearch =
.usingTls());
```

If the used image supports Docker's [Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) feature, you can directly leverage the `healthy` state of the container as your wait condition:
```java
@ClassRule2.32.3
public static GenericContainer container =
new GenericContainer("image-with-healthcheck:4.2")
.waitingFor(Wait.forHealthcheck());
```

For futher options, check out the `Wait` convenience class, or the various subclasses of `WaitStrategy`. If none of these options
meet your requirements, you can create your own subclass of `AbstractWaitStrategy` with an appropriate wait
mechanism in `waitUntilReady()`. The `GenericContainer.waitingFor()` method accepts any valid `WaitStrategy`.
Expand Down