Release instances blocking GC when JAXB has PerThread injections

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
index e05cb5f..548a7e8 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,6 +16,7 @@
 
 package org.glassfish.jersey.client;
 
+import org.glassfish.jersey.innate.io.SafelyClosable;
 import org.glassfish.jersey.internal.BootstrapBag;
 import org.glassfish.jersey.internal.BootstrapConfigurator;
 import org.glassfish.jersey.internal.inject.Bindings;
@@ -29,7 +30,7 @@
 
 import javax.ws.rs.core.Configuration;
 
-class ClientMessageBodyFactory extends MessageBodyFactory {
+class ClientMessageBodyFactory extends MessageBodyFactory implements SafelyClosable {
 
     /**
      * Keep reference to {@link ClientRuntime} so that {@code finalize} on it is not called
@@ -39,7 +40,7 @@
      * but if the finalizer is invoked before that, the HK2 injection manager gets closed.
      * </p>
      */
-    private final LazyValue<ClientRuntime> clientRuntime;
+    private LazyValue<ClientRuntime> clientRuntime;
 
     /**
      * Create a new message body factory.
@@ -52,6 +53,11 @@
         clientRuntime = Values.lazy(clientRuntimeValue);
     }
 
+    @Override
+    public void close() {
+        clientRuntime = null;
+    }
+
     /**
      * Configurator which initializes and register {@link MessageBodyWorkers} instance into {@link InjectionManager} and
      * {@link BootstrapBag}.
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
new file mode 100644
index 0000000..33983b6
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate.io;
+
+/**
+ * A SafelyClosable is a resource that can be closed.
+ * The close method is invoked to release resources that the object is holding.
+ * Closing the resource is safe in a sense that no Exception is being thrown.
+ */
+public interface SafelyClosable {
+
+    /**
+     * Close the resource, no checked exception thrown.
+     */
+    void close();
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
index 4df032c..0eb86f3 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -32,6 +32,7 @@
 import javax.ws.rs.ext.ReaderInterceptor;
 import javax.ws.rs.ext.WriterInterceptor;
 
+import org.glassfish.jersey.innate.io.SafelyClosable;
 import org.glassfish.jersey.internal.PropertiesDelegate;
 
 /**
@@ -43,7 +44,7 @@
  * @see MessageBodyReader
  * @see MessageBodyWriter
  */
-public interface MessageBodyWorkers {
+public interface MessageBodyWorkers extends SafelyClosable {
     /**
      * Get the map of media type to list of message body writers that are compatible with
      * a media type.
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index a9c6dc1..26c4651 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,17 +23,13 @@
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
-import java.net.URI;
 import java.text.ParseException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -41,21 +37,17 @@
 
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.core.Configuration;
-import javax.ws.rs.core.Cookie;
-import javax.ws.rs.core.EntityTag;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Link;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.ext.ReaderInterceptor;
 
-import javax.ws.rs.ext.RuntimeDelegate;
 import javax.xml.transform.Source;
 
+import org.glassfish.jersey.innate.io.SafelyClosable;
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.PropertiesDelegate;
-import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
 import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
 import org.glassfish.jersey.internal.util.collection.LazyValue;
 import org.glassfish.jersey.internal.util.collection.Value;
@@ -67,7 +59,7 @@
  *
  * @author Marek Potociar
  */
-public abstract class InboundMessageContext extends MessageHeaderMethods {
+public abstract class InboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
 
     private static final InputStream EMPTY = new InputStream() {
 
@@ -729,6 +721,9 @@
      */
     public void close() {
         entityContent.close(true);
+        if (workers != null) {
+            workers.close();
+        }
         setWorkers(null);
     }
 
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
index f8d31a2..698ab14 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -1189,4 +1189,11 @@
         }
         return false;
     }
+
+
+    @Override
+    public void close() {
+        // NOOP
+    }
+
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index 8e10cdf..fbadb05 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -39,6 +39,7 @@
 import javax.ws.rs.core.MultivaluedMap;
 
 import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.innate.io.SafelyClosable;
 import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
 import org.glassfish.jersey.internal.util.ReflectionHelper;
 import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
@@ -52,7 +53,7 @@
  *
  * @author Marek Potociar
  */
-public class OutboundMessageContext extends MessageHeaderMethods {
+public class OutboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
     private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
     private static final List<MediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
             Collections.<MediaType>singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
new file mode 100644
index 0000000..8a836b7
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.client;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JerseyClientRuntimeTest {
+
+    private static int COUNT = 10;
+    private List<WeakReference<Object>> list = new ArrayList<>();
+    private ReferenceQueue queue = new ReferenceQueue();
+
+    @Test
+    public void testClientRuntimeInstancesAreGCed() throws InterruptedException {
+        Client c = ClientBuilder.newClient();
+        c.register(new ClientRequestFilter() {
+            @Override
+            public void filter(ClientRequestContext requestContext) throws IOException {
+                requestContext.abortWith(Response
+                                .ok("<myDTO xmlns=\"http://org.example.dtos\"/>")
+                                .type(MediaType.APPLICATION_XML_TYPE)
+                        .build());
+            }
+        });
+
+        WebTarget target = c.target("http://localhost/nowhere");
+        for (int i = 0; i != COUNT; i++) {
+            target = target.property("SOME", "PROPERTY");
+            ClientConfig config = (ClientConfig) target.getConfiguration();
+            Object clientRuntime = getClientRuntime(config);
+            addToList(clientRuntime);
+            try (Response response = target.request().get()) {
+                MatcherAssert.assertThat(response.getStatus(), Matchers.is(200));
+                MyDTO dto = response.readEntity(MyDTO.class);
+                MatcherAssert.assertThat(dto, Matchers.notNullValue());
+            }
+        }
+
+        System.gc();
+        do {
+            Thread.sleep(100L);
+        } while (queueIsEmpty(queue));
+
+        c.close();
+
+    }
+
+    private static Object getClientRuntime(ClientConfig config) {
+        try {
+            Method m = ClientConfig.class.getDeclaredMethod("getRuntime");
+            m.setAccessible(true);
+            return m.invoke(config);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private static boolean queueIsEmpty(ReferenceQueue queue) {
+        return queue.poll() == null;
+    }
+
+    private void addToList(Object object) {
+        list.add(new WeakReference<>(object, queue));
+    }
+
+    @XmlRootElement(name = "myDTO", namespace = "http://org.example.dtos")
+    public static class MyDTO {
+
+    }
+}