Skip to content

Commit 2d57f61

Browse files
Improve startup wait checks (#6384)
* Log a warning if the container image and server architectures do not match * Make sure a container exiting successfully is properly handled * We don't need to continue checking if the container stops running Co-authored-by: Eddú Meléndez <[email protected]>
1 parent 83f5dfc commit 2d57f61

File tree

6 files changed

+114
-3
lines changed

6 files changed

+114
-3
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,11 @@ private void tryStart(Instant startedAt) {
470470
}
471471
);
472472

473+
String emulationWarning = checkForEmulation();
474+
if (emulationWarning != null) {
475+
logger().warn(emulationWarning);
476+
}
477+
473478
// Tell subclasses that we're starting
474479
containerIsStarting(containerInfo, reused);
475480

@@ -960,6 +965,32 @@ protected void waitUntilContainerStarted() {
960965
}
961966
}
962967

968+
private String checkForEmulation() {
969+
try {
970+
DockerClient dockerClient = DockerClientFactory.instance().client();
971+
String imageId = getContainerInfo().getImageId();
972+
String imageArch = dockerClient.inspectImageCmd(imageId).exec().getArch();
973+
String serverArch = dockerClient.versionCmd().exec().getArch();
974+
975+
if (!serverArch.equals(imageArch)) {
976+
return (
977+
"The architecture '" +
978+
imageArch +
979+
"' for image '" +
980+
getDockerImageName() +
981+
"' (ID " +
982+
imageId +
983+
") does not match the Docker server architecture '" +
984+
serverArch +
985+
"'. This will cause the container to execute much more slowly due to emulation and may lead to timeout failures."
986+
);
987+
}
988+
} catch (Exception archCheckException) {
989+
// ignore any exceptions since this is just used for a log message
990+
}
991+
return null;
992+
}
993+
963994
/**
964995
* {@inheritDoc}
965996
*/

core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ public StartupStatus checkStartupState(DockerClient dockerClient, String contain
3030
private StartupStatus checkState(InspectContainerResponse.ContainerState state) {
3131
if (Boolean.TRUE.equals(state.getRunning())) {
3232
return StartupStatus.SUCCESSFUL;
33-
} else if (!DockerStatus.isContainerExitCodeSuccess(state)) {
34-
return StartupStatus.FAILED;
33+
} else if (DockerStatus.isContainerStopped(state)) {
34+
if (DockerStatus.isContainerExitCodeSuccess(state)) {
35+
return StartupStatus.SUCCESSFUL;
36+
} else {
37+
return StartupStatus.FAILED;
38+
}
3539
} else {
3640
return StartupStatus.NOT_YET_KNOWN;
3741
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ protected void waitUntilReady() {
7474
.pollInSameThread()
7575
.pollInterval(Duration.ofMillis(100))
7676
.pollDelay(Duration.ZERO)
77+
.failFast("container is no longer running", () -> !waitStrategyTarget.isRunning())
7778
.ignoreExceptions()
7879
.forever()
7980
.until(externalCheck);

core/src/test/java/org/testcontainers/containers/GenericContainerTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.testcontainers.containers;
22

3+
import ch.qos.logback.classic.spi.ILoggingEvent;
4+
import ch.qos.logback.core.read.ListAppender;
35
import com.github.dockerjava.api.DockerClient;
46
import com.github.dockerjava.api.command.InspectContainerResponse;
57
import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState;
@@ -32,6 +34,7 @@
3234
import static org.assertj.core.api.Assertions.assertThat;
3335
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3436
import static org.assertj.core.api.Assertions.catchThrowable;
37+
import static org.assertj.core.api.Assumptions.assumeThat;
3538

3639
public class GenericContainerTest {
3740

@@ -180,6 +183,41 @@ public void shouldWaitUntilExposedPortIsMapped() {
180183
}
181184
}
182185

186+
@Test
187+
public void testArchitectureCheck() {
188+
assumeThat(DockerClientFactory.instance().client().versionCmd().exec().getArch()).isNotEqualTo("amd64");
189+
// Choose an image that is *different* from the server architecture--this ensures we always get a warning.
190+
final String image;
191+
if (DockerClientFactory.instance().client().versionCmd().exec().getArch().equals("amd64")) {
192+
// arm64 image
193+
image = "testcontainers/sshd@sha256:f701fa4ae2cd25ad2b2ea2df1aad00980f67bacdd03958a2d7d52ee63d7fb3e8";
194+
} else {
195+
// amd64 image
196+
image = "testcontainers/sshd@sha256:7879c6c99eeab01f1c6beb2c240d49a70430ef2d52f454765ec9707f547ef6f1";
197+
}
198+
199+
try (GenericContainer container = new GenericContainer<>(image)) {
200+
// Grab a copy of everything that is logged when we start the container
201+
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) container.logger();
202+
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
203+
listAppender.start();
204+
logger.addAppender(listAppender);
205+
206+
container.start();
207+
208+
String regexMatch = "The architecture '\\S+' for image .*";
209+
assertThat(listAppender.list)
210+
.describedAs(
211+
"Received log list does not have a message matching '" +
212+
regexMatch +
213+
"': " +
214+
listAppender.list.toString()
215+
)
216+
.filteredOn(event -> event.getMessage().matches(regexMatch))
217+
.isNotEmpty();
218+
}
219+
}
220+
183221
static class NoopStartupCheckStrategy extends StartupCheckStrategy {
184222

185223
@Override
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.testcontainers.containers.startupcheck;
2+
3+
import org.junit.Ignore;
4+
import org.junit.Test;
5+
import org.testcontainers.TestImages;
6+
import org.testcontainers.containers.GenericContainer;
7+
8+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
9+
10+
public class IsRunningStartupCheckStrategyTest {
11+
12+
@Test
13+
public void testCommandQuickExitSuccess() {
14+
try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/true")) {
15+
container.start(); // should start with no Exception
16+
}
17+
}
18+
19+
@Test
20+
@Ignore("This test can fail to throw an AssertionError if the container doesn't fail quickly enough")
21+
public void testCommandQuickExitFailure() {
22+
try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/false")) {
23+
assertThatThrownBy(container::start)
24+
.hasStackTraceContaining("Container startup failed")
25+
.hasStackTraceContaining("Container did not start correctly");
26+
}
27+
}
28+
29+
@Test
30+
public void testCommandStaysRunning() {
31+
try (
32+
GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/sleep", "60")
33+
) {
34+
container.start(); // should start with no Exception
35+
}
36+
}
37+
}

docs/features/startup_and_waits.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Checks that the container is running and has been running for a defined minimum
116116
[Using minimum duration strategy](../examples/junit4/generic/src/test/java/org/testcontainers/containers/startupcheck/StartupCheckStrategyTest.java) inside_block:withMinimumDurationStrategy
117117
<!--/codeinclude-->
118118

119-
### Other startup strategies
119+
### Other startup strategies
120120

121121
If none of these options meet your requirements, you can create your own subclass of
122122
[`StartupCheckStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers

0 commit comments

Comments
 (0)