diff --git a/core/src/main/java/org/testcontainers/DockerClientFactory.java b/core/src/main/java/org/testcontainers/DockerClientFactory.java index 36c4c717b76..1ad7a0bec7e 100644 --- a/core/src/main/java/org/testcontainers/DockerClientFactory.java +++ b/core/src/main/java/org/testcontainers/DockerClientFactory.java @@ -225,6 +225,9 @@ public void close() { } else { log.debug("Ryuk is disabled"); ryukContainerId = null; + // best-efforts cleanup at JVM shutdown, without using the Ryuk container + //noinspection deprecation + ResourceReaper.instance().setHook(); } boolean checksEnabled = !TestcontainersConfiguration.getInstance().isDisableChecks(); diff --git a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java index 961d066cb91..b56c70cb959 100644 --- a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java +++ b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java @@ -41,7 +41,6 @@ import java.io.File; import java.time.Duration; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -56,7 +55,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkArgument; @@ -310,8 +308,8 @@ private void runWithCompose(String cmd) { } private void registerContainersForShutdown() { - ResourceReaper.instance().registerFilterForCleanup(Arrays.asList( - new SimpleEntry<>("label", "com.docker.compose.project=" + project) + ResourceReaper.instance().registerLabelsFilterForCleanup(Collections.singletonMap( + "com.docker.compose.project", project )); } diff --git a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java index f43835fd01b..8c2cd3a6108 100644 --- a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java @@ -5,12 +5,14 @@ import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.Network; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; +import com.github.dockerjava.api.model.PruneType; import com.github.dockerjava.api.model.Volume; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; @@ -36,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -58,7 +61,14 @@ public final class ResourceReaper { private static final Logger LOGGER = LoggerFactory.getLogger(ResourceReaper.class); - private static final List>> DEATH_NOTE = new ArrayList<>(); + private static final List>> DEATH_NOTE = new ArrayList<>( + Arrays.asList( + DockerClientFactory.DEFAULT_LABELS.entrySet().stream() + .>map(it -> new SimpleEntry<>("label", it.getKey() + "=" + it.getValue())) + .collect(Collectors.toList()) + ) + ); + private static final RateLimiter RYUK_ACK_RATE_LIMITER = RateLimiterBuilder .newBuilder() .withRate(4, TimeUnit.SECONDS) @@ -66,16 +76,13 @@ public final class ResourceReaper { .build(); private static ResourceReaper instance; - private final DockerClient dockerClient; + private static AtomicBoolean ryukStarted = new AtomicBoolean(false); + private final DockerClient dockerClient = DockerClientFactory.lazyClient(); private Map registeredContainers = new ConcurrentHashMap<>(); private Set registeredNetworks = Sets.newConcurrentHashSet(); private Set registeredImages = Sets.newConcurrentHashSet(); private AtomicBoolean hookIsSet = new AtomicBoolean(false); - private ResourceReaper() { - dockerClient = DockerClientFactory.instance().client(); - } - /** * @@ -173,14 +180,6 @@ public InspectContainerResponse getContainerInfo() { CountDownLatch ryukScheduledLatch = new CountDownLatch(1); - synchronized (DEATH_NOTE) { - DEATH_NOTE.add( - DockerClientFactory.DEFAULT_LABELS.entrySet().stream() - .>map(it -> new SimpleEntry<>("label", it.getKey() + "=" + it.getValue())) - .collect(Collectors.toList()) - ); - } - String host = containerState.getHost(); Integer ryukPort = containerState.getFirstMappedPort(); Thread kiraThread = new Thread( @@ -238,6 +237,7 @@ public InspectContainerResponse getContainerInfo() { } } + ryukStarted.set(true); return ryukContainerId; } @@ -253,7 +253,7 @@ public synchronized static ResourceReaper instance() { * Perform a cleanup. */ public synchronized void performCleanup() { - registeredContainers.forEach(this::stopContainer); + registeredContainers.forEach(this::removeContainer); registeredNetworks.forEach(this::removeNetwork); registeredImages.forEach(this::removeImage); } @@ -262,7 +262,9 @@ public synchronized void performCleanup() { * Register a filter to be cleaned up. * * @param filter the filter + * @deprecated only label filter is supported by the prune API, use {@link #registerLabelsFilterForCleanup(Map)} */ + @Deprecated public void registerFilterForCleanup(List> filter) { synchronized (DEATH_NOTE) { DEATH_NOTE.add(filter); @@ -270,6 +272,19 @@ public void registerFilterForCleanup(List> filter) { } } + /** + * Register a label to be cleaned up. + * + * @param labels the filter + */ + public void registerLabelsFilterForCleanup(Map labels) { + registerFilterForCleanup( + labels.entrySet().stream() + .map(it -> new SimpleEntry<>("label", it.getKey() + "=" + it.getValue())) + .collect(Collectors.toList()) + ); + } + /** * Register a container to be cleaned up, either on explicit call to stopAndRemoveContainer, or at JVM shutdown. * @@ -287,7 +302,7 @@ public void registerContainerForCleanup(String containerId, String imageName) { * @param containerId the ID of the container */ public void stopAndRemoveContainer(String containerId) { - stopContainer(containerId, registeredContainers.get(containerId)); + removeContainer(containerId, registeredContainers.get(containerId)); registeredContainers.remove(containerId); } @@ -299,12 +314,12 @@ public void stopAndRemoveContainer(String containerId) { * @param imageName the image name of the container (used for logging) */ public void stopAndRemoveContainer(String containerId, String imageName) { - stopContainer(containerId, imageName); + removeContainer(containerId, imageName); registeredContainers.remove(containerId); } - private void stopContainer(String containerId, String imageName) { + private void removeContainer(String containerId, String imageName) { boolean running; try { InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); @@ -444,10 +459,52 @@ private void removeImage(String dockerImageName) { } } - private void setHook() { + private void prune(PruneType pruneType, List> filters) { + String[] labels = filters.stream() + .filter(it -> "label".equals(it.getKey())) + .map(Map.Entry::getValue) + .toArray(String[]::new); + switch (pruneType) { + // Docker only prunes stopped containers, so we have to do it manually + case CONTAINERS: + List containers = dockerClient.listContainersCmd() + .withFilter("label", Arrays.asList(labels)) + .withShowAll(true) + .exec(); + + containers.parallelStream().forEach(container -> { + removeContainer(container.getId(), container.getImage()); + }); + break; + default: + dockerClient.pruneCmd(pruneType).withLabelFilter(labels).exec(); + break; + } + } + + /** + * @deprecated internal API, not intended for public usage + */ + @Deprecated + public void setHook() { if (hookIsSet.compareAndSet(false, true)) { // If the JVM stops without containers being stopped, try and stop the container. - Runtime.getRuntime().addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, this::performCleanup)); + Runtime.getRuntime().addShutdownHook( + new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, + () -> { + performCleanup(); + + if (!ryukStarted.get()) { + synchronized (DEATH_NOTE) { + DEATH_NOTE.forEach(filters -> prune(PruneType.CONTAINERS, filters)); + DEATH_NOTE.forEach(filters -> prune(PruneType.NETWORKS, filters)); + DEATH_NOTE.forEach(filters -> prune(PruneType.VOLUMES, filters)); + DEATH_NOTE.forEach(filters -> prune(PruneType.IMAGES, filters)); + } + } + } + ) + ); } }