Skip to content

Commit e622b5e

Browse files
committed
fix: Improve performance of NetHttpRequest write timeouts by providing a possibility to use custom threadpool
1 parent fbdecb4 commit e622b5e

File tree

4 files changed

+66
-7
lines changed

4 files changed

+66
-7
lines changed

google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java

+19
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Properties;
3333
import java.util.concurrent.Callable;
3434
import java.util.concurrent.Executor;
35+
import java.util.concurrent.ExecutorService;
3536
import java.util.concurrent.Executors;
3637
import java.util.concurrent.Future;
3738
import java.util.concurrent.FutureTask;
@@ -153,6 +154,12 @@ public final class HttpRequest {
153154
/** Timeout in milliseconds to set POST/PUT data or {@code 0} for an infinite timeout. */
154155
private int writeTimeout = 0;
155156

157+
/**
158+
* Custom executor used to handle write timeouts, enabled by {@link #writeTimeout}.
159+
* By default, a new thread is created per each request.
160+
*/
161+
private ExecutorService writeTimeoutExecutor;
162+
156163
/** HTTP unsuccessful (non-2XX) response handler or {@code null} for none. */
157164
private HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler;
158165

@@ -490,6 +497,17 @@ public HttpRequest setWriteTimeout(int writeTimeout) {
490497
}
491498

492499
/**
500+
* Sets custom executor used to handle write timeouts, enabled by {@link #setWriteTimeout(int)}.
501+
* By default, a new thread is created per each request.
502+
*
503+
* @since 1.40.2
504+
*/
505+
public HttpRequest setWriteTimeoutExecutor(ExecutorService writeTimeoutExecutor) {
506+
this.writeTimeoutExecutor = writeTimeoutExecutor;
507+
return this;
508+
}
509+
510+
/**
493511
* Returns the HTTP request headers.
494512
*
495513
* @since 1.5
@@ -1003,6 +1021,7 @@ public HttpResponse execute() throws IOException {
10031021
// execute
10041022
lowLevelHttpRequest.setTimeout(connectTimeout, readTimeout);
10051023
lowLevelHttpRequest.setWriteTimeout(writeTimeout);
1024+
lowLevelHttpRequest.setWriteTimeoutExecutor(writeTimeoutExecutor);
10061025

10071026
// switch tracing scope to current span
10081027
@SuppressWarnings("MustBeClosedChecker")

google-http-client/src/main/java/com/google/api/client/http/LowLevelHttpRequest.java

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.google.api.client.util.StreamingContent;
1818
import java.io.IOException;
19+
import java.util.concurrent.ExecutorService;
1920

2021
/**
2122
* Low-level HTTP request.
@@ -158,6 +159,16 @@ public void setTimeout(int connectTimeout, int readTimeout) throws IOException {
158159
*/
159160
public void setWriteTimeout(int writeTimeout) throws IOException {}
160161

162+
/**
163+
* Sets custom timeout executor for POST/PUT requests.
164+
*
165+
* <p>Default implementation uses a new thread per each {@link #execute()} call.
166+
*
167+
* @param writeTimeoutExecutor custom timeout executor to use
168+
* @since 1.40.2
169+
*/
170+
public void setWriteTimeoutExecutor(ExecutorService writeTimeoutExecutor) {}
171+
161172
/** Executes the request and returns a low-level HTTP response object. */
162173
public abstract LowLevelHttpResponse execute() throws IOException;
163174
}

