Skip to content

fix: Prevent OkHttp idle threads from blocking JVM shutdown#1268

Merged
copybara-service[bot] merged 1 commit into
google:mainfrom
svetanis:fix/graceful-jvm-shutdown
Jul 2, 2026
Merged

fix: Prevent OkHttp idle threads from blocking JVM shutdown#1268
copybara-service[bot] merged 1 commit into
google:mainfrom
svetanis:fix/graceful-jvm-shutdown

Conversation

@svetanis

Copy link
Copy Markdown
Contributor

Prevent OkHttp idle threads from blocking JVM shutdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:
The ADK framework does not cleanly terminate the JVM upon process completion when making chat completions requests because ChatCompletionsHttpClient and the internal Gemini API client construct default OkHttpClient instances. These produce non-daemon threads with a keep-alive timeout, keeping the JVM alive.

Solution:
Created HttpUtils.createSharedHttpClient() utility method that returns an OkHttpClient using a custom dispatcher with a daemon thread factory and 5-minute timeouts. Refactored ChatCompletionsHttpClient, ApiClient, and Gemini to invoke this utility and maintain their own shared daemon-thread client pools.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

All existing core tests pass successfully. (A new test for HttpUtils can be added to verify the ThreadFactory creates daemon threads).

Manual End-to-End (E2E) Tests:

