From 1a318f3a15f457988c21986252c10d4a9271563f Mon Sep 17 00:00:00 2001 From: jansupol Date: Fri, 29 Nov 2024 14:03:26 +0100 Subject: [PATCH] Fix memory leak when client does not use HK2 Signed-off-by: jansupol --- .../innate/inject/NonInjectionManager.java | 253 +++++++++++------- .../inject/NonInjectionRequestScope.java | 27 +- tests/e2e-inject/non-inject/pom.xml | 57 ++++ .../noninject/DisposableSuplierTest.java | 98 +++++++ .../noninject/InstanceListSizeTest.java | 195 ++++++++++++++ .../e2e/inject/noninject/PreDestroyTest.java | 96 +++++++ tests/e2e-inject/pom.xml | 1 + 7 files changed, 628 insertions(+), 99 deletions(-) create mode 100644 tests/e2e-inject/non-inject/pom.xml create mode 100644 tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java create mode 100644 tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java create mode 100644 tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java index cc3b8eacad..a378ef0212 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java @@ -17,6 +17,7 @@ package org.glassfish.jersey.client.innate.inject; import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.inject.AbstractBinder; import org.glassfish.jersey.internal.inject.Binder; import org.glassfish.jersey.internal.inject.Binding; import org.glassfish.jersey.internal.inject.ClassBinding; @@ -30,6 +31,7 @@ import org.glassfish.jersey.internal.inject.SupplierClassBinding; import org.glassfish.jersey.internal.inject.SupplierInstanceBinding; import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Ref; import org.glassfish.jersey.internal.util.collection.Value; import org.glassfish.jersey.internal.util.collection.Values; import org.glassfish.jersey.process.internal.RequestScope; @@ -52,10 +54,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; -import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -74,8 +78,6 @@ public final class NonInjectionManager implements InjectionManager { private final MultivaluedMap> supplierTypeInstanceBindings = new MultivaluedHashMap<>(); private final MultivaluedMap> supplierTypeClassBindings = new MultivaluedHashMap<>(); - private final MultivaluedMap disposableSupplierObjects = new MultivaluedHashMap<>(); - private final Instances instances = new Instances(); private final Types types = new Types(); @@ -88,10 +90,11 @@ public final class NonInjectionManager implements InjectionManager { * @param the type for which the instance is created, either Class, or ParametrizedType (for instance * Provider<SomeClass>). */ - private class TypedInstances { + private class TypedInstances { private final MultivaluedMap> singletonInstances = new MultivaluedHashMap<>(); private ThreadLocal>> threadInstances = new ThreadLocal<>(); - private final List threadPredestroyables = Collections.synchronizedList(new LinkedList<>()); + private ThreadLocal> disposableSupplierObjects = + ThreadLocal.withInitial(() -> new MultivaluedHashMap<>()); private List> _getSingletons(TYPE clazz) { List> si; @@ -102,7 +105,7 @@ private List> _getSingletons(TYPE clazz) { } @SuppressWarnings("unchecked") - T _addSingleton(TYPE clazz, T instance, Binding binding, Annotation[] qualifiers) { + T _addSingleton(TYPE clazz, T instance, Binding binding, Annotation[] qualifiers, boolean destroy) { synchronized (singletonInstances) { // check existing singleton with a qualifier already created by another thread io a meantime List> values = singletonInstances.get(clazz); @@ -115,19 +118,19 @@ T _addSingleton(TYPE clazz, T instance, Binding binding, Annotation[] return (T) qualified.get(0).instance; } } - singletonInstances.add(clazz, new InstanceContext<>(instance, binding, qualifiers)); - threadPredestroyables.add(instance); + InstanceContext instanceContext = new InstanceContext<>(instance, binding, qualifiers, !destroy); + singletonInstances.add(clazz, instanceContext); return instance; } } @SuppressWarnings("unchecked") T addSingleton(TYPE clazz, T t, Binding binding, Annotation[] instanceQualifiers) { - T t2 = _addSingleton(clazz, t, binding, instanceQualifiers); + T t2 = _addSingleton(clazz, t, binding, instanceQualifiers, true); if (t2 == t) { for (Type contract : binding.getContracts()) { if (!clazz.equals(contract) && isClass(contract)) { - _addSingleton((TYPE) contract, t, binding, instanceQualifiers); + _addSingleton((TYPE) contract, t, binding, instanceQualifiers, false); } } } @@ -143,21 +146,22 @@ private List> _getThreadInstances(TYPE clazz) { return list; } - private void _addThreadInstance(TYPE clazz, T instance, Binding binding, Annotation[] qualifiers) { + private void _addThreadInstance( + TYPE clazz, T instance, Binding binding, Annotation[] qualifiers, boolean destroy) { MultivaluedMap> map = threadInstances.get(); if (map == null) { map = new MultivaluedHashMap<>(); threadInstances.set(map); } - map.add(clazz, new InstanceContext<>(instance, binding, qualifiers)); - threadPredestroyables.add(instance); + InstanceContext instanceContext = new InstanceContext<>(instance, binding, qualifiers, !destroy); + map.add(clazz, instanceContext); } void addThreadInstance(TYPE clazz, T t, Binding binding, Annotation[] instanceQualifiers) { - _addThreadInstance(clazz, t, binding, instanceQualifiers); + _addThreadInstance(clazz, t, binding, instanceQualifiers, true); for (Type contract : binding.getContracts()) { if (!clazz.equals(contract) && isClass(contract)) { - _addThreadInstance((TYPE) contract, t, binding, instanceQualifiers); + _addThreadInstance((TYPE) contract, t, binding, instanceQualifiers, false); } } } @@ -175,28 +179,78 @@ List> getContexts(TYPE clazz, Annotation[] annotations) { private List> _getContexts(TYPE clazz) { List> si = _getSingletons(clazz); List> ti = _getThreadInstances(clazz); - if (si == null && ti != null) { - si = ti; - } else if (ti != null) { - si.addAll(ti); - } - return si; + return InstanceContext.merge(si, ti); } T getInstance(TYPE clazz, Annotation[] annotations) { List i = getInstances(clazz, annotations); if (i != null) { checkUnique(i); - return i.get(0); + return instanceOrSupply(clazz, i.get(0)); } return null; } + private T instanceOrSupply(TYPE clazz, T t) { + if (!Class.class.isInstance(clazz) || ((Class) clazz).isInstance(t)) { + return t; + } else if (Supplier.class.isInstance(t)) { + return (T) registerDisposableSupplierAndGet((Supplier) t, this); + } else if (Provider.class.isInstance(t)) { + return (T) ((Provider) t).get(); + } else { + return t; + } + } + void dispose() { singletonInstances.forEach((clazz, instances) -> instances.forEach(instance -> preDestroy(instance.getInstance()))); - threadPredestroyables.forEach(NonInjectionManager.this::preDestroy); + disposeThreadInstances(true); /* The java.lang.ThreadLocal$ThreadLocalMap$Entry[] keeps references to this NonInjectionManager */ threadInstances = null; + disposableSupplierObjects = null; + } + + void disposeThreadInstances(boolean allThreadInstances) { + MultivaluedMap> ti = threadInstances.get(); + if (ti == null) { + return; + } + Set>>> tiSet = ti.entrySet(); + Iterator>>> tiSetIt = tiSet.iterator(); + while (tiSetIt.hasNext()) { + Map.Entry>> entry = tiSetIt.next(); + Iterator> listIt = entry.getValue().iterator(); + while (listIt.hasNext()) { + InstanceContext instanceContext = listIt.next(); + if (allThreadInstances || instanceContext.getBinding().getScope() != PerThread.class) { + listIt.remove(); + if (DisposableSupplier.class.isInstance(instanceContext.getInstance())) { + MultivaluedMap disposeMap = disposableSupplierObjects.get(); + Iterator>> disposeMapIt = disposeMap.entrySet().iterator(); + while (disposeMapIt.hasNext()) { + Map.Entry> disposeMapEntry = disposeMapIt.next(); + if (disposeMapEntry.getKey() == /* identity */ instanceContext.getInstance()) { + Iterator disposeMapEntryIt = disposeMapEntry.getValue().iterator(); + while (disposeMapEntryIt.hasNext()) { + Object disposeInstance = disposeMapEntryIt.next(); + ((DisposableSupplier) instanceContext.getInstance()).dispose(disposeInstance); + disposeMapEntryIt.remove(); + } + } + if (disposeMapEntry.getValue().isEmpty()) { + disposeMapIt.remove(); + } + } + } + instanceContext.destroy(NonInjectionManager.this); + } + if (entry.getValue().isEmpty()) { + tiSetIt.remove(); + } + } + } + disposableSupplierObjects.remove(); } } @@ -207,9 +261,19 @@ private class Types extends TypedInstances { } public NonInjectionManager() { + Binding binding = new AbstractBinder() { + @Override + protected void configure() { + bind(NonInjectionRequestScope.class).to(RequestScope.class).in(Singleton.class); + } + }.getBindings().iterator().next(); + RequestScope scope = new NonInjectionRequestScope(this); + instances.addSingleton(RequestScope.class, scope, binding, null); + types.addSingleton(RequestScope.class, scope, binding, null); } public NonInjectionManager(boolean warning) { + this(); if (warning) { logger.warning(LocalizationMessages.NONINJECT_FALLBACK()); } else { @@ -219,20 +283,21 @@ public NonInjectionManager(boolean warning) { @Override public void completeRegistration() { - instances._addSingleton(InjectionManager.class, this, new InjectionManagerBinding(), null); + instances._addSingleton(InjectionManager.class, this, new InjectionManagerBinding(), null, false); } @Override public void shutdown() { shutdown = true; - - disposableSupplierObjects.forEach((supplier, objects) -> objects.forEach(supplier::dispose)); - disposableSupplierObjects.clear(); - instances.dispose(); types.dispose(); } + void disposeRequestScopedInstances() { + instances.disposeThreadInstances(false); + types.disposeThreadInstances(false); + } + @Override public boolean isShutdown() { return shutdown; @@ -411,12 +476,7 @@ public T create(Class createMe) { return (T) this; } if (RequestScope.class.equals(createMe)) { - if (!isRequestScope) { - isRequestScope = true; - return (T) new NonInjectionRequestScope(); - } else { - throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); - } + throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); } ClassBindings classBindings = classBindings(createMe); @@ -431,12 +491,7 @@ public T createAndInitialize(Class createMe) { return (T) this; } if (RequestScope.class.equals(createMe)) { - if (!isRequestScope) { - isRequestScope = true; - return (T) new NonInjectionRequestScope(); - } else { - throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); - } + throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); } ClassBindings classBindings = classBindings(createMe); @@ -535,7 +590,9 @@ public void inject(Object injectMe, String classAnalyzer) { @Override public void preDestroy(Object preDestroyMe) { - Method preDestroy = getAnnotatedMethod(preDestroyMe, PreDestroy.class); + Method preDestroy = Method.class.isInstance(preDestroyMe) + ? (Method) preDestroyMe + : getAnnotatedMethod(preDestroyMe, PreDestroy.class); if (preDestroy != null) { ensureAccessible(preDestroy); try { @@ -567,20 +624,27 @@ private static Method getAnnotatedMethod(Object object, Class T createSupplierProxyIfNeeded(Boolean createProxy, Class iface, Supplier supplier) { + private T createSupplierProxyIfNeeded( + Boolean createProxy, Class iface, Supplier> supplier, TypedInstances typedInstances) { if (createProxy != null && createProxy && iface.isInterface()) { T proxy = (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[]{iface}, new InvocationHandler() { - final SingleRegisterSupplier singleSupplierRegister = new SingleRegisterSupplier<>(supplier); + final Set instances = new HashSet<>(); + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - T t = singleSupplierRegister.get(); + Supplier supplierT = supplier.get(); + T t = supplierT.get(); + if (DisposableSupplier.class.isInstance(supplierT) && !instances.contains(t)) { + MultivaluedMap map = typedInstances.disposableSupplierObjects.get(); + map.add((DisposableSupplier) supplierT, t); + } Object ret = method.invoke(t, args); return ret; } }); return proxy; } else { - return registerDisposableSupplierAndGet(supplier); + return registerDisposableSupplierAndGet(supplier.get(), typedInstances); } } @@ -592,8 +656,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl private class SingleRegisterSupplier { private final LazyValue once; - private SingleRegisterSupplier(Supplier supplier) { - once = Values.lazy((Value) () -> registerDisposableSupplierAndGet(supplier)); + private SingleRegisterSupplier(Supplier supplier, TypedInstances instances) { + once = Values.lazy((Value) () -> registerDisposableSupplierAndGet(supplier, instances)); } T get() { @@ -601,10 +665,10 @@ T get() { } } - private T registerDisposableSupplierAndGet(Supplier supplier) { + private T registerDisposableSupplierAndGet(Supplier supplier, TypedInstances typedInstances) { T instance = supplier.get(); if (DisposableSupplier.class.isInstance(supplier)) { - disposableSupplierObjects.add((DisposableSupplier) supplier, instance); + typedInstances.disposableSupplierObjects.get().add((DisposableSupplier) supplier, instance); } return instance; } @@ -678,7 +742,7 @@ private TypeBindings typeBindings(Type type) { * @param The expected return type for the TYPE. * @param The Type for which a {@link Binding} has been created. */ - private abstract class XBindings { + private abstract class XBindings { protected final List> instanceBindings = new LinkedList<>(); protected final List> supplierInstanceBindings = new LinkedList<>(); @@ -759,12 +823,8 @@ private X _getInstance(InstanceBinding instanceBinding) { private X _create(SupplierInstanceBinding binding) { Supplier supplier = binding.getSupplier(); - X t = registerDisposableSupplierAndGet(supplier); - if (Singleton.class.equals(binding.getScope())) { - _addInstance(t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(t, binding); - } + X t = registerDisposableSupplierAndGet(supplier, instances); + t = addInstance(type, t, binding); return t; } @@ -828,20 +888,17 @@ List allInstances() { protected abstract X _createAndStore(ClassBinding binding); - protected T _addInstance(TYPE type, T instance, Binding binding) { - return instances.addSingleton(type, instance, binding, instancesQualifiers); - } - - protected void _addThreadInstance(TYPE type, Object instance, Binding binding) { - instances.addThreadInstance(type, instance, binding, instancesQualifiers); - } - - protected T _addInstance(T instance, Binding binding) { + protected T _addSingletonInstance(TYPE type, T instance, Binding binding) { return instances.addSingleton(type, instance, binding, instancesQualifiers); } - protected void _addThreadInstance(Object instance, Binding binding) { - instances.addThreadInstance(type, instance, binding, instancesQualifiers); + protected T addInstance(TYPE type, T instance, Binding binding) { + if (Singleton.class.equals(binding.getScope())) { + instance = instances.addSingleton(type, instance, binding, instancesQualifiers); + } else if (_isPerThread(binding.getScope())) { + instances.addThreadInstance(type, instance, binding, instancesQualifiers); + } + return instance; } } @@ -901,28 +958,27 @@ private ServiceHolderImpl _serviceHolder(ClassBinding binding) { } protected T _create(SupplierClassBinding binding) { - Supplier supplier = instances.getInstance(binding.getSupplierClass(), null); - if (supplier == null) { - supplier = justCreate(binding.getSupplierClass()); - if (Singleton.class.equals(binding.getSupplierScope())) { - supplier = instances.addSingleton(binding.getSupplierClass(), supplier, binding, null); - } else if (_isPerThread(binding.getSupplierScope())) { - instances.addThreadInstance(binding.getSupplierClass(), supplier, binding, null); + Supplier> supplierSupplier = () -> { + Supplier supplier = instances.getInstance(binding.getSupplierClass(), null); + if (supplier == null) { + supplier = justCreate(binding.getSupplierClass()); + if (Singleton.class.equals(binding.getSupplierScope())) { + supplier = instances.addSingleton(binding.getSupplierClass(), supplier, binding, null); + } else if (_isPerThread(binding.getSupplierScope()) || binding.getSupplierScope() == null) { + instances.addThreadInstance(binding.getSupplierClass(), supplier, binding, null); + } } - } + return supplier; + }; - T t = createSupplierProxyIfNeeded(binding.isProxiable(), (Class) type, supplier); - if (Singleton.class.equals(binding.getScope())) { - t = _addInstance(type, t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(type, t, binding); - } + T t = createSupplierProxyIfNeeded(binding.isProxiable(), (Class) type, supplierSupplier, instances); +// t = addInstance(type, t, binding); The supplier here creates instances that ought not to be registered as beans return t; } protected T _createAndStore(ClassBinding binding) { T result = justCreate(binding.getService()); - result = _addInstance(binding.getService(), result, binding); + result = addInstance(binding.getService(), result, binding); return result; } } @@ -935,19 +991,15 @@ private TypeBindings(Type type) { protected T _create(SupplierClassBinding binding) { Supplier supplier = justCreate(binding.getSupplierClass()); - T t = registerDisposableSupplierAndGet(supplier); - if (Singleton.class.equals(binding.getScope())) { - t = _addInstance(type, t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(type, t, binding); - } + T t = registerDisposableSupplierAndGet(supplier, instances); + t = addInstance(type, t, binding); return t; } @Override protected T _createAndStore(ClassBinding binding) { T result = justCreate(binding.getService()); - result = _addInstance(type, result, binding); + result = addInstance(type, result, binding); return result; } @@ -968,7 +1020,7 @@ public Object get() { return NonInjectionManager.this.getInstance(actualTypeArgument); } } - }); + }, instances); @Override public Object get() { @@ -991,13 +1043,16 @@ private static class InstanceContext { private final T instance; private final Binding binding; private final Annotation[] createdWithQualifiers; + private boolean destroyed = false; - private InstanceContext(T instance, Binding binding, Annotation[] qualifiers) { + private InstanceContext(T instance, Binding binding, Annotation[] qualifiers, boolean destroyed) { this.instance = instance; this.binding = binding; this.createdWithQualifiers = qualifiers; + this.destroyed = destroyed; } + public Binding getBinding() { return binding; } @@ -1006,6 +1061,13 @@ public T getInstance() { return instance; } + public void destroy(NonInjectionManager nonInjectionManager) { + if (!destroyed) { + destroyed = true; + nonInjectionManager.preDestroy(instance); + } + } + @SuppressWarnings("unchecked") static List toInstances(List> instances, Annotation[] qualifiers) { return instances != null @@ -1024,6 +1086,15 @@ private static List> filterInstances(List> : null; } + private static List> merge(List> i1, List> i2) { + if (i1 == null) { + i1 = i2; + } else if (i2 != null) { + i1.addAll(i2); + } + return i1; + } + private boolean hasQualifiers(Annotation[] requested) { if (requested != null) { classLoop: diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java index 258780d53a..b8b23b2409 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024 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 @@ -20,14 +20,20 @@ import org.glassfish.jersey.internal.util.LazyUid; import org.glassfish.jersey.process.internal.RequestScope; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; public class NonInjectionRequestScope extends RequestScope { + + private final NonInjectionManager nonInjectionManager; + + public NonInjectionRequestScope(NonInjectionManager nonInjectionManager) { + this.nonInjectionManager = nonInjectionManager; + } + @Override public org.glassfish.jersey.process.internal.RequestContext createContext() { - return new Instance(); + return new Instance(nonInjectionManager); } /** @@ -35,6 +41,8 @@ public org.glassfish.jersey.process.internal.RequestContext createContext() { */ public static final class Instance implements org.glassfish.jersey.process.internal.RequestContext { + private final NonInjectionManager injectionManager; + private static final ExtendedLogger logger = new ExtendedLogger(Logger.getLogger(Instance.class.getName()), Level.FINEST); /* @@ -48,10 +56,11 @@ public static final class Instance implements org.glassfish.jersey.process.inter /** * Holds the number of snapshots of this scope. */ - private final AtomicInteger referenceCounter; + private int referenceCounter; - private Instance() { - this.referenceCounter = new AtomicInteger(1); + private Instance(NonInjectionManager injectionManager) { + this.injectionManager = injectionManager; + this.referenceCounter = 1; } /** @@ -65,7 +74,7 @@ private Instance() { @Override public NonInjectionRequestScope.Instance getReference() { // TODO: replace counter with a phantom reference + reference queue-based solution - referenceCounter.incrementAndGet(); + referenceCounter++; return this; } @@ -77,7 +86,9 @@ public NonInjectionRequestScope.Instance getReference() { */ @Override public void release() { - referenceCounter.decrementAndGet(); + if (0 == --referenceCounter) { + injectionManager.disposeRequestScopedInstances(); + } } @Override diff --git a/tests/e2e-inject/non-inject/pom.xml b/tests/e2e-inject/non-inject/pom.xml new file mode 100644 index 0000000000..0e53884884 --- /dev/null +++ b/tests/e2e-inject/non-inject/pom.xml @@ -0,0 +1,57 @@ + + + + + + e2e-inject + org.glassfish.jersey.tests + 2.46-SNAPSHOT + + 4.0.0 + + e2e-inject-noninject + + + + org.glassfish.jersey.incubator + jersey-injectless-client + 2.46-SNAPSHOT + + + org.glassfish.jersey.core + jersey-client + test + + + + org.junit.jupiter + junit-jupiter + test + + + org.hamcrest + hamcrest + test + + + + + diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java new file mode 100644 index 0000000000..6ccaa40d04 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 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.inject.noninject; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.DisposableSupplier; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import javax.inject.Inject; +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.core.Response; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +public class DisposableSuplierTest { + private static final AtomicInteger disposeCounter = new AtomicInteger(0); + private static final String HOST = "http://somewhere.anywhere"; + + public interface ResponseObject { + Response getResponse(); + } + + private static class TestDisposableSupplier implements DisposableSupplier { + AtomicInteger counter = new AtomicInteger(300); + + @Override + public void dispose(ResponseObject instance) { + disposeCounter.incrementAndGet(); + } + + @Override + public ResponseObject get() { + return new ResponseObject() { + + @Override + public Response getResponse() { + return Response.ok().build(); + } + }; + } + } + + private static class DisposableSupplierInjectingFilter implements ClientRequestFilter { + private final ResponseObject responseSupplier; + + @Inject + private DisposableSupplierInjectingFilter(ResponseObject responseSupplier) { + this.responseSupplier = responseSupplier; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(responseSupplier.getResponse()); + } + } + + @Test + public void testDisposeCount() { + disposeCounter.set(0); + int CNT = 4; + Client client = ClientBuilder.newClient() + .register(new AbstractBinder() { + @Override + protected void configure() { + bindFactory(TestDisposableSupplier.class).to(ResponseObject.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + } + }).register(DisposableSupplierInjectingFilter.class); + + for (int i = 0; i != CNT; i++) { + try (Response response = client.target(HOST).request().get()) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + } + } + + MatcherAssert.assertThat(disposeCounter.get(), Matchers.is(CNT)); + } +} diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java new file mode 100644 index 0000000000..e526007de6 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2024 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.inject.noninject; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.innate.inject.NonInjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import javax.inject.Inject; +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.Entity; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class InstanceListSizeTest { + + private static final String TEST_HEADER = "TEST"; + private static final String HOST = "https://anywhere.any"; + + @Test + public void leakTest() throws ExecutionException, InterruptedException { + int CNT = 100; + Client client = ClientBuilder.newClient(); + client.register(InjectionManagerGrowChecker.class); + Response response = client.target(HOST).request().header(TEST_HEADER, "0").get(); + int status = response.getStatus(); + response.close(); + //Create instance in NonInjectionManager$TypedInstances.threadPredestroyables + + for (int i = 0; i <= CNT; i++) { + final String header = String.valueOf(i + 1); + try (Response r = client.target(HOST).request() + .header(TEST_HEADER, header) + .async() + .post(Entity.text("text")).get()) { + int stat = r.getStatus(); + MatcherAssert.assertThat( + "NonInjectionManager#Types#disposableSupplierObjects is increasing", stat, Matchers.is(202)); + } + } + //Create 10 instance in NonInjectionManager$TypedInstances.threadPredestroyables + + for (int i = 0; i <= CNT; i++) { + final String header = String.valueOf(i + CNT + 2); + final Object text = CompletableFuture.supplyAsync(() -> { + Response test = client.target(HOST).request() + .header("TEST", header) + .post(Entity.text("text")); + int stat = test.getStatus(); + test.close(); + MatcherAssert.assertThat( + "NonInjectionManager#Types#disposableSupplierObjects is increasing", stat, Matchers.is(202)); + + return null; + }).join(); + } + //Create 10 instance in NonInjectionManager$TypedInstances.threadPredestroyables + + response = client.target(HOST).request().header(TEST_HEADER, 2 * CNT + 3).get(); + status = response.getStatus(); + MatcherAssert.assertThat(status, Matchers.is(202)); + response.close(); + } + + private static class InjectionManagerGrowChecker implements ClientRequestFilter { + private boolean first = true; + private int disposableSize = 0; + private int threadInstancesSize = 0; + private HttpHeaders headers; + private int headerCnt = 0; + + @Inject + public InjectionManagerGrowChecker(HttpHeaders headers) { + this.headers = headers; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Response.Status status = Response.Status.ACCEPTED; + if (headerCnt++ != Integer.parseInt(headers.getHeaderString("TEST"))) { + status = Response.Status.BAD_REQUEST; + } + + NonInjectionManager nonInjectionManager = getInjectionManager(requestContext); + Object types = getDeclaredField(nonInjectionManager, "types"); + Object instances = getDeclaredField(nonInjectionManager, "instances"); + if (first) { + first = false; + disposableSize = getThreadInstances(types, "disposableSupplierObjects") + + getThreadInstances(instances, "disposableSupplierObjects"); + threadInstancesSize = getThreadInstances(types, "threadInstances") + + getThreadInstances(instances, "threadInstances"); + } else { + int newPredestroyableSize = getThreadInstances(types, "disposableSupplierObjects") + + getThreadInstances(instances, "disposableSupplierObjects"); + if (newPredestroyableSize > disposableSize + 1 /* a new service to get disposed */) { + status = Response.Status.EXPECTATION_FAILED; + } + int newThreadInstances = getThreadInstances(types, "threadInstances") + + getThreadInstances(instances, "threadInstances"); + if (newThreadInstances > threadInstancesSize) { + status = Response.Status.PRECONDITION_FAILED; + } + } + + requestContext.abortWith(Response.status(status).build()); + } + } + + private static NonInjectionManager getInjectionManager(ClientRequestContext context) { + ClientRequest request = ((ClientRequest) context); + try { + Method clientConfigMethod = ClientRequest.class.getDeclaredMethod("getClientConfig"); + clientConfigMethod.setAccessible(true); + ClientConfig clientConfig = (ClientConfig) clientConfigMethod.invoke(request); + + Method runtimeMethod = ClientConfig.class.getDeclaredMethod("getRuntime"); + runtimeMethod.setAccessible(true); + Object clientRuntime = runtimeMethod.invoke(clientConfig); + Class clientRuntimeClass = clientRuntime.getClass(); + + Method injectionManagerMethod = clientRuntimeClass.getDeclaredMethod("getInjectionManager"); + injectionManagerMethod.setAccessible(true); + InjectionManager injectionManager = (InjectionManager) injectionManagerMethod.invoke(clientRuntime); + return (NonInjectionManager) injectionManager; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Object getDeclaredField(NonInjectionManager nonInjectionManager, String name) { + try { + Field typesField = NonInjectionManager.class.getDeclaredField(name); + typesField.setAccessible(true); + return typesField.get(nonInjectionManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static int getThreadInstances(Object typedInstances, String threadLocalName) { + try { + Field threadLocalField = + typedInstances.getClass().getSuperclass().getDeclaredField(threadLocalName); + threadLocalField.setAccessible(true); + ThreadLocal> threadLocal = + (ThreadLocal>) threadLocalField.get(typedInstances); + MultivaluedMap map = threadLocal.get(); + if (map == null) { + return 0; + } else { + int cnt = 0; + Set>> set = map.entrySet(); + for (Map.Entry> entry : map.entrySet()) { + cnt += entry.getValue().size(); + } + return cnt; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java new file mode 100644 index 0000000000..752fd243c2 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 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.inject.noninject; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.DisposableSupplier; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; +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.core.Response; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +public class PreDestroyTest { + private static final AtomicInteger disposeCounter = new AtomicInteger(0); + private static final String HOST = "http://somewhere.anywhere"; + + public interface ResponseObject { + Response getResponse(); + } + + public static class ResponseObjectImpl implements ResponseObject { + + public ResponseObjectImpl() { + + } + + @PreDestroy + public void preDestroy() { + disposeCounter.incrementAndGet(); + } + + @Override + public Response getResponse() { + return Response.ok().build(); + } + } + + private static class PreDestroyInjectingFilter implements ClientRequestFilter { + private final ResponseObject responseSupplier; + + @Inject + private PreDestroyInjectingFilter(ResponseObject responseSupplier) { + this.responseSupplier = responseSupplier; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(responseSupplier.getResponse()); + } + } + + @Test + public void testPreDestroyCount() { + disposeCounter.set(0); + int CNT = 4; + Client client = ClientBuilder.newClient() + .register(new AbstractBinder() { + @Override + protected void configure() { + bind(ResponseObjectImpl.class).to(ResponseObject.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + } + }).register(PreDestroyInjectingFilter.class); + + for (int i = 0; i != CNT; i++) { + try (Response response = client.target(HOST).request().get()) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + } + } + + MatcherAssert.assertThat(disposeCounter.get(), Matchers.is(1)); + } +} diff --git a/tests/e2e-inject/pom.xml b/tests/e2e-inject/pom.xml index 81acfcbcd2..8c67861390 100644 --- a/tests/e2e-inject/pom.xml +++ b/tests/e2e-inject/pom.xml @@ -36,5 +36,6 @@ cdi2-se cdi-inject-weld hk2 + non-inject