Skip to content

Commit ac1d4af

Browse files
authored
Add DockerHealthcheckWaitStrategy (#618)
Also adds a isHealthy() method to the ContainerState interface.
1 parent 23478fa commit ac1d4af

File tree

8 files changed

+126
-2
lines changed

8 files changed

+126
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
1515
- 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))
1616
- Added `ContainerState` interface representing the state of a started container ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))
1717
- Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))
18+
- 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)).
1819

1920
## [1.6.0] - 2018-01-28
2021

core/src/main/java/org/testcontainers/containers/ContainerState.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
public interface ContainerState {
1717

18+
String STATE_HEALTHY = "healthy";
19+
1820
/**
1921
* Get the IP address that this container may be reached on (may not be the local machine).
2022
*
@@ -27,14 +29,41 @@ default String getContainerIpAddress() {
2729
/**
2830
* @return is the container currently running?
2931
*/
30-
default Boolean isRunning() {
32+
default boolean isRunning() {
33+
if (getContainerId() == null) {
34+
return false;
35+
}
36+
37+
try {
38+
Boolean running = getCurrentContainerInfo().getState().getRunning();
39+
return Boolean.TRUE.equals(running);
40+
} catch (DockerException e) {
41+
return false;
42+
}
43+
}
44+
45+
/**
46+
* @return has the container health state 'healthy'?
47+
*/
48+
default boolean isHealthy() {
49+
if (getContainerId() == null) {
50+
return false;
51+
}
52+
3153
try {
32-
return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning();
54+
InspectContainerResponse inspectContainerResponse = getCurrentContainerInfo();
55+
String healthStatus = inspectContainerResponse.getState().getHealth().getStatus();
56+
57+
return healthStatus.equals(STATE_HEALTHY);
3358
} catch (DockerException e) {
3459
return false;
3560
}
3661
}
3762

63+
default InspectContainerResponse getCurrentContainerInfo() {
64+
return DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec();
65+
}
66+
3867
/**
3968
* Get the actual mapped port for a first port exposed by the container.
4069
*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.testcontainers.containers.wait.strategy;
2+
3+
import org.rnorth.ducttape.TimeoutException;
4+
import org.rnorth.ducttape.unreliables.Unreliables;
5+
import org.testcontainers.containers.ContainerLaunchException;
6+
7+
import java.util.concurrent.TimeUnit;
8+
9+
/**
10+
* Wait strategy leveraging Docker's built-in healthcheck mechanism.
11+
*
12+
* @see <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">https://docs.docker.com/engine/reference/builder/#healthcheck</a>
13+
*/
14+
public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy {
15+
16+
@Override
17+
protected void waitUntilReady() {
18+
19+
try {
20+
Unreliables.retryUntilTrue((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, waitStrategyTarget::isHealthy);
21+
} catch (TimeoutException e) {
22+
throw new ContainerLaunchException("Timed out waiting for container to become healthy");
23+
}
24+
}
25+
}

core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,13 @@ public static HttpWaitStrategy forHttps(String path) {
6161
public static LogMessageWaitStrategy forLogMessage(String regex, int times) {
6262
return new LogMessageWaitStrategy().withRegEx(regex).withTimes(times);
6363
}
64+
65+
/**
66+
* Convenience method to return a WaitStrategy leveraging Docker's built-in healthcheck.
67+
*
68+
* @return DockerHealthcheckWaitStrategy
69+
*/
70+
public static DockerHealthcheckWaitStrategy forHealthcheck() {
71+
return new DockerHealthcheckWaitStrategy();
72+
}
6473
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.testcontainers.containers.wait.strategy;
2+
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
import org.testcontainers.containers.ContainerLaunchException;
6+
import org.testcontainers.containers.GenericContainer;
7+
import org.testcontainers.images.builder.ImageFromDockerfile;
8+
9+
import java.time.Duration;
10+
11+
import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows;
12+
13+
public class DockerHealthcheckWaitStrategyTest {
14+
15+
private GenericContainer container;
16+
17+
@Before
18+
public void setUp() {
19+
// Using a Dockerfile here, since Dockerfile builder DSL doesn't support HEALTHCHECK
20+
container = new GenericContainer(new ImageFromDockerfile()
21+
.withFileFromClasspath("write_file_and_loop.sh", "health-wait-strategy-dockerfile/write_file_and_loop.sh")
22+
.withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile"))
23+
.waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3)));
24+
}
25+
26+
@Test
27+
public void startsOnceHealthy() {
28+
container.start();
29+
}
30+
31+
@Test
32+
public void containerStartFailsIfContainerIsUnhealthy() {
33+
container.withCommand("tail", "-f", "/dev/null");
34+
assertThrows("Container launch fails when unhealthy", ContainerLaunchException.class, container::start);
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM alpine:3.7
2+
3+
HEALTHCHECK --interval=1s CMD test -e /testfile
4+
5+
COPY write_file_and_loop.sh write_file_and_loop.sh
6+
RUN chmod +x write_file_and_loop.sh
7+
8+
CMD ["/write_file_and_loop.sh"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/ash
2+
3+
echo sleeping
4+
sleep 2
5+
echo writing file
6+
touch /testfile
7+
8+
while true; do sleep 1; done

docs/usage/options.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ public static GenericContainer elasticsearch =
109109
.usingTls());
110110
```
111111

112+
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:
113+
```java
114+
@ClassRule2.32.3
115+
public static GenericContainer container =
116+
new GenericContainer("image-with-healthcheck:4.2")
117+
.waitingFor(Wait.forHealthcheck());
118+
```
119+
112120
For futher options, check out the `Wait` convenience class, or the various subclasses of `WaitStrategy`. If none of these options
113121
meet your requirements, you can create your own subclass of `AbstractWaitStrategy` with an appropriate wait
114122
mechanism in `waitUntilReady()`. The `GenericContainer.waitingFor()` method accepts any valid `WaitStrategy`.

0 commit comments

Comments
 (0)