Wired the Google native chat completion client into the external model-prism project (PR #1199). Ran the integration through all the demo tests covering all main ADK features. Explicitly tested the native Gemini provider (via the google_search_agent tests) as well as third-party OpenAI-compatible providers. After the final agent response is logged, the Java process successfully exits cleanly with code 0 across all model types instead of hanging while waiting for background threads to timeout.

Checklist

  • I have read the CONTRIBUTING.md document.
  • My pull request contains a single commit.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

This fixes critical thread leaks for users running short-lived or background ADK agents.

@hemasekhar-p

Copy link
Copy Markdown
Contributor

Hi @svetanis, thank you for your contribution! We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team, we will keep you posted if any additional information is required. thank you.

@hemasekhar-p

Copy link
Copy Markdown
Contributor

@anFatum, Could you please review this PR.

@hemasekhar-p hemasekhar-p self-assigned this Jun 15, 2026
@svetanis svetanis force-pushed the fix/graceful-jvm-shutdown branch from f92a4ab to b95dd45 Compare June 16, 2026 00:08
@svetanis

Copy link
Copy Markdown
Contributor Author

Hi @hemasekhar-p,

Thanks for the update! Just a quick heads up: I went ahead and rebased the branch onto the latest main so that it stays perfectly clean and conflict-free for your review.

Looking forward to hearing back from the team!

@krwc krwc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! Left some comments requiring changes, but no major issues.

@@ -0,0 +1,62 @@
/*
* Copyright 2025 Google LLC

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 2026

@@ -0,0 +1,29 @@
package com.google.adk.utils;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add copyright headers to this file.

Comment on lines +57 to +59
.connectTimeout(Duration.ofMinutes(5))
.readTimeout(Duration.ofMinutes(5))
.writeTimeout(Duration.ofMinutes(5))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OkHttp's default timeouts are 10s. Any reason we decided to set them to 5mins now? If not, let's not change them to maintain backwards compat and let the callers decide.

import okhttp3.OkHttpClient;

/** Utility class for common HTTP client configuration across the ADK. */
public final class HttpUtils {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we can't make this class truly package private due to its use in multiple packages, I still think we should move it to somewhere else, indicating its not truly intended to be a part of public API of Java ADK.

How about we move it into core/src/main/java/com/google/adk/internal/http?

import okhttp3.OkHttpClient;

/** Utility class for common HTTP client configuration across the ADK. */
public final class HttpUtils {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: HttpClientFactory?

@krwc krwc added the waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. label Jul 1, 2026
…hutdown

This fixes an issue where the JVM hangs waiting for background networking threads to timeout after completing an execution. Replaced default OkHttpClient builders with a new HttpUtils.createSharedHttpClient factory that injects a custom thread factory setting daemon=true.
@svetanis svetanis force-pushed the fix/graceful-jvm-shutdown branch from b95dd45 to 9b046b6 Compare July 2, 2026 02:57
@svetanis

svetanis commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@krwc,

Thanks for the review! I've addressed all your comments:

  1. Copyright: Updated the year to 2026 in HttpClientFactory and added the standard header to the test file.
  2. Rename & Move: Moved the utility to core/src/main/java/com/google/adk/internal/http/HttpClientFactory.java to indicate it is not part of the public API, and renamed the test file accordingly.
  3. Timeouts: Removed the 5-minute timeouts from HttpClientFactory. Let the callers (ChatCompletionsHttpClient and the GenAI SDK) manage their own timeouts appropriately.

Rebased onto latest main, single clean commit, all tests passing.

@krwc krwc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@krwc krwc added ready to pull and removed waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. needs review labels Jul 2, 2026
@copybara-service copybara-service Bot merged commit ec6c45d into google:main Jul 2, 2026
8 checks passed
@krwc

krwc commented Jul 3, 2026

Copy link
Copy Markdown

FYI: we actually had to roll it back due to some issue with one of the internal systems using it. Will investigate this and try to forward fix.

@svetanis

svetanis commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@krwc ,

Thanks for the heads-up. Sorry to hear about the issue.

One area that might be worth investigating is the timeout configuration. The final version of the PR removed the explicit timeouts from the shared OkHttpClient factory, which means callers that don't set their own read/write timeouts would fall back to OkHttp's 10-second defaults.

Happy to help with the forward fix. A couple of options:

  1. Restore defaults in HttpClientFactory — add back sensible default timeouts (e.g. 5 minutes) so existing callers aren't surprised. Safest approach since we can't audit all consumers.
  2. Fix each caller — update ApiClient (and any other callers) to explicitly set their own read/write timeouts. Cleaner long-term but requires knowing every consumer.

The daemon thread change itself should be safe to keep. Let me know how you'd like to proceed.

@svetanis

svetanis commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@krwc ,

Following up on my earlier comment. I dug into this a bit more and I think I understand what's happening. It changes both where the problem is and what I'd suggest for the fix.

The change silently dropped Gemini/Vertex transport timeouts from infinite to 10s, on the Gemini path.

In google-genai:1.58.0 (com.google.genai.ApiClient#createHttpClient), when no custom client is set, the SDK builds its own OkHttpClient with connect/read/write = 0 (infinite, deliberate for LLM streaming) and applies httpOptions.timeout() plus proxy. When a custom client is injected, as this PR did via ClientOptions.customHttpClient, it skips all of that. Our injected client set no timeouts, so it carried OkHttp's 10s defaults onto a path that was previously infinite. Any generation whose time-to-first-byte or inter-chunk gap exceeds ~10s fails with SocketTimeoutException. That doesn't happen on every call, just often enough. A few demo runs can miss it; production traffic won't. If you still have a trace from the rollback, I'd expect it to show a SocketTimeoutException on the Gemini path.

One correction to my earlier comment: I'd floated restoring ~5-minute defaults, but the right value is infinite, for exact parity with the SDK. On this path OkHttp's 10s default isn't the backwards-compatible value; the SDK's infinite is.

Proposed forward fix (keeps the JVM-shutdown fix from #1267/#1268):

  • Build the injected client with connect/read/write = 0 plus the daemon dispatcher, and make it per-Gemini-instance rather than a shared static. The static also capped async concurrency at OkHttp's default 5-in-flight-per-host across the whole JVM. User-supplied Clients stay untouched.
  • Keep the daemon dispatcher for ChatCompletionsHttpClient; drop the sessions/ApiClient change (it's synchronous-only, so the daemon dispatcher was inert there).
  • Add a regression test asserting the client actually handed to the SDK has infinite timeouts and daemon threads, so neither the 10s default nor the hang can silently return.

How would you like to proceed? I can open a new issue and a PR, just a PR referencing #1267/#1268, or hold off while you handle it internally. Please let me know what's easiest on your end.

@krwc

krwc commented Jul 10, 2026

Copy link
Copy Markdown

We will handle this internally, I'm working on a relevant change in the background. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent OkHttp idle threads from blocking JVM shutdown

3 participants