diff --git a/.fernignore b/.fernignore index 4734b1b..157ce56 100644 --- a/.fernignore +++ b/.fernignore @@ -1,4 +1,4 @@ -# Custom client wrappers (extend generated DeepgramApiClient with Bearer auth, session ID) +# Custom client wrappers (Bearer auth, session ID, and owned HTTP-resource lifecycle) # Flat paths (local generation strips package-prefix) src/main/java/DeepgramClient.java src/main/java/AsyncDeepgramClient.java diff --git a/AGENTS.md b/AGENTS.md index 39cb638..b5842a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ How to identify: Current permanently frozen files: -- `src/main/java/com/deepgram/DeepgramClient.java`, `src/main/java/com/deepgram/AsyncDeepgramClient.java`, `src/main/java/com/deepgram/DeepgramClientBuilder.java`, `src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java` - custom wrapper entrypoints that add Bearer auth, session ID support, and custom transport behavior on top of Fern's generated API client +- `src/main/java/com/deepgram/DeepgramClient.java`, `src/main/java/com/deepgram/AsyncDeepgramClient.java`, `src/main/java/com/deepgram/DeepgramClientBuilder.java`, `src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java` - custom wrapper entrypoints that add Bearer auth, session ID support, custom transport behavior, and ownership-aware HTTP-resource lifecycle management on top of Fern's generated API client - `src/main/java/com/deepgram/core/transport/` - hand-written transport abstraction - `build.gradle`, `settings.gradle`, `gradle/`, `gradlew`, `gradlew.bat`, `pom.xml`, `Makefile` - build and project configuration - `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `LICENSE`, `docs/` - docs diff --git a/README.md b/README.md index 752d92c..bf40015 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,21 @@ DeepgramClient client = DeepgramClient.builder() .build(); ``` +### Resource lifecycle + +Close an SDK-created client when your application is finished with it. This releases the OkHttp +dispatcher and connection pool, which is especially important for short-lived command-line programs +that use WebSockets. + +```java +try (DeepgramClient client = DeepgramClient.builder().build()) { + // Use the client. +} +``` + +If you provide an `OkHttpClient` through `.httpClient(...)`, you retain ownership and must close its +resources yourself. + ## Features ### Speech-to-Text (Listen) diff --git a/src/main/java/com/deepgram/AsyncDeepgramClient.java b/src/main/java/com/deepgram/AsyncDeepgramClient.java index 185c556..57031ff 100644 --- a/src/main/java/com/deepgram/AsyncDeepgramClient.java +++ b/src/main/java/com/deepgram/AsyncDeepgramClient.java @@ -5,12 +5,32 @@ */ import com.deepgram.core.ClientOptions; -public class AsyncDeepgramClient extends AsyncDeepgramApiClient { +public class AsyncDeepgramClient extends AsyncDeepgramApiClient implements AutoCloseable { + private final boolean ownsHttpClient; + public AsyncDeepgramClient(ClientOptions clientOptions) { + this(clientOptions, false); + } + + AsyncDeepgramClient(ClientOptions clientOptions, boolean ownsHttpClient) { super(clientOptions); + this.ownsHttpClient = ownsHttpClient; } public static AsyncDeepgramClientBuilder builder() { return new AsyncDeepgramClientBuilder(); } + + /** + * Releases resources owned by an SDK-created HTTP client. Clients supplied through + * {@link AsyncDeepgramClientBuilder#httpClient(okhttp3.OkHttpClient)} remain owned by the caller. + */ + @Override + public void close() { + if (!ownsHttpClient) { + return; + } + clientOptions.httpClient().dispatcher().executorService().shutdown(); + clientOptions.httpClient().connectionPool().evictAll(); + } } diff --git a/src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java b/src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java index 074f04c..ed391da 100644 --- a/src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java +++ b/src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java @@ -30,6 +30,8 @@ public class AsyncDeepgramClientBuilder extends AsyncDeepgramApiClientBuilder { private DeepgramTransportFactory transportFactory; + private boolean hasCustomHttpClient; + /** * Sets a custom transport factory for all WebSocket connections. When set, WebSocket clients will use this factory * instead of the default OkHttp WebSocket. Use this to route Deepgram API calls through alternative transports such @@ -87,6 +89,7 @@ public AsyncDeepgramClientBuilder maxRetries(int maxRetries) { @Override public AsyncDeepgramClientBuilder httpClient(OkHttpClient httpClient) { + this.hasCustomHttpClient = httpClient != null; super.httpClient(httpClient); return this; } @@ -149,6 +152,6 @@ public AsyncDeepgramClient build() { "Please provide apiKey, accessToken, or set the DEEPGRAM_API_KEY environment variable."); } validateConfiguration(); - return new AsyncDeepgramClient(buildClientOptions()); + return new AsyncDeepgramClient(buildClientOptions(), !hasCustomHttpClient); } } diff --git a/src/main/java/com/deepgram/DeepgramClient.java b/src/main/java/com/deepgram/DeepgramClient.java index 96b08c0..6036300 100644 --- a/src/main/java/com/deepgram/DeepgramClient.java +++ b/src/main/java/com/deepgram/DeepgramClient.java @@ -30,12 +30,32 @@ */ import com.deepgram.core.ClientOptions; -public class DeepgramClient extends DeepgramApiClient { +public class DeepgramClient extends DeepgramApiClient implements AutoCloseable { + private final boolean ownsHttpClient; + public DeepgramClient(ClientOptions clientOptions) { + this(clientOptions, false); + } + + DeepgramClient(ClientOptions clientOptions, boolean ownsHttpClient) { super(clientOptions); + this.ownsHttpClient = ownsHttpClient; } public static DeepgramClientBuilder builder() { return new DeepgramClientBuilder(); } + + /** + * Releases resources owned by an SDK-created HTTP client. Clients supplied through + * {@link DeepgramClientBuilder#httpClient(okhttp3.OkHttpClient)} remain owned by the caller. + */ + @Override + public void close() { + if (!ownsHttpClient) { + return; + } + clientOptions.httpClient().dispatcher().executorService().shutdown(); + clientOptions.httpClient().connectionPool().evictAll(); + } } diff --git a/src/main/java/com/deepgram/DeepgramClientBuilder.java b/src/main/java/com/deepgram/DeepgramClientBuilder.java index 614a23e..2b74968 100644 --- a/src/main/java/com/deepgram/DeepgramClientBuilder.java +++ b/src/main/java/com/deepgram/DeepgramClientBuilder.java @@ -29,6 +29,8 @@ public class DeepgramClientBuilder extends DeepgramApiClientBuilder { private DeepgramTransportFactory transportFactory; + private boolean hasCustomHttpClient; + /** * Sets a custom transport factory for all WebSocket connections. When set, WebSocket clients will use this factory * instead of the default OkHttp WebSocket. Use this to route Deepgram API calls through alternative transports such @@ -86,6 +88,7 @@ public DeepgramClientBuilder maxRetries(int maxRetries) { @Override public DeepgramClientBuilder httpClient(OkHttpClient httpClient) { + this.hasCustomHttpClient = httpClient != null; super.httpClient(httpClient); return this; } @@ -148,6 +151,6 @@ public DeepgramClient build() { "Please provide apiKey, accessToken, or set the DEEPGRAM_API_KEY environment variable."); } validateConfiguration(); - return new DeepgramClient(buildClientOptions()); + return new DeepgramClient(buildClientOptions(), !hasCustomHttpClient); } } diff --git a/src/test/java/com/deepgram/ClientBuilderTest.java b/src/test/java/com/deepgram/ClientBuilderTest.java index fd7dacd..fc9e251 100644 --- a/src/test/java/com/deepgram/ClientBuilderTest.java +++ b/src/test/java/com/deepgram/ClientBuilderTest.java @@ -145,6 +145,81 @@ void testCustomHttpClient() { } } + @Nested + @DisplayName("Client lifecycle") + class ClientLifecycle { + @Test + @DisplayName("closing the default client releases SDK-owned HTTP resources") + void closesDefaultClientResources() { + DeepgramClient client = DeepgramClient.builder().apiKey("test-key").build(); + + client.close(); + + assertThat(client.clientOptions + .httpClient() + .dispatcher() + .executorService() + .isShutdown()) + .isTrue(); + } + + @Test + @DisplayName("closing the default async client releases SDK-owned HTTP resources") + void closesDefaultAsyncClientResources() { + AsyncDeepgramClient client = + AsyncDeepgramClient.builder().apiKey("test-key").build(); + + client.close(); + + assertThat(client.clientOptions + .httpClient() + .dispatcher() + .executorService() + .isShutdown()) + .isTrue(); + } + + @Test + @DisplayName("closing a client does not release caller-owned HTTP resources") + void doesNotCloseCustomClientResources() { + OkHttpClient customHttpClient = new OkHttpClient.Builder().build(); + DeepgramClient client = DeepgramClient.builder() + .apiKey("test-key") + .httpClient(customHttpClient) + .build(); + + try { + client.close(); + + assertThat(customHttpClient.dispatcher().executorService().isShutdown()) + .isFalse(); + } finally { + customHttpClient.dispatcher().executorService().shutdown(); + customHttpClient.connectionPool().evictAll(); + } + } + + @Test + @DisplayName("closing an async client does not release caller-owned HTTP resources") + void doesNotCloseCustomAsyncClientResources() { + OkHttpClient customHttpClient = new OkHttpClient.Builder().build(); + AsyncDeepgramClient client = AsyncDeepgramClient.builder() + .apiKey("test-key") + .httpClient(customHttpClient) + .build(); + + try { + client.close(); + + assertThat(customHttpClient.dispatcher().executorService().isShutdown()) + .isFalse(); + } finally { + customHttpClient.dispatcher().executorService().shutdown(); + customHttpClient.connectionPool().evictAll(); + } + } + } + @Nested @DisplayName("Custom headers configuration") class CustomHeadersConfiguration {