From 4b83c78618ad98c8e3c52ac0aabde0914d6946fa Mon Sep 17 00:00:00 2001 From: Vasil Ilchev Date: Tue, 28 Apr 2026 13:13:35 +0300 Subject: [PATCH] Fix cleaner clean reference to service before call on it - chain api (#3041) * Fix cleaner clean reference to service before call on it - chain api Signed-off-by: vasilchev * style and comment fix Signed-off-by: vasilchev --------- Signed-off-by: vasilchev --- .../eclipse/hawkbit/sdk/HawkbitClient.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java index c341b660a..24b236c49 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java @@ -17,6 +17,7 @@ import java.io.OutputStream; import java.io.Reader; import java.lang.annotation.Annotation; import java.lang.ref.Cleaner; +import java.lang.ref.Reference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -245,7 +246,7 @@ public class HawkbitClient { final HttpClientKey key = new HttpClientKey( url.startsWith("https://"), controller == null ? null : controller.getCertificate(), tenant.getTenantCA()); final HttpClient httpClient = httpClient(key); - final T service = Feign.builder() + final T rawService = Feign.builder() .client(new ApacheHttp5Client(httpClient)) .encoder(encoder) .decoder(decoder) @@ -253,8 +254,29 @@ public class HawkbitClient { .contract(contract) .requestInterceptor(requestInterceptorFn.apply(tenant, controller)) .target(serviceType, url); - CLEANER.register(service, key::release); - return service; + // JIT can consider a local proxy variable dead before the HTTP call it dispatched + // returns, causing CLEANER to fire mid-request and close the connection pool. + // Wrapping in a dynamic proxy and calling Reference.reachabilityFence(proxy) in + // every method's finally block prevents that: the proxy is live for the + // full duration of each call, so CLEANER only fires after the call completes, after 'finally' block. + final T wrapped = wrapWithReachabilityFence(serviceType, rawService); + CLEANER.register(wrapped, key::release); + return wrapped; + } + + @SuppressWarnings("unchecked") + private static T wrapWithReachabilityFence(final Class serviceType, final T delegate) { + return (T) Proxy.newProxyInstance( + delegate.getClass().getClassLoader(), new Class[] { serviceType }, + (proxy, method, args) -> { + try { + return method.invoke(delegate, args); + } catch (final InvocationTargetException e) { + throw e.getTargetException() == null ? e : e.getTargetException(); + } finally { + Reference.reachabilityFence(proxy); + } + }); } @SuppressWarnings("unchecked") @@ -516,6 +538,7 @@ public class HawkbitClient { HTTP_CLIENTS.remove(key); } try { + log.debug("Closing http client for {} - no pointers left", key); closeableHttpClient.close(); } catch (final IOException e) { log.error("Failed to close http client", e);