Skip to content

Commit 8210e08

Browse files
ssheikinfindepinineinchnick
committed
Prevent ghost containers after retries
reportLeakedContainers adapted from trinodb/trino#20297 trinodb/trino#21280 Co-authored-by: Piotr Findeisen <[email protected]> Co-authored-by: Jan Waś <[email protected]>
1 parent 77a423c commit 8210e08

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ private void tryStart() {
550550
} else {
551551
logger().error("There are no stdout/stderr logs available for the failed container");
552552
}
553+
stop();
553554
}
554555

555556
throw new ContainerLaunchException("Could not create/start container", e);

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.github.dockerjava.api.DockerClient;
66
import com.github.dockerjava.api.command.InspectContainerResponse;
77
import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState;
8+
import com.github.dockerjava.api.model.Container;
89
import com.github.dockerjava.api.model.ExposedPort;
910
import com.github.dockerjava.api.model.Info;
1011
import com.github.dockerjava.api.model.Ports;
@@ -28,20 +29,45 @@
2829
import org.testcontainers.utility.DockerImageName;
2930
import org.testcontainers.utility.MountableFile;
3031

32+
import java.time.Duration;
3133
import java.util.Arrays;
3234
import java.util.List;
3335
import java.util.Map;
36+
import java.util.Optional;
3437
import java.util.concurrent.TimeUnit;
3538
import java.util.function.Predicate;
3639
import java.util.stream.Collectors;
3740

41+
import static com.google.common.base.MoreObjects.toStringHelper;
42+
import static com.google.common.collect.ImmutableList.toImmutableList;
43+
import static java.lang.String.format;
44+
import static java.util.Arrays.asList;
45+
import static java.util.Collections.singletonMap;
46+
import static java.util.stream.Collectors.joining;
3847
import static org.assertj.core.api.Assertions.assertThat;
3948
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4049
import static org.assertj.core.api.Assertions.catchThrowable;
4150
import static org.assertj.core.api.Assumptions.assumeThat;
51+
import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage;
4252

4353
public class GenericContainerTest {
4454

55+
@Test
56+
public void testStartupTimeoutWithAttemptsNotLeakingContainers()
57+
{
58+
try (
59+
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
60+
.withStartupAttempts(3)
61+
.waitingFor(forLogMessage("this text does not exist in logs", 1)
62+
.withStartupTimeout(Duration.ofMillis(1)))
63+
.withCommand("tail", "-f", "/dev/null");
64+
) {
65+
assertThatThrownBy(container::start)
66+
.hasStackTraceContaining("Retry limit hit with exception");
67+
}
68+
assertThat(reportLeakedContainers()).isEmpty();
69+
}
70+
4571
@Test
4672
public void shouldReportOOMAfterWait() {
4773
Info info = DockerClientFactory.instance().client().infoCmd().exec();
@@ -273,6 +299,32 @@ public void shouldRespectWaitStrategy() {
273299
}
274300
}
275301

302+
private static Optional<String> reportLeakedContainers()
303+
{
304+
@SuppressWarnings("resource") // Throws when close is attempted, as this is a global instance.
305+
DockerClient dockerClient = DockerClientFactory.lazyClient();
306+
307+
List<Container> containers = dockerClient.listContainersCmd()
308+
.withLabelFilter(singletonMap(DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL, DockerClientFactory.SESSION_ID))
309+
// ignore status "exited" - for example, failed containers after using `withStartupAttempts()`
310+
.withStatusFilter(asList("created", "restarting", "running", "paused"))
311+
.exec()
312+
.stream()
313+
.collect(toImmutableList());
314+
315+
if (containers.isEmpty()) {
316+
return Optional.empty();
317+
}
318+
319+
return Optional.of(format("Leaked containers: %s", containers.stream()
320+
.map(container -> toStringHelper("container")
321+
.add("id", container.getId())
322+
.add("image", container.getImage())
323+
.add("imageId", container.getImageId())
324+
.toString())
325+
.collect(joining(", ", "[", "]"))));
326+
}
327+
276328
static class NoopStartupCheckStrategy extends StartupCheckStrategy {
277329

278330
@Override

0 commit comments

Comments
 (0)