Skip to content

fix: remove static ExecutorService in AsyncFeign to prevent ClassLoader leak#3308

Closed
anuragg-saxenaa wants to merge 1 commit intoOpenFeign:masterfrom
anuragg-saxenaa:fix/issue-3178-async-executor-classloader-leak
Closed

fix: remove static ExecutorService in AsyncFeign to prevent ClassLoader leak#3308
anuragg-saxenaa wants to merge 1 commit intoOpenFeign:masterfrom
anuragg-saxenaa:fix/issue-3178-async-executor-classloader-leak

Conversation

@anuragg-saxenaa
Copy link
Copy Markdown

Problem

AsyncFeign held a static singleton ExecutorService via LazyInitializedExecutorService:

private static class LazyInitializedExecutorService {
  private static final ExecutorService instance =
      Executors.newCachedThreadPool(r -> {
        final Thread result = new Thread(r);
        result.setDaemon(true);
        return result;
      });
}

In servlet containers (Tomcat, Jetty, etc.), redeploying an application does not restart the JVM. The static thread pool holds a strong reference to the ContextClassLoader of the first deployment. On each redeployment:

  • The old ClassLoader is never GC'd (retained by the static thread pool's threads)
  • Thread count grows linearly
  • Metaspace eventually exhausts → OutOfMemoryError

Setting threads as daemon only helps at JVM exit — it does not prevent ClassLoader leaks during hot-redeploy.

Fix

Remove LazyInitializedExecutorService entirely. AsyncBuilder.client is now initialized with a fresh Executors.newCachedThreadPool(...) per builder instance. This means:

  • Each AsyncFeign.builder() creates its own independent executor
  • When the client is discarded, the executor becomes eligible for GC
  • Users who need lifecycle control or sharing can supply their own via .client(AsyncClient)

Testing

Added asyncFeignHasNoStaticExecutorService() to AsyncFeignTest — uses reflection to assert that no static field of type ExecutorService exists in any inner class of AsyncFeign.

Fixes #3178

… ClassLoader leaks

AsyncFeign used a LazyInitializedExecutorService inner class to hold a
static singleton ExecutorService. In servlet containers (Tomcat, Jetty),
redeploying an application does not restart the JVM. The static thread
pool retains a strong reference to the ContextClassLoader of the first
deployment, preventing it from being GC'd on subsequent redeployments.
This causes ClassLoader leaks and eventual Metaspace OutOfMemoryError.

The fix removes the static LazyInitializedExecutorService entirely.
AsyncBuilder now creates a fresh per-instance executor in its default
client field initializer. Users who need to share or control lifecycle of
the executor should continue to supply their own via builder.client().

Fixes OpenFeign#3178

Signed-off-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
@yvasyliev
Copy link
Copy Markdown
Contributor

isn't it already covered in #3277?

@anuragg-saxenaa
Copy link
Copy Markdown
Author

Closing as duplicate of #3277 which addresses the same issue with a more complete fix (also covering CoroutineFeign). Thanks for the pointer @yvasyliev!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Static ExecutorService in AsyncFeign causes ClassLoader Leak (Metaspace OOM)

2 participants