diff --git a/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ContextManagers.java b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ContextManagers.java index fed5219d..e47e56b6 100644 --- a/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ContextManagers.java +++ b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ContextManagers.java @@ -19,13 +19,9 @@ import nl.talsmasoftware.context.api.ContextManager; import nl.talsmasoftware.context.api.ContextSnapshot; import nl.talsmasoftware.context.api.ContextSnapshot.Reactivation; -import nl.talsmasoftware.context.api.ContextTimer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; -import java.util.ServiceLoader; +import java.util.ListIterator; import java.util.logging.Level; import java.util.logging.Logger; @@ -43,15 +39,6 @@ public final class ContextManagers { private static final Logger LOGGER = Logger.getLogger(ContextManagers.class.getName()); - /** - * Sometimes a single, fixed classloader may be necessary (e.g. #97) - */ - private static volatile ClassLoader classLoaderOverride = null; - - private static volatile List> contextManagers = null; - - private static volatile List contextTimers = null; - /** * Private constructor to avoid instantiation of this class. */ @@ -72,32 +59,26 @@ private ContextManagers() { * @return A new snapshot that can be reactivated elsewhere (e.g. a background thread) * within a try-with-resources construct. */ + @SuppressWarnings("rawtypes") public static nl.talsmasoftware.context.api.ContextSnapshot createContextSnapshot() { final long start = System.nanoTime(); - final List> managers = new LinkedList<>(); - final List values = new LinkedList<>(); - Long managerStart = null; - for (ContextManager manager : getContextManagers()) { - managerStart = System.nanoTime(); + final List managers = ServiceCache.cached(ContextManager.class); + final Object[] values = new Object[managers.size()]; + + for (ListIterator it = managers.listIterator(); it.hasNext(); ) { + final ContextManager manager = it.next(); + long managerStart = System.nanoTime(); try { - final Object activeContextValue = manager.getActiveContextValue(); - if (activeContextValue != null) { - values.add(activeContextValue); - managers.add(manager); - if (LOGGER.isLoggable(Level.FINEST)) { - LOGGER.finest("Active context value of " + manager + " added to new snapshot: " + activeContextValue); - } - Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext"); - } else if (LOGGER.isLoggable(Level.FINEST)) { - LOGGER.log(Level.FINEST, "There is no active context for " + manager + " in this snapshot."); - } + values[it.previousIndex()] = getActiveContextValue(manager); + Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext"); } catch (RuntimeException rte) { - LOGGER.log(Level.WARNING, "Exception obtaining active context from " + manager + " for snapshot.", rte); + LOGGER.log(Level.WARNING, "Error obtaining active context from " + manager + " (in thread " + Thread.currentThread().getName() + ").", rte); Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext.exception"); } } + final ContextSnapshotImpl result = new ContextSnapshotImpl(managers, values); - if (managerStart == null && LOGGER.isLoggable(Level.FINER)) { + if (managers.isEmpty() && LOGGER.isLoggable(Level.FINER)) { LOGGER.finer(result + " was created but no ContextManagers were found! " + " Thead=" + Thread.currentThread() + ", ContextClassLoader=" + Thread.currentThread().getContextClassLoader()); @@ -126,17 +107,14 @@ public static nl.talsmasoftware.context.api.ContextSnapshot createContextSnapsho public static void clearActiveContexts() { final long start = System.nanoTime(); Long managerStart = null; - for (ContextManager manager : getContextManagers()) { + for (ContextManager manager : ServiceCache.cached(ContextManager.class)) { managerStart = System.nanoTime(); try { - manager.clear(); - if (LOGGER.isLoggable(Level.FINEST)) { - LOGGER.finest("Active context of " + manager + " was cleared."); - } + clear(manager); Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear"); } catch (RuntimeException rte) { - LOGGER.log(Level.WARNING, "Exception clearing active context from " + manager + ".", rte); - contextManagers = null; + LOGGER.log(Level.WARNING, "Error clearing active context from " + manager + ".", rte); + ServiceCache.clear(); Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear.exception"); } } @@ -170,48 +148,20 @@ public static void clearActiveContexts() { * @since 1.0.5 */ public static synchronized void useClassLoader(ClassLoader classLoader) { - if (classLoaderOverride == classLoader) { - LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)"); - return; - } - LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")"); - classLoaderOverride = classLoader; - contextManagers = null; - contextTimers = null; - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private static List> getContextManagers() { - if (contextManagers == null) { - synchronized (ContextManagers.class) { - if (contextManagers == null) { - contextManagers = (List) load(ContextManager.class); - } - } - } - return contextManagers; + ServiceCache.useClassLoader(classLoader); } - static List getContextTimers() { - if (contextTimers == null) { - synchronized (ContextManagers.class) { - if (contextTimers == null) { - contextTimers = load(ContextTimer.class); - } - } - } - return contextTimers; + private static Object getActiveContextValue(ContextManager manager) { + final Object activeContextValue = manager.getActiveContextValue(); + LOGGER.finest(() -> activeContextValue == null + ? "There is no active context value for " + manager + " (in thread " + Thread.currentThread().getName() + ")." + : "Active context value of " + manager + " in " + Thread.currentThread().getName() + ": " + activeContextValue); + return activeContextValue; } - private static List load(Class type) { - ArrayList list = new ArrayList<>(); - if (classLoaderOverride == null) { - ServiceLoader.load(type).forEach(list::add); - } else { - ServiceLoader.load(type, classLoaderOverride).forEach(list::add); - } - list.trimToSize(); - return Collections.unmodifiableList(list); + private static void clear(ContextManager manager) { + manager.clear(); + LOGGER.finest(() -> "Active context of " + manager + " was cleared."); } /** @@ -219,22 +169,23 @@ private static List load(Class type) { * snapshot in each corresponding {@link ContextManager}. */ @SuppressWarnings("rawtypes") - private static final class ContextSnapshotImpl implements nl.talsmasoftware.context.api.ContextSnapshot { - private static final ContextManager[] MANAGER_ARRAY = new ContextManager[0]; - private final ContextManager[] managers; + private static final class ContextSnapshotImpl implements ContextSnapshot { + private final List managers; private final Object[] values; - private ContextSnapshotImpl(List> managers, List values) { - this.managers = managers.toArray(MANAGER_ARRAY); - this.values = values.toArray(); + private ContextSnapshotImpl(List managers, Object[] values) { + this.managers = managers; + this.values = values; } public Reactivation reactivate() { final long start = System.nanoTime(); - final List> reactivatedContexts = new ArrayList>(managers.length); + final Context[] reactivatedContexts = new Context[managers.size()]; try { - for (int i = 0; i < managers.length && i < values.length; i++) { - reactivatedContexts.add(reactivate(managers[i], values[i])); + for (ListIterator it = managers.listIterator(); it.hasNext(); ) { + final ContextManager manager = it.next(); + final Object value = values[it.previousIndex()]; + reactivatedContexts[it.previousIndex()] = value != null ? reactivate(manager, value) : null; } ReactivationImpl reactivation = new ReactivationImpl(reactivatedContexts); Timers.timed(System.nanoTime() - start, nl.talsmasoftware.context.api.ContextSnapshot.class, "reactivate"); @@ -251,7 +202,7 @@ public Reactivation reactivate() { reactivationException.addSuppressed(rte); } } - contextManagers = null; + ServiceCache.clear(); throw reactivationException; } } @@ -269,7 +220,7 @@ private Context reactivate(ContextManager contextManager, Object snapshotValue) @Override public String toString() { - return "ContextSnapshot{size=" + managers.length + '}'; + return "ContextSnapshot{size=" + managers.size() + '}'; } } @@ -278,18 +229,19 @@ public String toString() { * when it is closed itself.
* This context contains no meaningful value in itself and purely exists to close the reactivated contexts. */ + @SuppressWarnings("rawtypes") private static final class ReactivationImpl implements Reactivation { - private final List> reactivated; + private final Context[] reactivated; - private ReactivationImpl(List> reactivated) { + private ReactivationImpl(Context[] reactivated) { this.reactivated = reactivated; } public void close() { RuntimeException closeException = null; // close in reverse order of reactivation - for (int i = this.reactivated.size() - 1; i >= 0; i--) { - Context reactivated = this.reactivated.get(i); + for (int i = this.reactivated.length - 1; i >= 0; i--) { + Context reactivated = this.reactivated[i]; if (reactivated != null) try { reactivated.close(); } catch (RuntimeException rte) { @@ -302,7 +254,7 @@ public void close() { @Override public String toString() { - return "ContextSnapshot.Reactivation{size=" + reactivated.size() + '}'; + return "ContextSnapshot.Reactivation{size=" + reactivated.length + '}'; } } } diff --git a/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ServiceCache.java b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ServiceCache.java new file mode 100644 index 00000000..18d8122d --- /dev/null +++ b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/ServiceCache.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2024 Talsma ICT + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package nl.talsmasoftware.context.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; + +/** + * Cache for resolved services. + * + *

+ * This is required because the ServiceLoader itself is not thread-safe due to its internal lazy iterator. + */ +class ServiceCache { + private static final Logger LOGGER = Logger.getLogger(ServiceCache.class.getName()); + + /** + * Internal concurrent map as cache. + */ + @SuppressWarnings("rawtypes") + private static final ConcurrentMap CACHE = new ConcurrentHashMap<>(); + + /** + * Sometimes a single, fixed classloader may be necessary (e.g. #97) + */ + private static volatile ClassLoader classLoaderOverride = null; + + static synchronized void useClassLoader(ClassLoader classLoader) { + if (classLoaderOverride == classLoader) { + LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)"); + return; + } + LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")"); + classLoaderOverride = classLoader; + CACHE.clear(); + } + + @SuppressWarnings("unchecked") + static List cached(Class serviceClass) { + return (List) CACHE.computeIfAbsent(serviceClass, ServiceCache::load); + } + + static void clear() { + CACHE.clear(); + } + + /** + * Loads the service implementations of the requested type. + * + *

+ * This method is synchronized because ServiceLoader is not thread-safe. + * Fortunately this only gets called after a cache miss, so should not affect performance. + * + *

+ * The returned {@code List} will be {@linkplain Collections#unmodifiableList(List) unmodifiable}. + * + * @param serviceType The service type to load. + * @param The service type to load. + * @return Unmodifiable list of service implementations. + */ + private synchronized static List load(Class serviceType) { + final ArrayList services = new ArrayList<>(); + final ServiceLoader loader = classLoaderOverride == null + ? ServiceLoader.load(serviceType) + : ServiceLoader.load(serviceType, classLoaderOverride); + for (T service : loader) { + services.add(service); + } + services.trimToSize(); + // TODO debug logging + return Collections.unmodifiableList(services); + } +} diff --git a/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/Timers.java b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/Timers.java index 4b4da233..32c58760 100644 --- a/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/Timers.java +++ b/context-propagation-core/src/main/java/nl/talsmasoftware/context/core/Timers.java @@ -25,7 +25,7 @@ final class Timers { private static final Logger TIMING_LOGGER = Logger.getLogger(Timers.class.getName()); static void timed(long durationNanos, Class type, String method) { - for (ContextTimer delegate : ContextManagers.getContextTimers()) { + for (ContextTimer delegate : ServiceCache.cached(ContextTimer.class)) { delegate.update(type, method, durationNanos, TimeUnit.NANOSECONDS); } if (TIMING_LOGGER.isLoggable(Level.FINEST)) { diff --git a/context-propagation-core/src/test/java/nl/talsmasoftware/context/core/ContextSnapshotTest.java b/context-propagation-core/src/test/java/nl/talsmasoftware/context/core/ContextSnapshotTest.java index 865ca7d6..637c522f 100644 --- a/context-propagation-core/src/test/java/nl/talsmasoftware/context/core/ContextSnapshotTest.java +++ b/context-propagation-core/src/test/java/nl/talsmasoftware/context/core/ContextSnapshotTest.java @@ -20,7 +20,6 @@ import nl.talsmasoftware.context.dummy.DummyContextManager; import org.junit.jupiter.api.Test; -import java.io.Closeable; import java.io.IOException; import static org.hamcrest.MatcherAssert.assertThat; @@ -36,9 +35,7 @@ public class ContextSnapshotTest { @Test public void testSnapshotToString() { - try (Context ctx = MGR.initializeNewContext("Dummy value")) { - assertThat(ContextManagers.createContextSnapshot(), hasToString(startsWith("ContextSnapshot{size="))); - } + assertThat(ContextManagers.createContextSnapshot(), hasToString(startsWith("ContextSnapshot{size="))); } @Test @@ -48,9 +45,9 @@ public void testSnapshotReactivate() throws IOException { try (Context ctx2 = MGR.initializeNewContext("New value")) { assertThat(MGR.getActiveContextValue(), is("New value")); - try (Closeable reactivation = snapshot.reactivate()) { + try (ContextSnapshot.Reactivation reactivation = snapshot.reactivate()) { assertThat(MGR.getActiveContextValue(), is("Old value")); - assertThat(reactivation, hasToString(startsWith("ReactivatedContext{size="))); + assertThat(reactivation, hasToString(startsWith("ContextSnapshot.Reactivation{size="))); } assertThat(MGR.getActiveContextValue(), is("New value"));