Skip to content

Commit 552937e

Browse files
rnorthbsideup
authored andcommitted
Improve cleanup for docker-compose. (#394)
* Improve cleanup for docker-compose. Reduce unnecessary cleanup attempts which cause errors to be logged. docker-compose down is now trusted to have cleanup up properly if it exits with a 0 status code. * Swap order of removal of containers vs networks if docker-compose down failed * Update changelog * Further improvements following code review * Reinstate semi-public methods and mark as deprecated Improve comments to aid clarity in removeNetwork method
1 parent 3cce55a commit 552937e

File tree

4 files changed

+104
-46
lines changed

4 files changed

+104
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
88
- Fixed leakage of Vibur and Tomcat JDBC test dependencies in `jdbc-test` and `mysql` modules (#382)
99
- Added timeout and retries for creation of `RemoteWebDriver` (#381, #373, #257)
1010
- Fixed various shading issues
11+
- Improved removal of containers/networks when using Docker Compose, eliminating irrelevant errors during cleanup (#342, #394)
1112

1213
### Changed
1314
- Added support for Docker networks (#372)

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

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.google.common.base.Splitter;
99
import com.google.common.collect.Maps;
1010
import com.google.common.util.concurrent.Uninterruptibles;
11+
import org.apache.commons.lang.StringUtils;
1112
import org.junit.runner.Description;
1213
import org.rnorth.ducttape.ratelimits.RateLimiter;
1314
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
@@ -46,8 +47,9 @@ public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>> e
4647
private final String identifier;
4748
private final Map<String, AmbassadorContainer> ambassadorContainers = new HashMap<>();
4849
private final List<File> composeFiles;
49-
private Set<String> spawnedContainerIds = Collections.emptySet();
50-
private Map<String, Integer> scalingPreferences = new HashMap<>();
50+
private final Set<String> spawnedContainerIds = new HashSet<>();
51+
private final Set<String> spawnedNetworkIds = new HashSet<>();
52+
private final Map<String, Integer> scalingPreferences = new HashMap<>();
5153
private DockerClient dockerClient;
5254
private boolean localCompose;
5355
private boolean pull = true;
@@ -116,15 +118,13 @@ public void starting(Description description) {
116118
}
117119

118120
private void pullImages() {
119-
getDockerCompose("pull")
120-
.start();
121+
runWithCompose("pull");
121122
}
122123

123124

124125
private void createServices() {
125-
// Start the docker-compose container, which starts up the services
126-
getDockerCompose("up -d")
127-
.start();
126+
// Run the docker-compose container, which starts up the services
127+
runWithCompose("up -d");
128128
}
129129

130130
private void tailChildContainerLogs() {
@@ -137,16 +137,18 @@ private void tailChildContainerLogs() {
137137
);
138138
}
139139

140-
private DockerCompose getDockerCompose(String cmd) {
140+
private void runWithCompose(String cmd) {
141141
final DockerCompose dockerCompose;
142142
if (localCompose) {
143143
dockerCompose = new LocalDockerCompose(composeFiles, identifier);
144144
} else {
145145
dockerCompose = new ContainerisedDockerCompose(composeFiles, identifier);
146146
}
147-
return dockerCompose
147+
148+
dockerCompose
148149
.withCommand(cmd)
149-
.withEnv(env);
150+
.withEnv(env)
151+
.invoke();
150152
}
151153

152154
private void applyScaling() {
@@ -157,8 +159,7 @@ private void applyScaling() {
157159
sb.append(" ").append(scale.getKey()).append("=").append(scale.getValue());
158160
}
159161

160-
getDockerCompose(sb.toString())
161-
.start();
162+
runWithCompose(sb.toString());
162163
}
163164
}
164165

@@ -171,19 +172,18 @@ private void registerContainersForShutdown() {
171172
containers.forEach(container ->
172173
ResourceReaper.instance().registerContainerForCleanup(container.getId(), container.getNames()[0]));
173174

174-
// Ensure that the default network for this compose environment, if any, is also cleaned up
175-
ResourceReaper.instance().registerNetworkForCleanup(identifier + "_default");
176175
// Compose can define their own networks as well; ensure these are cleaned up
177176
dockerClient.listNetworksCmd().exec().forEach(network -> {
178177
if (network.getName().contains(identifier)) {
179-
ResourceReaper.instance().registerNetworkForCleanup(network.getId());
178+
spawnedNetworkIds.add(network.getId());
179+
ResourceReaper.instance().registerNetworkIdForCleanup(network.getId());
180180
}
181181
});
182182

183183
// remember the IDs to allow containers to be killed as soon as we reach stop()
184-
spawnedContainerIds = containers.stream()
184+
spawnedContainerIds.addAll(containers.stream()
185185
.map(Container::getId)
186-
.collect(Collectors.toSet());
186+
.collect(Collectors.toSet()));
187187

188188
} catch (DockerException e) {
189189
logger().debug("Failed to stop a service container with exception", e);
@@ -240,15 +240,25 @@ public void finished(Description description) {
240240
ambassadorContainers.forEach((String address, AmbassadorContainer container) -> container.stop());
241241

242242
// Kill the services using docker-compose
243-
getDockerCompose("down -v")
244-
.start();
243+
try {
244+
runWithCompose("down -v");
245+
246+
// If we reach here then docker-compose down has cleared networks and containers;
247+
// we can unregister from ResourceReaper
248+
spawnedContainerIds.forEach(ResourceReaper.instance()::unregisterContainer);
249+
spawnedNetworkIds.forEach(ResourceReaper.instance()::unregisterNetwork);
250+
} catch (Exception e) {
251+
// docker-compose down failed; use ResourceReaper to ensure cleanup
245252

246-
// remove the networks before removing the containers
247-
ResourceReaper.instance().removeNetworks(identifier);
253+
// kill the spawned service containers
254+
spawnedContainerIds.forEach(ResourceReaper.instance()::stopAndRemoveContainer);
255+
256+
// remove the networks after removing the containers
257+
spawnedNetworkIds.forEach(ResourceReaper.instance()::removeNetworkById);
258+
}
248259

249-
// kill the spawned service containers
250-
spawnedContainerIds.forEach(id -> ResourceReaper.instance().stopAndRemoveContainer(id));
251260
spawnedContainerIds.clear();
261+
spawnedNetworkIds.clear();
252262
}
253263
}
254264

@@ -372,7 +382,7 @@ interface DockerCompose {
372382

373383
DockerCompose withEnv(Map<String, String> env);
374384

375-
void start();
385+
void invoke();
376386

377387
default void validateFileList(List<File> composeFiles) {
378388
checkNotNull(composeFiles);
@@ -417,7 +427,7 @@ public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
417427
}
418428

419429
@Override
420-
public void start() {
430+
public void invoke() {
421431
super.start();
422432

423433
this.followOutput(new Slf4jLogConsumer(logger()));
@@ -431,6 +441,19 @@ public void start() {
431441
logger().info("Docker Compose has finished running");
432442

433443
AuditLogger.doComposeLog(this.getCommandParts(), this.getEnv());
444+
445+
final Integer exitCode = this.dockerClient.inspectContainerCmd(containerId)
446+
.exec()
447+
.getState()
448+
.getExitCode();
449+
450+
if (exitCode == null || exitCode != 0) {
451+
throw new ContainerLaunchException(
452+
"Containerised Docker Compose exited abnormally with code " +
453+
exitCode +
454+
" whilst running command: " +
455+
StringUtils.join(this.getCommandParts(), ' '));
456+
}
434457
}
435458
}
436459

@@ -468,7 +491,7 @@ public DockerCompose withEnv(Map<String, String> env) {
468491
}
469492

470493
@Override
471-
public void start() {
494+
public void invoke() {
472495
// bail out early
473496
if (!CommandLine.executableExists(COMPOSE_EXECUTABLE)) {
474497
throw new ContainerLaunchException("Local Docker Compose not found. Is " + COMPOSE_EXECUTABLE + " on the PATH?");

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public interface Network extends AutoCloseable, TestRule {
2020

2121
@Override
2222
default void close() {
23-
ResourceReaper.instance().removeNetworks(getId());
23+
ResourceReaper.instance().removeNetworkById(getId());
2424
}
2525

2626
static Network newNetwork() {
@@ -66,7 +66,7 @@ private String create() {
6666
}
6767

6868
String id = createNetworkCmd.exec().getId();
69-
ResourceReaper.instance().registerNetworkForCleanup(id);
69+
ResourceReaper.instance().registerNetworkIdForCleanup(id);
7070
return id;
7171
}
7272

core/src/main/java/org/testcontainers/utility/ResourceReaper.java

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
import org.slf4j.LoggerFactory;
1111
import org.testcontainers.DockerClientFactory;
1212

13-
import java.util.*;
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Set;
1417
import java.util.concurrent.ConcurrentHashMap;
1518

1619
/**
@@ -127,49 +130,80 @@ private void stopContainer(String containerId, String imageName) {
127130
/**
128131
* Register a network to be cleaned up at JVM shutdown.
129132
*
130-
* @param networkName the image name of the network
133+
* @param id the ID of the network
131134
*/
135+
public void registerNetworkIdForCleanup(String id) {
136+
registeredNetworks.add(id);
137+
}
138+
139+
/**
140+
* @param networkName the name of the network
141+
* @deprecated see {@link ResourceReaper#registerNetworkIdForCleanup(String)}
142+
*/
143+
@Deprecated
132144
public void registerNetworkForCleanup(String networkName) {
133-
registeredNetworks.add(networkName);
145+
try {
146+
// Try to find the network by name, so that we can register its ID for later deletion
147+
dockerClient.listNetworksCmd()
148+
.withNameFilter(networkName)
149+
.exec()
150+
.forEach(network -> registerNetworkIdForCleanup(network.getId()));
151+
} catch (Exception e) {
152+
LOGGER.trace("Error encountered when looking up network (name: {})", networkName);
153+
}
154+
}
155+
156+
/**
157+
* Removes a network by ID.
158+
* @param id
159+
*/
160+
public void removeNetworkById(String id) {
161+
removeNetwork(id);
134162
}
135163

136164
/**
137-
* Removes any networks that contain the identifier.
165+
* Removes a network by ID.
138166
* @param identifier
167+
* @deprecated see {@link ResourceReaper#removeNetworkById(String)}
139168
*/
169+
@Deprecated
140170
public void removeNetworks(String identifier) {
141-
removeNetwork(identifier);
171+
removeNetworkById(identifier);
142172
}
143173

144-
private void removeNetwork(String networkName) {
174+
private void removeNetwork(String id) {
145175
try {
146-
try {
147-
// First try to remove by name
148-
dockerClient.removeNetworkCmd(networkName).exec();
149-
} catch (Exception e) {
150-
LOGGER.trace("Error encountered removing network by name ({}) - it may not have been removed", networkName);
151-
}
152-
153176
List<Network> networks;
154177
try {
155-
// Then try to list all networks with the same name
156-
networks = dockerClient.listNetworksCmd().withNameFilter(networkName).exec();
178+
// Try to find the network if it still exists
179+
// Listing by ID first prevents docker-java logging an error if we just go blindly into removeNetworkCmd
180+
networks = dockerClient.listNetworksCmd().withIdFilter(id).exec();
157181
} catch (Exception e) {
158-
LOGGER.trace("Error encountered when looking up network for removal (name: {}) - it may not have been removed", networkName);
182+
LOGGER.trace("Error encountered when looking up network for removal (name: {}) - it may not have been removed", id);
159183
return;
160184
}
161185

186+
// at this point networks should contain either 0 or 1 entries, depending on whether the network exists
187+
// using a for loop we essentially treat the network like an optional, only applying the removal if it exists
162188
for (Network network : networks) {
163189
try {
164190
dockerClient.removeNetworkCmd(network.getId()).exec();
165191
registeredNetworks.remove(network.getId());
166-
LOGGER.debug("Removed network: {}", networkName);
192+
LOGGER.debug("Removed network: {}", id);
167193
} catch (Exception e) {
168194
LOGGER.trace("Error encountered removing network (name: {}) - it may not have been removed", network.getName());
169195
}
170196
}
171197
} finally {
172-
registeredNetworks.remove(networkName);
198+
registeredNetworks.remove(id);
173199
}
174200
}
201+
202+
public void unregisterNetwork(String identifier) {
203+
registeredNetworks.remove(identifier);
204+
}
205+
206+
public void unregisterContainer(String identifier) {
207+
registeredContainers.remove(identifier);
208+
}
175209
}

0 commit comments

Comments
 (0)