google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpRequest.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class NetHttpRequest extends LowLevelHttpRequest {
3636

3737
private final HttpURLConnection connection;
3838
private int writeTimeout;
39+
private ExecutorService writeTimeoutExecutor;
3940

4041
/** @param connection HTTP URL connection */
4142
NetHttpRequest(HttpURLConnection connection) {
@@ -65,6 +66,11 @@ public void setWriteTimeout(int writeTimeout) throws IOException {
6566
this.writeTimeout = writeTimeout;
6667
}
6768

69+
@Override
70+
public void setWriteTimeoutExecutor(ExecutorService writeTimeoutExecutor) {
71+
this.writeTimeoutExecutor = writeTimeoutExecutor;
72+
}
73+
6874
interface OutputWriter {
6975
void write(OutputStream outputStream, StreamingContent content) throws IOException;
7076
}
@@ -184,9 +190,13 @@ public Boolean call() throws IOException {
184190
}
185191
};
186192

187-
final ExecutorService executor = Executors.newSingleThreadExecutor();
193+
final boolean externalWriteTimeoutExecutor = writeTimeoutExecutor != null;
194+
final ExecutorService executor = externalWriteTimeoutExecutor ? writeTimeoutExecutor : Executors.newSingleThreadExecutor();
188195
final Future<Boolean> future = executor.submit(new FutureTask<Boolean>(writeContent), null);
189-
executor.shutdown();
196+
197+
if (!externalWriteTimeoutExecutor) {
198+
executor.shutdown();
199+
}
190200

191201
try {
192202
future.get(writeTimeout, TimeUnit.MILLISECONDS);
@@ -197,7 +207,8 @@ public Boolean call() throws IOException {
197207
} catch (TimeoutException e) {
198208
throw new IOException("Socket write timed out", e);
199209
}
200-
if (!executor.isTerminated()) {
210+
211+
if (!externalWriteTimeoutExecutor && !executor.isTerminated()) {
201212
executor.shutdown();
202213
}
203214
}

google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpRequestTest.java

+22-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
import java.io.OutputStream;
1616
import java.net.URL;
1717
import java.nio.charset.StandardCharsets;
18+
import java.util.concurrent.ExecutorService;
19+
import java.util.concurrent.Executors;
1820
import java.util.concurrent.TimeoutException;
1921
import org.junit.Test;
22+
import javax.annotation.Nullable;
2023

2124
public class NetHttpRequestTest {
2225

@@ -44,7 +47,7 @@ public void testHangingWrite() throws InterruptedException {
4447
@Override
4548
public void run() {
4649
try {
47-
postWithTimeout(0);
50+
postWithTimeout(0, null);
4851
} catch (IOException e) {
4952
// expected to be interrupted
5053
assertEquals(e.getCause().getClass(), InterruptedException.class);
@@ -63,9 +66,9 @@ public void run() {
6366
}
6467

6568
@Test(timeout = 1000)
66-
public void testOutputStreamWriteTimeout() throws Exception {
69+
public void testOutputStreamWriteTimeout() {
6770
try {
68-
postWithTimeout(100);
71+
postWithTimeout(100, null);
6972
fail("should have timed out");
7073
} catch (IOException e) {
7174
assertEquals(e.getCause().getClass(), TimeoutException.class);
@@ -74,10 +77,25 @@ public void testOutputStreamWriteTimeout() throws Exception {
7477
}
7578
}
7679

77-
private static void postWithTimeout(int timeout) throws Exception {
80+
@Test(timeout = 1000)
81+
public void testOutputStreamWriteTimeoutWithCustomExecutor() {
82+
ExecutorService customWriteTimeoutExecutor = Executors.newSingleThreadExecutor();
83+
try {
84+
postWithTimeout(100, customWriteTimeoutExecutor);
85+
fail("should have timed out");
86+
} catch (IOException e) {
87+
assertEquals(e.getCause().getClass(), TimeoutException.class);
88+
assertFalse(customWriteTimeoutExecutor.isTerminated());
89+
} catch (Exception e) {
90+
fail("Expected an IOException not a " + e.getCause().getClass().getName());
91+
}
92+
}
93+
94+
private static void postWithTimeout(int timeout, @Nullable ExecutorService writeTimeoutExecutor) throws Exception {
7895
MockHttpURLConnection connection = new MockHttpURLConnection(new URL(HttpTesting.SIMPLE_URL));
7996
connection.setRequestMethod("POST");
8097
NetHttpRequest request = new NetHttpRequest(connection);
98+
request.setWriteTimeoutExecutor(writeTimeoutExecutor);
8199
InputStream is = NetHttpRequestTest.class.getClassLoader().getResourceAsStream("file.txt");
82100
HttpContent content = new InputStreamContent("text/plain", is);
83101
request.setStreamingContent(content);

0 commit comments

Comments
 (0)