This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit cbcccb33bc35ba9977fcd905158be44947df8bae Author: Robert Lazarski <[email protected]> AuthorDate: Tue Aug 4 11:04:53 2026 -1000 Bound and shut down the response-endpoint resolver pool Second pre-push review pass, both findings valid against the previous commit. The cached pool was unbounded, so naming many distinct slow-resolving hosts could spawn a thread per request -- one denial of service traded for another. It is now capped at four threads with a 64-deep queue; a lookup refused because the queue is full rejects the endpoint, which fails closed. The pool was also never stopped, leaking the class loader across a redeployment. The addressing module already implements Module.shutdown, so the pool is torn down there -- the same treatment MultipartTempFileTracker gets from AxisConfiguration.cleanup(), which this had not mirrored. Co-Authored-By: Claude Fable 5 <[email protected]> --- .../axis2/handlers/addressing/Addressing.java | 1 + .../addressing/ResponseEndpointPolicy.java | 72 +++++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/modules/addressing/src/org/apache/axis2/handlers/addressing/Addressing.java b/modules/addressing/src/org/apache/axis2/handlers/addressing/Addressing.java index 047e8e1be6..63dbbaefbe 100644 --- a/modules/addressing/src/org/apache/axis2/handlers/addressing/Addressing.java +++ b/modules/addressing/src/org/apache/axis2/handlers/addressing/Addressing.java @@ -54,6 +54,7 @@ public class Addressing implements Module { } public void shutdown(ConfigurationContext configurationContext) throws AxisFault { + ResponseEndpointPolicy.shutdown(); } } diff --git a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java index 377c6befa2..f9d7a3fe28 100644 --- a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java +++ b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java @@ -37,9 +37,11 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -96,6 +98,12 @@ final class ResponseEndpointPolicy { private static ExecutorService resolver; + /** Caps concurrent name lookups so a burst cannot spawn a thread per request. */ + private static final int MAX_RESOLVER_THREADS = 4; + + /** Lookups beyond this wait; beyond the wait they are refused. */ + private static final int RESOLVER_QUEUE_DEPTH = 64; + /** * Schemes Axis2 ships a sender for that can carry a decoupled reply. * @@ -284,11 +292,17 @@ final class ResponseEndpointPolicy { * @return the resolved addresses, or null if the name did not resolve in time */ private static InetAddress[] resolveWithTimeout(final String host, long timeoutMillis) { - Future<InetAddress[]> pending = resolver().submit(new Callable<InetAddress[]>() { - public InetAddress[] call() throws UnknownHostException { - return InetAddress.getAllByName(host); - } - }); + Future<InetAddress[]> pending; + try { + pending = resolver().submit(new Callable<InetAddress[]>() { + public InetAddress[] call() throws UnknownHostException { + return InetAddress.getAllByName(host); + } + }); + } catch (RejectedExecutionException e) { + log.warn("Rejecting WS-Addressing response endpoint: too many name lookups in flight"); + return null; + } try { return pending.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { @@ -307,23 +321,49 @@ final class ResponseEndpointPolicy { } /** - * Lookup threads are daemons and the pool is unbounded-but-caching, so idle - * threads retire on their own and none of this outlives the JVM or holds up - * a redeployment. + * The pool that runs name lookups, created on first use. + * + * <p>Deliberately bounded. An unbounded pool would have swapped one denial + * of service for another: a caller naming many distinct slow-resolving hosts + * could spawn a thread per request. Excess lookups queue, and once the queue + * is full they are refused outright — which fails closed, since a refused + * lookup rejects the endpoint. + * + * <p>Threads are daemons and retire when idle, and {@link #shutdown()} stops + * the pool so it cannot pin a web application's class loader across a + * redeployment. */ private static synchronized ExecutorService resolver() { if (resolver == null) { - resolver = Executors.newCachedThreadPool(new ThreadFactory() { - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "axis2-wsa-endpoint-resolver"); - t.setDaemon(true); - return t; - } - }); + ThreadPoolExecutor pool = new ThreadPoolExecutor( + 0, MAX_RESOLVER_THREADS, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(RESOLVER_QUEUE_DEPTH), + new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "axis2-wsa-endpoint-resolver"); + t.setDaemon(true); + return t; + } + }, + new ThreadPoolExecutor.AbortPolicy()); + pool.allowCoreThreadTimeOut(true); + resolver = pool; } return resolver; } + /** + * Stop the resolver pool. Called from the addressing module's shutdown so + * the threads do not outlive the configuration that created them. + */ + static synchronized void shutdown() { + if (resolver != null) { + resolver.shutdownNow(); + resolver = null; + } + } + /** IPv6 unique local addresses, fc00::/7, which isSiteLocalAddress misses. */ private static boolean isUniqueLocalIPv6(InetAddress address) { byte[] bytes = address.getAddress();
