From 92b2fbc502a29dd28e622c885462dd98a9dd5ff5 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Sat, 18 Jul 2026 11:50:49 +0530 Subject: [PATCH] fix: shut down executeAsync executor Signed-off-by: Arnab Nandy --- .../google/api/client/http/HttpRequest.java | 13 +++++-- .../api/client/http/HttpRequestTest.java | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java index 78f15d868..d3fefd8cd 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java +++ b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java @@ -32,6 +32,7 @@ import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; @@ -1148,15 +1149,21 @@ public HttpResponse call() throws Exception { /** * {@link Beta}
* Executes this request asynchronously using {@link #executeAsync(Executor)} in a single separate - * thread using {@link Executors#newFixedThreadPool(int)}. + * thread using {@link Executors#newFixedThreadPool(int)}. The executor is shut down after the + * request has been submitted. * * @return A future for accessing the results of the asynchronous request. * @since 1.13 */ @Beta public Future executeAsync() { - return executeAsync( - Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build())); + ExecutorService executor = + Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); + try { + return executeAsync(executor); + } finally { + executor.shutdown(); + } } /** diff --git a/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTest.java b/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTest.java index 085b9f563..b25dec1cf 100644 --- a/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTest.java +++ b/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTest.java @@ -41,7 +41,9 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -1209,6 +1211,42 @@ public void testExecuteAsync() assertNotNull(futureResponse.get(10, TimeUnit.MILLISECONDS)); } + @Test + public void testExecuteAsync_defaultExecutorDoesNotLeakThreads() throws Exception { + Set threadsBeforeTest = getThreadIds(); + HttpTransport transport = new MockHttpTransport(); + + for (int i = 0; i < 20; i++) { + HttpRequest request = + transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); + assertNotNull(request.executeAsync().get(10, TimeUnit.SECONDS)); + } + + long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10); + while (hasNewExecutorThread(threadsBeforeTest) && System.currentTimeMillis() < timeout) { + Thread.sleep(10); + } + assertFalse(hasNewExecutorThread(threadsBeforeTest)); + } + + private static Set getThreadIds() { + Set threadIds = new HashSet(); + for (Thread thread : Thread.getAllStackTraces().keySet()) { + threadIds.add(thread.getId()); + } + return threadIds; + } + + private static boolean hasNewExecutorThread(Set threadsBeforeTest) { + for (Thread thread : Thread.getAllStackTraces().keySet()) { + if (!threadsBeforeTest.contains(thread.getId()) + && thread.getName().matches("pool-\\d+-thread-\\d+")) { + return true; + } + } + return false; + } + @Test public void testExecute_redirects() throws Exception { class MyTransport extends MockHttpTransport {