Skip to content

Commit 0561c12

Browse files
committed
HTTPClient: add idle timeout and change timeout signatures
This adds an idle timeout based off PR #20. In addition this changes the timeout TimeUnit to be the second argument (similar to how java typically orders the unit argument)
1 parent 951146c commit 0561c12

File tree

3 files changed

+75
-26
lines changed

3 files changed

+75
-26
lines changed

client/src/main/java/org/threadly/litesockets/client/http/HTTPClient.java

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.net.URL;
66
import java.nio.ByteBuffer;
77
import java.util.ArrayDeque;
8+
import java.util.Iterator;
89
import java.util.concurrent.CancellationException;
910
import java.util.concurrent.ConcurrentHashMap;
1011
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -42,6 +43,7 @@
4243
import org.threadly.litesockets.utils.SSLUtils;
4344
import org.threadly.util.AbstractService;
4445
import org.threadly.util.Clock;
46+
import org.threadly.util.Pair;
4547

4648
/**
4749
* <p>This is a HTTPClient for doing many simple HTTPRequests. Every request will be make a new connection and requests
@@ -51,20 +53,23 @@
5153
public class HTTPClient extends AbstractService {
5254
public static final int DEFAULT_CONCURRENT = 2;
5355
public static final int DEFAULT_TIMEOUT = 15000;
56+
public static final int DEFAULT_MAX_IDLE = 45000;
5457
public static final int MAX_HTTP_RESPONSE = 1048576; //1MB
5558

5659
private final int maxResponseSize;
5760
private final SubmitterScheduler ssi;
5861
private final SocketExecuter sei;
5962
private final ConcurrentLinkedQueue<HTTPRequestWrapper> queue = new ConcurrentLinkedQueue<>();
6063
private final ConcurrentHashMap<TCPClient, HTTPRequestWrapper> inProcess = new ConcurrentHashMap<>();
61-
private final ConcurrentHashMap<HTTPAddress, ArrayDeque<TCPClient>> sockets = new ConcurrentHashMap<>();
64+
private final ConcurrentHashMap<HTTPAddress, ArrayDeque<Pair<Long,TCPClient>>> sockets = new ConcurrentHashMap<>();
6265
private final CopyOnWriteArraySet<TCPClient> tcpClients = new CopyOnWriteArraySet<>();
6366
private final MainClientProcessor mcp = new MainClientProcessor();
6467
private final RunSocket runSocketTask;
6568
private final int maxConcurrent;
66-
private volatile int defaultTimeoutMS = HTTPRequest.DEFAULT_TIMEOUT_MS;
69+
private volatile Runnable checkIdle = null;
70+
private volatile long defaultTimeoutMS = HTTPRequest.DEFAULT_TIMEOUT_MS;
6771
private volatile SSLContext sslContext = SSLUtils.OPEN_SSL_CTX;
72+
private volatile long maxIdleTime = DEFAULT_MAX_IDLE;
6873

6974
private NoThreadSocketExecuter ntse = null;
7075
private SingleThreadScheduler sts = null;
@@ -96,7 +101,6 @@ public HTTPClient(int maxConcurrent, int maxResponseSize) {
96101
runSocketTask = new RunSocket(ssi);
97102
}
98103

99-
100104
/**
101105
* <p>This constructor will let you set the max Concurrent Requests and max Response Size
102106
* as well as your own {@link SocketExecuter} as the thread pool to use.</p>
@@ -168,8 +172,34 @@ public void closeAllClients() {
168172
*
169173
* @param timeout time in milliseconds to wait for HTTPRequests to finish.
170174
*/
171-
public void setTimeout(TimeUnit unit, int timeout) {
172-
this.defaultTimeoutMS = (int)Math.min(Math.max(unit.toMillis(timeout),HTTPRequest.MIN_TIMEOUT_MS), HTTPRequest.MAX_TIMEOUT_MS);
175+
public void setTimeout(long timeout, TimeUnit unit) {
176+
this.defaultTimeoutMS = Math.min(Math.max(unit.toMillis(timeout),HTTPRequest.MIN_TIMEOUT_MS), HTTPRequest.MAX_TIMEOUT_MS);
177+
}
178+
179+
public long getMaxIdleTimeout() {
180+
return this.maxIdleTime;
181+
}
182+
183+
/**
184+
* Sets the max amount of time we will hold onto idle connections. A 0 means we close connections when done, less
185+
* than zero means we will never expire connections.
186+
*
187+
* @param it the time in milliseconds to wait before timing out a connection.
188+
*/
189+
public void setMaxIdleTimeout(long it, TimeUnit unit) {
190+
this.maxIdleTime = unit.toMillis(it);
191+
if(this.maxIdleTime > 0) {
192+
this.checkIdle = new Runnable() {
193+
@Override
194+
public void run() {
195+
if(checkIdle == this) {
196+
checkIdleSockets();
197+
ssi.schedule(this, Math.max(100, maxIdleTime/2));
198+
}
199+
}
200+
};
201+
this.ssi.schedule(checkIdle, Math.max(100, maxIdleTime/2));
202+
}
173203
}
174204

175205
/**
@@ -260,11 +290,10 @@ public ListenableFuture<HTTPResponseData> requestAsync(final URL url) {
260290
public ListenableFuture<HTTPResponseData> requestAsync(final URL url, final HTTPRequestType rt, final ByteBuffer bb) {
261291
HTTPRequestBuilder hrb = new HTTPRequestBuilder(url);
262292
hrb.setRequestType(rt);
263-
hrb.setTimeout(TimeUnit.MILLISECONDS, this.defaultTimeoutMS);
293+
hrb.setTimeout(this.defaultTimeoutMS, TimeUnit.MILLISECONDS);
264294
return requestAsync(hrb.buildClientHTTPRequest());
265295
}
266296

267-
268297
/**
269298
* Sends an asynchronous HTTP request.
270299
*
@@ -319,6 +348,7 @@ protected void startupService() {
319348
if (ntse != null) {
320349
ntse.start();
321350
}
351+
setMaxIdleTimeout(this.maxIdleTime, TimeUnit.MILLISECONDS);
322352
}
323353

324354
@Override
@@ -338,18 +368,18 @@ protected void shutdownService() {
338368
}
339369

340370
private TCPClient getTCPClient(final HTTPAddress ha) throws IOException {
341-
ArrayDeque<TCPClient> ll = sockets.get(ha);
371+
ArrayDeque<Pair<Long,TCPClient>> pl = sockets.get(ha);
342372
TCPClient tc = null;
343-
if(ll != null) {
344-
synchronized(ll) {
345-
while(ll.size() > 0 && tc == null) {
346-
if(ll.peek().isClosed()) {
347-
ll.pop();
373+
if(pl != null) {
374+
synchronized(pl) {
375+
while(pl.size() > 0 && tc == null) {
376+
if(pl.peek().getRight().isClosed()) {
377+
pl.pop();
348378
} else {
349-
tc = ll.pop();
379+
tc = pl.pop().getRight();
350380
}
351381
}
352-
if(ll.size() == 0) {
382+
if(pl.size() == 0) {
353383
sockets.remove(ha);
354384
}
355385
}
@@ -371,14 +401,35 @@ private TCPClient getTCPClient(final HTTPAddress ha) throws IOException {
371401
}
372402

373403
private void addBackTCPClient(final HTTPAddress ha, final TCPClient client) {
404+
if(maxIdleTime == 0) {
405+
client.close();
406+
return;
407+
}
374408
if(!client.isClosed()) {
375-
ArrayDeque<TCPClient> ll = sockets.get(ha);
409+
ArrayDeque<Pair<Long,TCPClient>> ll = sockets.get(ha);
376410
if(ll == null) {
377411
sockets.put(ha, new ArrayDeque<>(8));
378412
ll = sockets.get(ha);
379413
}
380414
synchronized(ll) {
381-
ll.add(client);
415+
ll.add(new Pair<>(Clock.lastKnownForwardProgressingMillis(), client));
416+
}
417+
}
418+
}
419+
420+
private void checkIdleSockets() {
421+
if(maxIdleTime > 0) {
422+
for(ArrayDeque<Pair<Long,TCPClient>> adq: sockets.values()) {
423+
synchronized(adq) {
424+
Iterator<Pair<Long,TCPClient>> iter = adq.iterator();
425+
while(iter.hasNext()) {
426+
Pair<Long,TCPClient> c = iter.next();
427+
if(Clock.lastKnownForwardProgressingMillis() - c.getLeft() > maxIdleTime) {
428+
iter.remove();
429+
c.getRight().close();
430+
}
431+
}
432+
}
382433
}
383434
}
384435
}
@@ -417,7 +468,7 @@ public void onClose(Client client) {
417468
if(hrw != null) {
418469
boolean wasProcessing = hrw.hrp.isProcessing();
419470
hrw.hrp.connectionClosed();
420-
if(!hrw.slf.isDone() && !wasProcessing) {
471+
if(! hrw.slf.isDone() && ! wasProcessing) {
421472
hrw.client = null;
422473
process(hrw);
423474
} else {
@@ -554,3 +605,4 @@ public String toString() {
554605
}
555606
}
556607
}
608+

client/src/test/java/org/threadly/litesockets/client/http/HTTPClientTests.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import static org.junit.Assert.fail;
66

77
import java.io.IOException;
8-
import java.net.MalformedURLException;
98
import java.net.URL;
109
import java.nio.ByteBuffer;
1110
import java.util.HashMap;
@@ -25,8 +24,6 @@
2524
import org.threadly.litesockets.SocketExecuter;
2625
import org.threadly.litesockets.TCPServer;
2726
import org.threadly.litesockets.ThreadedSocketExecuter;
28-
import org.threadly.litesockets.buffers.MergedByteBuffers;
29-
import org.threadly.litesockets.buffers.ReuseableMergedByteBuffers;
3027
import org.threadly.litesockets.client.http.HTTPClient.HTTPResponseData;
3128
import org.threadly.litesockets.client.http.HTTPStreamClient.HTTPStreamReader;
3229
import org.threadly.litesockets.protocols.http.request.ClientHTTPRequest;
@@ -300,7 +297,7 @@ public void closeBeforeLength() throws IOException, HTTPParsingException {
300297
fakeServer = new TestHTTPServer(port, RESPONSE_HUGE, CONTENT, false, true);
301298
final HTTPRequestBuilder hrb = new HTTPRequestBuilder(new URL("http://localhost:"+port));
302299
hrb.setBody(IOUtils.EMPTY_BYTEBUFFER);
303-
hrb.setTimeout(TimeUnit.MILLISECONDS, 10000);
300+
hrb.setTimeout(10000, TimeUnit.MILLISECONDS);
304301
final HTTPClient httpClient = new HTTPClient();
305302
httpClient.start();
306303
try{
@@ -319,7 +316,7 @@ public void timeoutRequest() throws IOException, HTTPParsingException {
319316
server.start();
320317
final HTTPRequestBuilder hrb = new HTTPRequestBuilder(new URL("http://localhost:"+port));
321318
hrb.setBody(IOUtils.EMPTY_BYTEBUFFER);
322-
hrb.setTimeout(TimeUnit.MILLISECONDS, 500);
319+
hrb.setTimeout(500, TimeUnit.MILLISECONDS);
323320
final HTTPClient httpClient = new HTTPClient();
324321
httpClient.start();
325322
try{
@@ -336,7 +333,7 @@ public void expireRequest() throws IOException, HTTPParsingException {
336333
fakeServer = new TestHTTPServer(port, RESPONSE_CL, "", false, false);
337334
final HTTPRequestBuilder hrb = new HTTPRequestBuilder(new URL("http://localhost:"+port));
338335
hrb.setBody(IOUtils.EMPTY_BYTEBUFFER);
339-
hrb.setTimeout(TimeUnit.MILLISECONDS, 30);
336+
hrb.setTimeout(30, TimeUnit.MILLISECONDS);
340337
final HTTPClient httpClient = new HTTPClient();
341338
httpClient.start();
342339
long start = Clock.accurateForwardProgressingMillis();
@@ -430,7 +427,7 @@ public void accept(Client c) {
430427
}});
431428
server.start();
432429
final HTTPRequestBuilder hrb = new HTTPRequestBuilder(new URL("http://localhost:"+port));
433-
hrb.setTimeout(TimeUnit.MILLISECONDS, 500);
430+
hrb.setTimeout(500, TimeUnit.MILLISECONDS);
434431
hrb.setBody(IOUtils.EMPTY_BYTEBUFFER);
435432
final HTTPClient httpClient = new HTTPClient();
436433
httpClient.start();

protocol/src/main/java/org/threadly/litesockets/protocols/http/request/HTTPRequestBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public HTTPRequestBuilder setBody(final String str, Charset cs) {
240240
return setBody(ByteBuffer.wrap(str.getBytes(cs)));
241241
}
242242

243-
public HTTPRequestBuilder setTimeout(TimeUnit unit, int size) {
243+
public HTTPRequestBuilder setTimeout(long size, TimeUnit unit) {
244244
this.timeoutMS = (int)Math.min(Math.max(unit.toMillis(size),HTTPRequest.MIN_TIMEOUT_MS), HTTPRequest.MAX_TIMEOUT_MS);
245245
return this;
246246
}

0 commit comments

Comments
 (0)