Skip to content

Commit fb869c0

Browse files
rnorthbsideup
andcommitted
Prevent duplicate attempts to start Ryuk container (#2245)
Prevent duplicate attempts to start Ryuk container If for example, checks fail once but the Docker client is otherwise initialized, an error would occur due to an attempt to start a Ryuk container twice with the same name. This changed is aimed at avoiding that situation. Additionally, if checks fail once they should fail on every subsequent attempt to start a client. Therefore, we cache the failure and rethrow it. Co-authored-by: Sergei Egorov <[email protected]>
1 parent 7143939 commit fb869c0

File tree

3 files changed

+96
-42
lines changed

3 files changed

+96
-42
lines changed

core/src/main/java/org/testcontainers/DockerClientFactory.java

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
import lombok.SneakyThrows;
1818
import lombok.Synchronized;
1919
import lombok.extern.slf4j.Slf4j;
20-
import org.hamcrest.BaseMatcher;
21-
import org.hamcrest.Description;
22-
import org.rnorth.visibleassertions.VisibleAssertions;
2320
import org.testcontainers.dockerclient.DockerClientProviderStrategy;
2421
import org.testcontainers.dockerclient.DockerMachineClientProviderStrategy;
2522
import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback;
@@ -68,12 +65,16 @@ public class DockerClientFactory {
6865
@VisibleForTesting
6966
DockerClient dockerClient;
7067

68+
@VisibleForTesting
69+
RuntimeException cachedChecksFailure;
70+
7171
private String activeApiVersion;
7272
private String activeExecutionDriver;
7373

7474
@Getter(lazy = true)
7575
private final boolean fileMountingSupported = checkMountableFile();
7676

77+
7778
static {
7879
System.setProperty("org.testcontainers.shaded.io.netty.packagePrefix", "org.testcontainers.shaded.");
7980
}
@@ -124,6 +125,12 @@ public DockerClient client() {
124125
return dockerClient;
125126
}
126127

128+
// fail-fast if checks have failed previously
129+
if (cachedChecksFailure != null) {
130+
log.debug("There is a cached checks failure - throwing", cachedChecksFailure);
131+
throw cachedChecksFailure;
132+
}
133+
127134
final DockerClientProviderStrategy strategy = getOrInitializeStrategy();
128135

129136
String hostIpAddress = strategy.getDockerHostIpAddress();
@@ -140,51 +147,56 @@ public DockerClient client() {
140147
" Operating System: " + dockerInfo.getOperatingSystem() + "\n" +
141148
" Total Memory: " + dockerInfo.getMemTotal() / (1024 * 1024) + " MB");
142149

143-
String ryukContainerId = null;
150+
final String ryukContainerId;
151+
144152
boolean useRyuk = !Boolean.parseBoolean(System.getenv("TESTCONTAINERS_RYUK_DISABLED"));
145153
if (useRyuk) {
154+
log.debug("Ryuk is enabled");
146155
ryukContainerId = ResourceReaper.start(hostIpAddress, client);
147156
log.info("Ryuk started - will monitor and terminate Testcontainers containers on JVM exit");
157+
} else {
158+
log.debug("Ryuk is disabled");
159+
ryukContainerId = null;
148160
}
149161

150162
boolean checksEnabled = !TestcontainersConfiguration.getInstance().isDisableChecks();
151163
if (checksEnabled) {
152-
VisibleAssertions.info("Checking the system...");
153-
checkDockerVersion(version.getVersion());
154-
if (ryukContainerId != null) {
155-
checkDiskSpace(client, ryukContainerId);
156-
} else {
157-
runInsideDocker(
158-
client,
159-
createContainerCmd -> {
160-
createContainerCmd.withName("testcontainers-checks-" + SESSION_ID);
161-
createContainerCmd.getHostConfig().withAutoRemove(true);
162-
createContainerCmd.withCmd("tail", "-f", "/dev/null");
163-
},
164-
(__, containerId) -> {
165-
checkDiskSpace(client, containerId);
166-
return "";
167-
}
168-
);
164+
log.debug("Checks are enabled");
165+
166+
try {
167+
log.info("Checking the system...");
168+
checkDockerVersion(version.getVersion());
169+
if (ryukContainerId != null) {
170+
checkDiskSpace(client, ryukContainerId);
171+
} else {
172+
runInsideDocker(
173+
client,
174+
createContainerCmd -> {
175+
createContainerCmd.withName("testcontainers-checks-" + SESSION_ID);
176+
createContainerCmd.getHostConfig().withAutoRemove(true);
177+
createContainerCmd.withCmd("tail", "-f", "/dev/null");
178+
},
179+
(__, containerId) -> {
180+
checkDiskSpace(client, containerId);
181+
return "";
182+
}
183+
);
184+
}
185+
} catch (RuntimeException e) {
186+
cachedChecksFailure = e;
187+
throw e;
169188
}
189+
} else {
190+
log.debug("Checks are disabled");
170191
}
171192

172193
dockerClient = client;
173194
return dockerClient;
174195
}
175196

176197
private void checkDockerVersion(String dockerVersion) {
177-
VisibleAssertions.assertThat("Docker version", dockerVersion, new BaseMatcher<String>() {
178-
@Override
179-
public boolean matches(Object o) {
180-
return new ComparableVersion(o.toString()).compareTo(new ComparableVersion("1.6.0")) >= 0;
181-
}
182-
183-
@Override
184-
public void describeTo(Description description) {
185-
description.appendText("should be at least 1.6.0");
186-
}
187-
});
198+
boolean versionIsSufficient = new ComparableVersion(dockerVersion).compareTo(new ComparableVersion("1.6.0")) >= 0;
199+
check("Docker server version should be at least 1.6.0", versionIsSufficient);
188200
}
189201

190202
private void checkDiskSpace(DockerClient dockerClient, String id) {
@@ -201,12 +213,21 @@ private void checkDiskSpace(DockerClient dockerClient, String id) {
201213

202214
DiskSpaceUsage df = parseAvailableDiskSpace(outputStream.toString());
203215

204-
VisibleAssertions.assertTrue(
216+
check(
205217
"Docker environment should have more than 2GB free disk space",
206218
df.availableMB.map(it -> it >= 2048).orElse(true)
207219
);
208220
}
209221

222+
private void check(String message, boolean isSuccessful) {
223+
if (isSuccessful) {
224+
log.info("✔︎ {}", message);
225+
} else {
226+
log.error("❌ {}", message);
227+
throw new IllegalStateException("Check failed: " + message);
228+
}
229+
}
230+
210231
private boolean checkMountableFile() {
211232
DockerClient dockerClient = client();
212233

@@ -267,8 +288,8 @@ private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd>
267288
} finally {
268289
try {
269290
client.removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec();
270-
} catch (NotFoundException | InternalServerErrorException ignored) {
271-
log.debug("", ignored);
291+
} catch (NotFoundException | InternalServerErrorException e) {
292+
log.debug("Swallowed exception while removing container", e);
272293
}
273294
}
274295
}
@@ -286,7 +307,7 @@ DiskSpaceUsage parseAvailableDiskSpace(String dfOutput) {
286307
for (String line : lines) {
287308
String[] fields = line.split("\\s+");
288309
if (fields.length > 5 && fields[5].equals("/")) {
289-
long availableKB = Long.valueOf(fields[3]);
310+
long availableKB = Long.parseLong(fields[3]);
290311
df.availableMB = Optional.of(availableKB / 1024L);
291312
df.usedPercent = Optional.of(Integer.valueOf(fields[4].replace("%", "")));
292313
break;
@@ -318,10 +339,4 @@ public String getActiveExecutionDriver() {
318339
public boolean isUsing(Class<? extends DockerClientProviderStrategy> providerStrategyClass) {
319340
return strategy != null && providerStrategyClass.isAssignableFrom(this.strategy.getClass());
320341
}
321-
322-
private static class NotEnoughDiskSpaceException extends RuntimeException {
323-
NotEnoughDiskSpaceException(String message) {
324-
super(message);
325-
}
326-
}
327342
}

core/src/test/java/org/testcontainers/DockerClientFactoryTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
package org.testcontainers;
22

3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.exception.DockerException;
35
import com.github.dockerjava.api.exception.NotFoundException;
6+
import org.junit.Rule;
47
import org.junit.Test;
8+
import org.mockito.Mockito;
59
import org.rnorth.visibleassertions.VisibleAssertions;
610
import org.testcontainers.DockerClientFactory.DiskSpaceUsage;
711
import org.testcontainers.dockerclient.LogToStringContainerCallback;
12+
import org.testcontainers.utility.MockTestcontainersConfigurationRule;
813
import org.testcontainers.utility.TestcontainersConfiguration;
914

1015
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1117

1218
/**
1319
* Test for {@link DockerClientFactory}.
1420
*/
1521
public class DockerClientFactoryTest {
1622

23+
@Rule
24+
public MockTestcontainersConfigurationRule configurationMock = new MockTestcontainersConfigurationRule();
25+
1726
@Test
1827
public void runCommandInsideDockerShouldNotFailIfImageDoesNotExistsLocally() {
1928

@@ -52,4 +61,31 @@ public void dockerHostIpAddress() {
5261
instance.strategy = null;
5362
assertThat(instance.dockerHostIpAddress()).isNotNull();
5463
}
64+
65+
@Test
66+
public void failedChecksFailFast() {
67+
Mockito.doReturn(false).when(TestcontainersConfiguration.getInstance()).isDisableChecks();
68+
69+
// Make sure that Ryuk is started
70+
assertThat(DockerClientFactory.instance().client()).isNotNull();
71+
72+
DockerClientFactory instance = new DockerClientFactory();
73+
DockerClient dockerClient = instance.dockerClient;
74+
assertThat(instance.cachedChecksFailure).isNull();
75+
try {
76+
// Remove cached client to force the initialization logic
77+
instance.dockerClient = null;
78+
79+
// Ryuk should fail to start twice due to the name conflict (equal to the session id)
80+
assertThatThrownBy(instance::client).isInstanceOf(DockerException.class);
81+
82+
RuntimeException failure = new IllegalStateException("Boom!");
83+
instance.cachedChecksFailure = failure;
84+
// Fail fast
85+
assertThatThrownBy(instance::client).isEqualTo(failure);
86+
} finally {
87+
instance.dockerClient = dockerClient;
88+
instance.cachedChecksFailure = null;
89+
}
90+
}
5591
}

core/src/test/java/org/testcontainers/utility/MockTestcontainersConfigurationRule.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public Statement apply(@NotNull Statement base, @NotNull Description description
2323
@Override
2424
public void evaluate() throws Throwable {
2525
TestcontainersConfiguration previous = REF.get();
26+
if (previous == null) {
27+
previous = TestcontainersConfiguration.getInstance();
28+
}
2629
REF.set(Mockito.spy(previous));
2730

2831
try {

0 commit comments

Comments
 (0)