diff --git a/src/main/java/com/google/genai/Client.java b/src/main/java/com/google/genai/Client.java index e1f90b2c24d..4ba008c95d5 100644 --- a/src/main/java/com/google/genai/Client.java +++ b/src/main/java/com/google/genai/Client.java @@ -629,6 +629,15 @@ Optional baseUrl() { @Override public void close() { apiClient.close(); + // android:strip_begin + try { + // apiClient.close() already tears down the shared OkHttp dispatcher/pool; gaosClient.close() + // is called for lifecycle completeness in case it manages other internal resources. + gaosClient.close(); + } catch (Exception e) { + // ignore + } + // android:strip_end } /** diff --git a/src/main/java/com/google/genai/gaos/AsyncGenAI.java b/src/main/java/com/google/genai/gaos/AsyncGenAI.java index c1b0dcd9378..5d4b96bf82a 100644 --- a/src/main/java/com/google/genai/gaos/AsyncGenAI.java +++ b/src/main/java/com/google/genai/gaos/AsyncGenAI.java @@ -30,7 +30,7 @@ *

You can use the Gemini API for use cases like reasoning across text and images, content generation, * dialogue agents, summarization and classification systems, and more. */ -public class AsyncGenAI { +public class AsyncGenAI implements java.lang.AutoCloseable { private static final Headers _headers = Headers.EMPTY; private final AsyncInteractions interactions; @@ -84,4 +84,15 @@ public AsyncEnvironments environments() { public GenAI sync() { return syncSDK; } + + /** + * Releases the configured HTTP client's owned resources. The sync and + * async SDKs share one client, which is closed at most once. + * + * @throws Exception if the configured client cannot be closed + */ + @Override + public void close() throws Exception { + this.sdkConfiguration.closeClient(); + } } diff --git a/src/main/java/com/google/genai/gaos/GenAI.java b/src/main/java/com/google/genai/gaos/GenAI.java index 01b7765d65d..fb60d9676b8 100644 --- a/src/main/java/com/google/genai/gaos/GenAI.java +++ b/src/main/java/com/google/genai/gaos/GenAI.java @@ -29,7 +29,6 @@ import java.lang.String; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; /** @@ -41,7 +40,7 @@ * cases like reasoning across text and images, content generation, dialogue agents, summarization and * classification systems, and more. */ -public class GenAI { +public class GenAI implements java.lang.AutoCloseable { private static final Headers _headers = Headers.EMPTY; @@ -94,6 +93,7 @@ public Triggers triggers() { public Environments environments() { return environments; } + private SDKConfiguration sdkConfiguration; private final AsyncGenAI asyncSDK; /** @@ -195,11 +195,10 @@ public Builder retryConfig(RetryConfig retryConfig) { * @param retryScheduler The ScheduledExecutorService to use. * @return The builder instance. */ - public Builder asyncRetryScheduler(ScheduledExecutorService retryScheduler) { + public Builder asyncRetryScheduler(java.util.concurrent.ScheduledExecutorService retryScheduler) { this.sdkConfiguration.setAsyncRetryScheduler(retryScheduler); return this; } - /** * Enables debug logging for HTTP requests and responses, including JSON body content. *

@@ -276,13 +275,14 @@ public static Builder builder() { private GenAI(SDKConfiguration sdkConfiguration) { sdkConfiguration.initialize(); + sdkConfiguration = sdkConfiguration.hooks().sdkInit(sdkConfiguration); this.interactions = new Interactions(sdkConfiguration); this.webhooks = new Webhooks(sdkConfiguration); this.agents = new Agents(sdkConfiguration); this.triggers = new Triggers(sdkConfiguration); this.environments = new Environments(sdkConfiguration); - sdkConfiguration = sdkConfiguration.hooks().sdkInit(sdkConfiguration); this.asyncSDK = new AsyncGenAI(this, sdkConfiguration); + this.sdkConfiguration = sdkConfiguration; } /** @@ -294,4 +294,14 @@ public AsyncGenAI async() { return asyncSDK; } + + /** + * Releases the configured HTTP client's owned resources. + * + * @throws Exception if the configured client cannot be closed + */ + @Override + public void close() throws Exception { + this.sdkConfiguration.closeClient(); + } } diff --git a/src/main/java/com/google/genai/gaos/SDKConfiguration.java b/src/main/java/com/google/genai/gaos/SDKConfiguration.java index 9ee365e02da..9250f64bdd0 100644 --- a/src/main/java/com/google/genai/gaos/SDKConfiguration.java +++ b/src/main/java/com/google/genai/gaos/SDKConfiguration.java @@ -37,7 +37,7 @@ public class SDKConfiguration { private static final String LANGUAGE = "java"; public static final String OPENAPI_DOC_VERSION = "v1beta"; public static final String SDK_VERSION = "0.1.0"; - public static final String GEN_VERSION = "2.924.0"; + public static final String GEN_VERSION = "2.930.0"; private static final String BASE_PACKAGE = "com.google.genai.gaos"; public static final String USER_AGENT = String.format("speakeasy-sdk/%s %s %s %s %s", @@ -64,6 +64,17 @@ public void setClient(HTTPClient client) { Utils.checkNotNull(client, "client"); this.client = client; } + + private final java.util.concurrent.atomic.AtomicReference closedClient = + new java.util.concurrent.atomic.AtomicReference<>(); + + public void closeClient() throws Exception { + Object client = client(); + if (closedClient.getAndSet(client) != client + && client instanceof java.lang.AutoCloseable) { + ((java.lang.AutoCloseable) client).close(); + } + } private String serverUrl; diff --git a/src/main/java/com/google/genai/gaos/utils/HTTPClient.java b/src/main/java/com/google/genai/gaos/utils/HTTPClient.java index f25e2fc0177..92a712b1e0e 100644 --- a/src/main/java/com/google/genai/gaos/utils/HTTPClient.java +++ b/src/main/java/com/google/genai/gaos/utils/HTTPClient.java @@ -26,7 +26,21 @@ import com.google.genai.gaos.utils.transport.HttpRequest; import com.google.genai.gaos.utils.transport.HttpResponse; -public interface HTTPClient { +public interface HTTPClient extends java.lang.AutoCloseable { + + /** + * Releases resources owned by this client. + * + *

The default is a no-op so custom implementations that borrow an + * externally owned client are not forced to tear down resources they do + * not own. Implementations that own resources should override this method. + * + * @throws Exception if an owned resource cannot be released + */ + @Override + default void close() throws Exception { + // Default no-op + } /** * Sends an HTTP request and returns the response. diff --git a/src/main/java/com/google/genai/gaos/utils/SpeakeasyHTTPClient.java b/src/main/java/com/google/genai/gaos/utils/SpeakeasyHTTPClient.java index 92044ab9e57..25218710550 100644 --- a/src/main/java/com/google/genai/gaos/utils/SpeakeasyHTTPClient.java +++ b/src/main/java/com/google/genai/gaos/utils/SpeakeasyHTTPClient.java @@ -353,4 +353,13 @@ private static byte[] toByteArray(InputStream in) throws IOException { } return buffer.toByteArray(); } + + @Override + public void close() throws Exception { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + if (client.cache() != null) { + client.cache().close(); + } + } } diff --git a/src/test/java/com/google/genai/AutoCloseableExecutionTest.java b/src/test/java/com/google/genai/AutoCloseableExecutionTest.java new file mode 100644 index 00000000000..e755cc2596d --- /dev/null +++ b/src/test/java/com/google/genai/AutoCloseableExecutionTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.genai; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +public class AutoCloseableExecutionTest { + + public static class SampleApp { + public static void main(String[] args) { + System.out.println("Starting SampleApp..."); + try (Client client = Client.builder().apiKey("dummy-key").vertexAI(false).build()) { + System.out.println("Client created and will close on exit."); + } + System.out.println("SampleApp main() finished."); + } + } + + @Test + public void testGenAiImplementsAutoCloseable() throws Exception { + Instant start = Instant.now(); + try (com.google.genai.gaos.GenAI genai = com.google.genai.gaos.GenAI.builder().build()) { + assertTrue(genai instanceof AutoCloseable); + } + Duration elapsed = Duration.between(start, Instant.now()); + assertTrue(elapsed.toMillis() < 5000, "Close took too long: " + elapsed.toMillis() + "ms"); + } + + @Test + public void testClientImplementsAutoCloseable() throws Exception { + Instant start = Instant.now(); + try (Client client = Client.builder().apiKey("test-key").vertexAI(false).build()) { + assertTrue(client instanceof AutoCloseable); + } + Duration elapsed = Duration.between(start, Instant.now()); + assertTrue(elapsed.toMillis() < 5000, "Close took too long: " + elapsed.toMillis() + "ms"); + } + + @Test + public void testSpeakeasyHTTPClientClosesDispatcherImmediately() throws Exception { + Instant start = Instant.now(); + try (com.google.genai.gaos.utils.SpeakeasyHTTPClient httpClient = + new com.google.genai.gaos.utils.SpeakeasyHTTPClient()) { + assertTrue(httpClient instanceof AutoCloseable); + } + Duration elapsed = Duration.between(start, Instant.now()); + assertTrue(elapsed.toMillis() < 5000, "Close took too long: " + elapsed.toMillis() + "ms"); + } + + @Test + public void testForkedJvmExitsImmediatelyWithout60sTimeout() throws Exception { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + + ProcessBuilder pb = + new ProcessBuilder(javaBin, "-cp", classpath, SampleApp.class.getName()); + pb.redirectErrorStream(true); + + Instant start = Instant.now(); + Process process = pb.start(); + boolean exited = process.waitFor(10, TimeUnit.SECONDS); + Duration elapsed = Duration.between(start, Instant.now()); + + assertTrue(exited, "Process hung and did not exit within 10 seconds!"); + assertEquals(0, process.exitValue(), "Process exited with non-zero exit code"); + assertTrue( + elapsed.toMillis() < 5000, + "Process exit took " + elapsed.toMillis() + "ms (should be < 5000ms, not 60000ms)"); + System.out.println("Forked JVM exited cleanly in " + elapsed.toMillis() + "ms."); + } +}