Skip to content

Commit aca9a93

Browse files
authored
#369 Daemonize threads (#646)
* #369 Mark all long-living threads started by Testcontainers as daemons and group them * simplify the classpath logic * fixes * Add a line about the logs to CHANGELOG and explain the DaemonTest and why we fork
1 parent 9fb8e4b commit aca9a93

File tree

10 files changed

+379
-9
lines changed

10 files changed

+379
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file.
1010

1111
### Changed
1212
- Support multiple HTTP status codes for HttpWaitStrategy ([\#630](https://github.com/testcontainers/testcontainers-java/issues/630))
13+
- Mark all long-living threads started by Testcontainers as daemons and group them. ([\#646](https://github.com/testcontainers/testcontainers-java/issues/646))
14+
- Remove noisy `DEBUG` logging of Netty packets ([\#646](https://github.com/testcontainers/testcontainers-java/issues/646))
1315

1416
## [1.7.0] - 2018-04-07
1517

core/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ dependencies {
106106
compile 'org.apache.commons:commons-compress:1.15'
107107
// Added for JDK9 compatibility since it's missing this package
108108
compile 'javax.xml.bind:jaxb-api:2.3.0'
109-
compile 'org.rnorth.duct-tape:duct-tape:1.0.6'
109+
compile 'org.rnorth.duct-tape:duct-tape:1.0.7'
110110
compile 'org.rnorth.visible-assertions:visible-assertions:2.1.0'
111111

112-
shaded ('com.github.docker-java:docker-java:3.0.12') {
112+
shaded ('com.github.docker-java:docker-java:3.1.0-rc-2') {
113113
exclude(group: 'org.glassfish.jersey.core')
114114
exclude(group: 'org.glassfish.jersey.connectors')
115115
exclude(group: 'log4j')

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
@Slf4j
4040
public class DockerClientFactory {
4141

42+
public static final ThreadGroup TESTCONTAINERS_THREAD_GROUP = new ThreadGroup("testcontainers");
4243
public static final String TESTCONTAINERS_LABEL = DockerClientFactory.class.getPackage().getName();
4344
public static final String TESTCONTAINERS_SESSION_ID_LABEL = TESTCONTAINERS_LABEL + ".sessionId";
4445

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ protected Path createVolumeDirectory(boolean temporary) {
321321
Path directory = new File(".tmp-volume-" + System.currentTimeMillis()).toPath();
322322
PathUtils.mkdirp(directory);
323323

324-
if (temporary) Runtime.getRuntime().addShutdownHook(new Thread(() -> {
324+
if (temporary) Runtime.getRuntime().addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> {
325325
PathUtils.recursiveDeleteDir(directory);
326326
}));
327327

@@ -478,7 +478,7 @@ private void applyConfiguration(CreateContainerCmd createCommand) {
478478

479479
private Set<Link> findLinksFromThisContainer(String alias, LinkableContainer linkableContainer) {
480480
return dockerClient.listContainersCmd()
481-
.withStatusFilter("running")
481+
.withStatusFilter(Arrays.asList("running"))
482482
.exec().stream()
483483
.flatMap(container -> Stream.of(container.getNames()))
484484
.filter(name -> name.endsWith(linkableContainer.getContainerName()))

core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.github.dockerjava.api.DockerClient;
44
import com.github.dockerjava.core.DockerClientBuilder;
55
import com.github.dockerjava.core.DockerClientConfig;
6-
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
76
import com.google.common.base.Throwables;
87
import org.apache.commons.io.IOUtils;
98
import org.jetbrains.annotations.Nullable;
@@ -13,6 +12,7 @@
1312
import org.rnorth.ducttape.unreliables.Unreliables;
1413
import org.slf4j.Logger;
1514
import org.slf4j.LoggerFactory;
15+
import org.testcontainers.dockerclient.transport.TestcontainersDockerCmdExecFactory;
1616
import org.testcontainers.utility.TestcontainersConfiguration;
1717

1818
import java.util.ArrayList;
@@ -166,7 +166,7 @@ public DockerClient getClient() {
166166
protected DockerClient getClientForConfig(DockerClientConfig config) {
167167
return DockerClientBuilder
168168
.getInstance(config)
169-
.withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
169+
.withDockerCmdExecFactory(new TestcontainersDockerCmdExecFactory())
170170
.build();
171171
}
172172

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package org.testcontainers.dockerclient.transport;
2+
3+
import com.github.dockerjava.api.command.DockerCmdExecFactory;
4+
import com.github.dockerjava.core.AbstractDockerCmdExecFactory;
5+
import com.github.dockerjava.core.DockerClientConfig;
6+
import com.github.dockerjava.core.SSLConfig;
7+
import com.github.dockerjava.core.WebTarget;
8+
import com.github.dockerjava.netty.NettyWebTarget;
9+
import io.netty.bootstrap.Bootstrap;
10+
import io.netty.channel.*;
11+
import io.netty.channel.epoll.EpollDomainSocketChannel;
12+
import io.netty.channel.epoll.EpollEventLoopGroup;
13+
import io.netty.channel.kqueue.KQueueDomainSocketChannel;
14+
import io.netty.channel.kqueue.KQueueEventLoopGroup;
15+
import io.netty.channel.nio.NioEventLoopGroup;
16+
import io.netty.channel.socket.DuplexChannel;
17+
import io.netty.channel.socket.SocketChannel;
18+
import io.netty.channel.socket.nio.NioSocketChannel;
19+
import io.netty.channel.unix.DomainSocketAddress;
20+
import io.netty.channel.unix.UnixChannel;
21+
import io.netty.handler.codec.http.HttpClientCodec;
22+
import io.netty.handler.ssl.SslHandler;
23+
import io.netty.handler.timeout.IdleState;
24+
import io.netty.handler.timeout.IdleStateEvent;
25+
import io.netty.handler.timeout.IdleStateHandler;
26+
import io.netty.util.concurrent.DefaultThreadFactory;
27+
import org.apache.commons.lang.SystemUtils;
28+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
29+
import org.testcontainers.DockerClientFactory;
30+
31+
import javax.net.ssl.SSLEngine;
32+
import javax.net.ssl.SSLParameters;
33+
import java.io.IOException;
34+
import java.net.SocketTimeoutException;
35+
import java.security.Security;
36+
import java.util.concurrent.ThreadFactory;
37+
import java.util.concurrent.TimeUnit;
38+
39+
import static com.google.common.base.Preconditions.checkNotNull;
40+
41+
/**
42+
* This class is a modified version of docker-java's NettyDockerCmdExecFactory v3.1.0-rc-2
43+
* Changes:
44+
* - daemonized thread factory
45+
* - thread group
46+
* - the logging handler removed
47+
* -
48+
*/
49+
public class TestcontainersDockerCmdExecFactory extends AbstractDockerCmdExecFactory implements DockerCmdExecFactory {
50+
51+
private static final String THREAD_PREFIX = "testcontainers-netty";
52+
53+
/*
54+
* useful links:
55+
*
56+
* http://stackoverflow.com/questions/33296749/netty-connect-to-unix-domain-socket-failed
57+
* http://netty.io/wiki/native-transports.html
58+
* https://github.com/netty/netty/blob/master/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java
59+
* https://github.com/slandelle/netty-request-chunking/blob/master/src/test/java/slandelle/ChunkingTest.java
60+
*/
61+
62+
private Bootstrap bootstrap;
63+
64+
private EventLoopGroup eventLoopGroup;
65+
66+
private NettyInitializer nettyInitializer;
67+
68+
private WebTarget baseResource;
69+
70+
private Integer connectTimeout = null;
71+
72+
private Integer readTimeout = null;
73+
74+
@Override
75+
public void init(DockerClientConfig dockerClientConfig) {
76+
super.init(dockerClientConfig);
77+
78+
bootstrap = new Bootstrap();
79+
80+
String scheme = dockerClientConfig.getDockerHost().getScheme();
81+
82+
if ("unix".equals(scheme)) {
83+
nettyInitializer = new UnixDomainSocketInitializer();
84+
} else if ("tcp".equals(scheme)) {
85+
nettyInitializer = new InetSocketInitializer();
86+
}
87+
88+
eventLoopGroup = nettyInitializer.init(bootstrap, dockerClientConfig);
89+
90+
baseResource = new NettyWebTarget(this::connect).path(dockerClientConfig.getApiVersion().asWebPathPart());
91+
}
92+
93+
private DuplexChannel connect() {
94+
try {
95+
return connect(bootstrap);
96+
} catch (InterruptedException e) {
97+
throw new RuntimeException(e);
98+
}
99+
}
100+
101+
private DuplexChannel connect(final Bootstrap bootstrap) throws InterruptedException {
102+
return nettyInitializer.connect(bootstrap);
103+
}
104+
105+
private interface NettyInitializer {
106+
EventLoopGroup init(final Bootstrap bootstrap, DockerClientConfig dockerClientConfig);
107+
108+
DuplexChannel connect(final Bootstrap bootstrap) throws InterruptedException;
109+
}
110+
111+
private class UnixDomainSocketInitializer implements NettyInitializer {
112+
@Override
113+
public EventLoopGroup init(Bootstrap bootstrap, DockerClientConfig dockerClientConfig) {
114+
if (SystemUtils.IS_OS_LINUX) {
115+
return epollGroup();
116+
} else if (SystemUtils.IS_OS_MAC_OSX) {
117+
return kqueueGroup();
118+
}
119+
throw new RuntimeException("Unspported OS");
120+
}
121+
122+
private ThreadFactory createThreadFactory() {
123+
return new DefaultThreadFactory(THREAD_PREFIX, true, Thread.NORM_PRIORITY, DockerClientFactory.TESTCONTAINERS_THREAD_GROUP);
124+
}
125+
126+
public EventLoopGroup epollGroup() {
127+
EventLoopGroup epollEventLoopGroup = new EpollEventLoopGroup(0, createThreadFactory());
128+
129+
ChannelFactory<EpollDomainSocketChannel> factory = () -> configure(new EpollDomainSocketChannel());
130+
131+
bootstrap.group(epollEventLoopGroup).channelFactory(factory).handler(new ChannelInitializer<UnixChannel>() {
132+
@Override
133+
protected void initChannel(final UnixChannel channel) throws Exception {
134+
channel.pipeline().addLast(new HttpClientCodec());
135+
}
136+
});
137+
return epollEventLoopGroup;
138+
}
139+
140+
public EventLoopGroup kqueueGroup() {
141+
EventLoopGroup nioEventLoopGroup = new KQueueEventLoopGroup(0, createThreadFactory());
142+
143+
bootstrap.group(nioEventLoopGroup).channel(KQueueDomainSocketChannel.class)
144+
.handler(new ChannelInitializer<KQueueDomainSocketChannel>() {
145+
@Override
146+
protected void initChannel(final KQueueDomainSocketChannel channel) throws Exception {
147+
channel.pipeline().addLast(new HttpClientCodec());
148+
}
149+
});
150+
151+
return nioEventLoopGroup;
152+
}
153+
154+
@Override
155+
public DuplexChannel connect(Bootstrap bootstrap) throws InterruptedException {
156+
return (DuplexChannel) bootstrap.connect(new DomainSocketAddress("/var/run/docker.sock")).sync().channel();
157+
}
158+
}
159+
160+
private class InetSocketInitializer implements NettyInitializer {
161+
@Override
162+
public EventLoopGroup init(Bootstrap bootstrap, final DockerClientConfig dockerClientConfig) {
163+
EventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(0, new DefaultThreadFactory(THREAD_PREFIX));
164+
165+
// TODO do we really need BouncyCastle?
166+
Security.addProvider(new BouncyCastleProvider());
167+
168+
ChannelFactory<NioSocketChannel> factory = () -> configure(new NioSocketChannel());
169+
170+
bootstrap.group(nioEventLoopGroup).channelFactory(factory)
171+
.handler(new ChannelInitializer<SocketChannel>() {
172+
@Override
173+
protected void initChannel(final SocketChannel channel) throws Exception {
174+
channel.pipeline().addLast(new HttpClientCodec());
175+
}
176+
});
177+
178+
return nioEventLoopGroup;
179+
}
180+
181+
@Override
182+
public DuplexChannel connect(Bootstrap bootstrap) throws InterruptedException {
183+
DockerClientConfig dockerClientConfig = getDockerClientConfig();
184+
String host = dockerClientConfig.getDockerHost().getHost();
185+
int port = dockerClientConfig.getDockerHost().getPort();
186+
187+
if (port == -1) {
188+
throw new RuntimeException("no port configured for " + host);
189+
}
190+
191+
DuplexChannel channel = (DuplexChannel) bootstrap.connect(host, port).sync().channel();
192+
193+
final SslHandler ssl = initSsl(dockerClientConfig);
194+
195+
if (ssl != null) {
196+
channel.pipeline().addFirst(ssl);
197+
}
198+
199+
return channel;
200+
}
201+
202+
private SslHandler initSsl(DockerClientConfig dockerClientConfig) {
203+
SslHandler ssl = null;
204+
205+
try {
206+
String host = dockerClientConfig.getDockerHost().getHost();
207+
int port = dockerClientConfig.getDockerHost().getPort();
208+
209+
final SSLConfig sslConfig = dockerClientConfig.getSSLConfig();
210+
211+
if (sslConfig != null && sslConfig.getSSLContext() != null) {
212+
213+
SSLEngine engine = sslConfig.getSSLContext().createSSLEngine(host, port);
214+
engine.setUseClientMode(true);
215+
engine.setSSLParameters(enableHostNameVerification(engine.getSSLParameters()));
216+
217+
// in the future we may use HostnameVerifier like here:
218+
// https://github.com/AsyncHttpClient/async-http-client/blob/1.8.x/src/main/java/com/ning/http/client/providers/netty/NettyConnectListener.java#L76
219+
220+
ssl = new SslHandler(engine);
221+
}
222+
223+
} catch (Exception e) {
224+
throw new RuntimeException(e);
225+
}
226+
227+
return ssl;
228+
}
229+
}
230+
231+
public SSLParameters enableHostNameVerification(SSLParameters sslParameters) {
232+
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
233+
return sslParameters;
234+
}
235+
236+
@Override
237+
public void close() throws IOException {
238+
checkNotNull(eventLoopGroup, "Factory not initialized. You probably forgot to call init()!");
239+
240+
eventLoopGroup.shutdownGracefully();
241+
}
242+
243+
/**
244+
* Configure connection timeout in milliseconds
245+
*/
246+
public TestcontainersDockerCmdExecFactory withConnectTimeout(Integer connectTimeout) {
247+
this.connectTimeout = connectTimeout;
248+
return this;
249+
}
250+
251+
/**
252+
* Configure read timeout in milliseconds
253+
*/
254+
public TestcontainersDockerCmdExecFactory withReadTimeout(Integer readTimeout) {
255+
this.readTimeout = readTimeout;
256+
return this;
257+
}
258+
259+
private <T extends Channel> T configure(T channel) {
260+
ChannelConfig channelConfig = channel.config();
261+
262+
if (connectTimeout != null) {
263+
channelConfig.setConnectTimeoutMillis(connectTimeout);
264+
}
265+
if (readTimeout != null) {
266+
channel.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler());
267+
}
268+
269+
return channel;
270+
}
271+
272+
private final class ReadTimeoutHandler extends IdleStateHandler {
273+
private boolean alreadyTimedOut;
274+
275+
ReadTimeoutHandler() {
276+
super(readTimeout, 0, 0, TimeUnit.MILLISECONDS);
277+
}
278+
279+
/**
280+
* Called when a read timeout was detected.
281+
*/
282+
@Override
283+
protected synchronized void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
284+
assert evt.state() == IdleState.READER_IDLE;
285+
final Channel channel = ctx.channel();
286+
if (channel == null || !channel.isActive() || alreadyTimedOut) {
287+
return;
288+
}
289+
DockerClientConfig dockerClientConfig = getDockerClientConfig();
290+
final Object dockerAPIEndpoint = dockerClientConfig.getDockerHost();
291+
final String msg = "Read timed out: No data received within " + readTimeout
292+
+ "ms. Perhaps the docker API (" + dockerAPIEndpoint
293+
+ ") is not responding normally, or perhaps you need to increase the readTimeout value.";
294+
final Exception ex = new SocketTimeoutException(msg);
295+
ctx.fireExceptionCaught(ex);
296+
alreadyTimedOut = true;
297+
}
298+
}
299+
300+
protected WebTarget getBaseResource() {
301+
checkNotNull(baseResource, "Factory not initialized, baseResource not set. You probably forgot to call init()!");
302+
return baseResource;
303+
}
304+
}

core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class ImageFromDockerfile extends LazyFuture<String> implements
3939
private static final Set<String> imagesToDelete = Sets.newConcurrentHashSet();
4040

4141
static {
42-
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
42+
Runtime.getRuntime().addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> {
4343
DockerClient dockerClientForCleaning = DockerClientFactory.instance().client();
4444
try {
4545
for (String dockerImageName : imagesToDelete) {

0 commit comments

Comments
 (0)