Skip to content

Commit a2521fa

Browse files
committed
AsyncHttpClient name option
Fixes AsyncHttpClient#952 Name option is useful for two purposes: ## Thread name Typical application with several netty and/or async-http-client instances may have hundreds of threads like "New I/O worker 120", which makes it hard to understand which thread belongs to which part of the application. Specified name is used in AsyncHttpClient to name threads created by AsyncHttpClient and netty. If name is not specified, `AsyncHttpClient` string is used. ## Debugging `name` is stored in `AsyncHttpClientConfig`, and available in debugger debugging when application has multiple client instances. It can be useful to distinguish between instance, for example, when breakpoint is set inside of AsyncHttpClient.
1 parent b5613c9 commit a2521fa

File tree

10 files changed

+249
-7
lines changed

10 files changed

+249
-7
lines changed

api/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.asynchttpclient.filter.ResponseFilter;
3535
import org.asynchttpclient.proxy.ProxyServer;
3636
import org.asynchttpclient.proxy.ProxyServerSelector;
37+
import org.asynchttpclient.util.PrefixIncrementThreadFactory;
3738
import org.asynchttpclient.util.ProxyUtils;
3839

3940
/**
@@ -66,6 +67,8 @@ public class AsyncHttpClientConfig {
6667
AHC_VERSION = prop.getProperty("ahc.version", "UNKNOWN");
6768
}
6869

70+
protected String name;
71+
6972
protected int connectTimeout;
7073

7174
protected int maxConnections;
@@ -118,7 +121,8 @@ public class AsyncHttpClientConfig {
118121
protected AsyncHttpClientConfig() {
119122
}
120123

121-
private AsyncHttpClientConfig(int connectTimeout,//
124+
private AsyncHttpClientConfig(String name,//
125+
int connectTimeout,//
122126
int maxConnections,//
123127
int maxConnectionsPerHost,//
124128
int requestTimeout,//
@@ -160,6 +164,7 @@ private AsyncHttpClientConfig(int connectTimeout,//
160164
boolean keepEncodingHeader,//
161165
AsyncHttpProviderConfig<?, ?> providerConfig) {
162166

167+
this.name = name;
163168
this.connectTimeout = connectTimeout;
164169
this.maxConnections = maxConnections;
165170
this.maxConnectionsPerHost = maxConnectionsPerHost;
@@ -178,7 +183,15 @@ private AsyncHttpClientConfig(int connectTimeout,//
178183
this.proxyServerSelector = proxyServerSelector;
179184
this.compressionEnforced = compressionEnforced;
180185
this.userAgent = userAgent;
181-
this.applicationThreadPool = applicationThreadPool == null ? Executors.newCachedThreadPool() : applicationThreadPool;
186+
187+
if (applicationThreadPool != null) {
188+
this.applicationThreadPool = applicationThreadPool;
189+
} else {
190+
PrefixIncrementThreadFactory threadFactory = new PrefixIncrementThreadFactory(
191+
getNameOrDefault() + "-");
192+
this.applicationThreadPool = Executors.newCachedThreadPool(threadFactory);
193+
}
194+
182195
this.realm = realm;
183196
this.requestFilters = requestFilters;
184197
this.responseFilters = responseFilters;
@@ -203,6 +216,32 @@ private AsyncHttpClientConfig(int connectTimeout,//
203216
this.keepEncodingHeader = keepEncodingHeader;
204217
}
205218

219+
/**
220+
* Return the name of {@link AsyncHttpClient}, which is used for thread naming
221+
* and debugging.
222+
*
223+
* @return the name.
224+
*/
225+
public String getName() {
226+
return name;
227+
}
228+
229+
/**
230+
* Return the name of {@link AsyncHttpClient}, or default string if name is null or empty.
231+
*
232+
* @return the name.
233+
*/
234+
public String getNameOrDefault() {
235+
String r = name;
236+
if (r == null || r.isEmpty()) {
237+
r = defaultName();
238+
}
239+
if (r == null || r.isEmpty()) {
240+
r = "AsyncHttpClient";
241+
}
242+
return r;
243+
}
244+
206245
/**
207246
* Return the maximum number of connections an {@link AsyncHttpClient} can
208247
* handle.
@@ -577,6 +616,7 @@ public boolean isKeepEncodingHeader() {
577616
* Builder for an {@link AsyncHttpClient}
578617
*/
579618
public static class Builder {
619+
private String name = defaultName();
580620
private int connectTimeout = defaultConnectTimeout();
581621
private int maxConnections = defaultMaxConnections();
582622
private int maxConnectionsPerHost = defaultMaxConnectionsPerHost();
@@ -624,6 +664,16 @@ public static class Builder {
624664
public Builder() {
625665
}
626666

667+
/**
668+
* Set the name of {@link AsyncHttpClient}. That name is used for thread
669+
* naming and can be used for debugging multiple {@link AsyncHttpClient}
670+
* instance.
671+
*/
672+
public Builder setName(String name) {
673+
this.name = name;
674+
return this;
675+
}
676+
627677
/**
628678
* Set the maximum number of connections an {@link AsyncHttpClient} can
629679
* handle.
@@ -1113,6 +1163,7 @@ public Builder setKeepEncodingHeader(boolean keepEncodingHeader) {
11131163
* @param prototype the configuration to use as a prototype.
11141164
*/
11151165
public Builder(AsyncHttpClientConfig prototype) {
1166+
name = prototype.getName();
11161167
allowPoolingConnections = prototype.isAllowPoolingConnections();
11171168
connectTimeout = prototype.getConnectTimeout();
11181169
pooledConnectionIdleTimeout = prototype.getPooledConnectionIdleTimeout();
@@ -1178,7 +1229,8 @@ public AsyncHttpClientConfig build() {
11781229
if (proxyServerSelector == null)
11791230
proxyServerSelector = ProxyServerSelector.NO_PROXY_SELECTOR;
11801231

1181-
return new AsyncHttpClientConfig(connectTimeout,//
1232+
return new AsyncHttpClientConfig(name,//
1233+
connectTimeout,//
11821234
maxConnections,//
11831235
maxConnectionsPerHost,//
11841236
requestTimeout,//

api/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigBean.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ void configureFilters() {
5151
void configureDefaults() {
5252
maxConnections = defaultMaxConnections();
5353
maxConnectionsPerHost = defaultMaxConnectionsPerHost();
54+
name = defaultName();
5455
connectTimeout = defaultConnectTimeout();
5556
webSocketTimeout = defaultWebSocketTimeout();
5657
pooledConnectionIdleTimeout = defaultPooledConnectionIdleTimeout();
@@ -88,6 +89,11 @@ public Thread newThread(Runnable r) {
8889
});
8990
}
9091

92+
public AsyncHttpClientConfigBean setName(String name) {
93+
this.name = name;
94+
return this;
95+
}
96+
9197
public AsyncHttpClientConfigBean setMaxTotalConnections(int maxConnections) {
9298
this.maxConnections = maxConnections;
9399
return this;

api/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ private AsyncHttpClientConfigDefaults() {
1919

2020
public static final String ASYNC_CLIENT_CONFIG_ROOT = "org.asynchttpclient.";
2121

22+
public static String defaultName() {
23+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getString(ASYNC_CLIENT_CONFIG_ROOT + "name");
24+
}
25+
2226
public static int defaultMaxConnections() {
2327
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + "maxConnections");
2428
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2015 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at
7+
* http://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* Unless required by applicable law or agreed to in writing,
10+
* software distributed under the Apache License Version 2.0 is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
13+
*/
14+
package org.asynchttpclient.util;
15+
16+
import java.util.concurrent.ThreadFactory;
17+
import java.util.concurrent.atomic.AtomicInteger;
18+
19+
/**
20+
* Thread factory that generates thread names by adding incrementing number
21+
* to the specified prefix.
22+
*
23+
* @author Stepan Koltsov
24+
*/
25+
public class PrefixIncrementThreadFactory implements ThreadFactory {
26+
private final String namePrefix;
27+
private final AtomicInteger threadNumber = new AtomicInteger();
28+
29+
public PrefixIncrementThreadFactory(String namePrefix) {
30+
if (namePrefix == null || namePrefix.isEmpty()) {
31+
throw new IllegalArgumentException("namePrefix must not be empty");
32+
}
33+
this.namePrefix = namePrefix;
34+
}
35+
36+
public Thread newThread(Runnable r) {
37+
return new Thread(r, namePrefix + threadNumber.incrementAndGet());
38+
}
39+
}

api/src/main/resources/ahc-default.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
org.asynchttpclient.name=AsyncHttpClient
12
org.asynchttpclient.maxConnections=-1
23
org.asynchttpclient.maxConnectionsPerHost=-1
34
org.asynchttpclient.connectTimeout=5000
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2015 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at
7+
* http://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* Unless required by applicable law or agreed to in writing,
10+
* software distributed under the Apache License Version 2.0 is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
13+
*/
14+
package org.asynchttpclient;
15+
16+
import java.util.Arrays;
17+
import java.util.Random;
18+
import java.util.concurrent.Future;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.testng.Assert;
22+
import org.testng.annotations.Test;
23+
24+
/**
25+
* Tests configured client name is used for thread names.
26+
*
27+
* @author Stepan Koltsov
28+
*/
29+
public abstract class ThreadNameTest extends AbstractBasicTest {
30+
31+
private static Thread[] getThreads() {
32+
int count = Thread.activeCount() + 1;
33+
for (;;) {
34+
Thread[] threads = new Thread[count];
35+
int filled = Thread.enumerate(threads);
36+
if (filled < threads.length) {
37+
return Arrays.copyOf(threads, filled);
38+
}
39+
40+
count *= 2;
41+
}
42+
}
43+
44+
@Test(groups = { "standalone", "default_provider" })
45+
public void testQueryParameters() throws Exception {
46+
String name = "ahc-" + (new Random().nextLong() & 0x7fffffffffffffffL);
47+
AsyncHttpClientConfig.Builder config = new AsyncHttpClientConfig.Builder();
48+
config.setName(name);
49+
try (AsyncHttpClient client = getAsyncHttpClient(config.build())) {
50+
Future<Response> f = client.prepareGet("http://127.0.0.1:" + port1 + "/").execute();
51+
f.get(3, TimeUnit.SECONDS);
52+
53+
// We cannot assert that all threads are created with specified name,
54+
// so we checking that at least one thread is.
55+
boolean found = false;
56+
for (Thread thread : getThreads()) {
57+
if (thread.getName().startsWith(name)) {
58+
found = true;
59+
break;
60+
}
61+
}
62+
63+
Assert.assertTrue(found, "must found threads starting with random string " + name);
64+
}
65+
}
66+
}

providers/netty3/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.concurrent.ExecutorService;
2929
import java.util.concurrent.Executors;
3030
import java.util.concurrent.Semaphore;
31+
import java.util.concurrent.ThreadFactory;
3132
import java.util.concurrent.atomic.AtomicBoolean;
3233

3334
import javax.net.ssl.SSLEngine;
@@ -52,21 +53,26 @@
5253
import org.asynchttpclient.netty.request.NettyRequestSender;
5354
import org.asynchttpclient.proxy.ProxyServer;
5455
import org.asynchttpclient.uri.Uri;
56+
import org.asynchttpclient.util.PrefixIncrementThreadFactory;
5557
import org.jboss.netty.bootstrap.ClientBootstrap;
5658
import org.jboss.netty.channel.Channel;
5759
import org.jboss.netty.channel.ChannelPipeline;
5860
import org.jboss.netty.channel.ChannelPipelineFactory;
5961
import org.jboss.netty.channel.DefaultChannelFuture;
6062
import org.jboss.netty.channel.group.ChannelGroup;
6163
import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
64+
import org.jboss.netty.channel.socket.nio.NioClientBossPool;
6265
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
66+
import org.jboss.netty.channel.socket.nio.NioWorkerPool;
6367
import org.jboss.netty.handler.codec.http.HttpClientCodec;
6468
import org.jboss.netty.handler.codec.http.HttpContentDecompressor;
6569
import org.jboss.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
6670
import org.jboss.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
6771
import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
6872
import org.jboss.netty.handler.ssl.SslHandler;
6973
import org.jboss.netty.handler.stream.ChunkedWriteHandler;
74+
import org.jboss.netty.util.HashedWheelTimer;
75+
import org.jboss.netty.util.ThreadNameDeterminer;
7076
import org.jboss.netty.util.Timer;
7177
import org.slf4j.Logger;
7278
import org.slf4j.LoggerFactory;
@@ -181,11 +187,16 @@ public Semaphore apply(Object partitionKey) {
181187

182188
} else {
183189
ExecutorService e = nettyConfig.getBossExecutorService();
184-
if (e == null)
185-
e = Executors.newCachedThreadPool();
190+
if (e == null) {
191+
ThreadFactory threadFactory = new PrefixIncrementThreadFactory(
192+
config.getNameOrDefault() + "-boss-");
193+
e = Executors.newCachedThreadPool(threadFactory);
194+
}
186195
int numWorkers = config.getIoThreadMultiplier() * Runtime.getRuntime().availableProcessors();
187196
LOGGER.trace("Number of application's worker threads is {}", numWorkers);
188-
socketChannelFactory = new NioClientSocketChannelFactory(e, config.executorService(), numWorkers);
197+
NioClientBossPool nioClientBossPool = new NioClientBossPool(e, 1, new HashedWheelTimer(), ThreadNameDeterminer.CURRENT);
198+
NioWorkerPool nioWorkerPool = new NioWorkerPool(config.executorService(), numWorkers, ThreadNameDeterminer.CURRENT);
199+
socketChannelFactory = new NioClientSocketChannelFactory(nioClientBossPool, nioWorkerPool);
189200
allowReleaseSocketChannelFactory = true;
190201
}
191202

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2015 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at
7+
* http://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* Unless required by applicable law or agreed to in writing,
10+
* software distributed under the Apache License Version 2.0 is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
13+
*/
14+
package org.asynchttpclient.netty;
15+
16+
import org.asynchttpclient.AsyncHttpClient;
17+
import org.asynchttpclient.AsyncHttpClientConfig;
18+
import org.asynchttpclient.RetryRequestTest;
19+
import org.asynchttpclient.ThreadNameTest;
20+
21+
/**
22+
* @author Stepan Koltsov
23+
*/
24+
public class NettyThreadNameTest extends ThreadNameTest {
25+
@Override
26+
public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) {
27+
return NettyProviderUtil.nettyProvider(config);
28+
}
29+
}

providers/netty4/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.netty.handler.ssl.SslHandler;
3939
import io.netty.handler.stream.ChunkedWriteHandler;
4040
import io.netty.util.Timer;
41+
import io.netty.util.concurrent.DefaultThreadFactory;
4142
import io.netty.util.internal.chmv8.ConcurrentHashMapV8;
4243

4344
import java.io.IOException;
@@ -172,7 +173,12 @@ public Semaphore apply(Object partitionKey) {
172173

173174
// check if external EventLoopGroup is defined
174175
allowReleaseEventLoopGroup = nettyConfig.getEventLoopGroup() == null;
175-
eventLoopGroup = allowReleaseEventLoopGroup ? new NioEventLoopGroup() : nettyConfig.getEventLoopGroup();
176+
if (allowReleaseEventLoopGroup) {
177+
DefaultThreadFactory threadFactory = new DefaultThreadFactory(config.getNameOrDefault());
178+
eventLoopGroup = new NioEventLoopGroup(0, threadFactory);
179+
} else {
180+
eventLoopGroup = nettyConfig.getEventLoopGroup();
181+
}
176182
if (eventLoopGroup instanceof OioEventLoopGroup)
177183
throw new IllegalArgumentException("Oio is not supported");
178184

0 commit comments

Comments
 (0)