From 771665e2863c6f0ea6379453e2c1cb8debf70942 Mon Sep 17 00:00:00 2001 From: Oran Epelbaum Date: Sat, 18 Oct 2025 15:32:59 +0100 Subject: [PATCH] Fix ExtendedEnum ClassNotFoundException in ForkJoinPool (#2748) Replace RenameHandler.lookupType() with Class.forName() using explicit classloader in ExtendedEnum and CombinedExtendedEnum. RenameHandler relies on thread context classloader which is null in ForkJoinPool workers, causing ClassNotFoundException in production environments with isolated classloader hierarchies (application servers, OSGi). The fix uses ExtendedEnum.class.getClassLoader() instead of TCCL, ensuring classes are loaded from the correct classloader regardless of threading context. Testing: - ExtendedEnumThreadSafetyTest: Verifies thread-safety patterns in various concurrent contexts (ForkJoinPool, parallel streams, CompletableFuture). This test is comprehensive and valuable, but it does not reproduce the bug, due to how classloading works under Surefire. - ExtendedEnumClassLoaderIsolationTest: Reproduces the production bug by running in an isolated parent/child classloader hierarchy with null TCCL, executed automatically during Maven verify phase, outside of the regular Maven/Surefire context. --- modules/collect/pom.xml | 26 ++ .../collect/named/CombinedExtendedEnum.java | 2 +- .../strata/collect/named/ExtendedEnum.java | 2 +- .../ExtendedEnumClassLoaderIsolationTest.java | 200 ++++++++++ .../named/ExtendedEnumThreadSafetyTest.java | 343 ++++++++++++++++++ .../collect/named/ThreadMoreSampleNameds.java | 23 ++ .../collect/named/ThreadSafeSampleNamed.java | 27 ++ .../collect/named/ThreadSafeSampleNameds.java | 24 ++ .../config/base/ThreadSafeSampleNamed.ini | 9 + 9 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java create mode 100644 modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java create mode 100644 modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java create mode 100644 modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java create mode 100644 modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java create mode 100644 modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini diff --git a/modules/collect/pom.xml b/modules/collect/pom.xml index 1847a79f00..0b9d8f16e5 100644 --- a/modules/collect/pom.xml +++ b/modules/collect/pom.xml @@ -99,6 +99,32 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + verify-classloader-fix + verify + + exec + + + java + test + + -classpath + + com.opengamma.strata.collect.named.ExtendedEnumClassLoaderIsolationTest + ${project.build.directory}/${project.build.finalName}.jar + ${project.basedir}/../basics/target/strata-basics-${project.version}.jar + ${project.basedir}/../data/target/strata-data-${project.version}.jar + + + + + diff --git a/modules/collect/src/main/java/com/opengamma/strata/collect/named/CombinedExtendedEnum.java b/modules/collect/src/main/java/com/opengamma/strata/collect/named/CombinedExtendedEnum.java index 3cad81029a..6cf4478cf3 100644 --- a/modules/collect/src/main/java/com/opengamma/strata/collect/named/CombinedExtendedEnum.java +++ b/modules/collect/src/main/java/com/opengamma/strata/collect/named/CombinedExtendedEnum.java @@ -94,7 +94,7 @@ private static ImmutableList> parseC for (String key : section.keys()) { Class cls; try { - cls = RenameHandler.INSTANCE.lookupType(key); + cls = Class.forName(key, true, CombinedExtendedEnum.class.getClassLoader()); } catch (Exception ex) { throw new IllegalArgumentException("Unable to find extended enum class: " + key, ex); } diff --git a/modules/collect/src/main/java/com/opengamma/strata/collect/named/ExtendedEnum.java b/modules/collect/src/main/java/com/opengamma/strata/collect/named/ExtendedEnum.java index 9e9ae2c69b..a2dc7c282a 100644 --- a/modules/collect/src/main/java/com/opengamma/strata/collect/named/ExtendedEnum.java +++ b/modules/collect/src/main/java/com/opengamma/strata/collect/named/ExtendedEnum.java @@ -171,7 +171,7 @@ private static ImmutableList> parseProviders( for (String key : section.keys()) { Class cls; try { - cls = RenameHandler.INSTANCE.lookupType(key); + cls = Class.forName(key, true, ExtendedEnum.class.getClassLoader()); } catch (Exception ex) { throw new IllegalArgumentException("Unable to find enum provider class: " + key, ex); } diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java new file mode 100644 index 0000000000..886339554e --- /dev/null +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumClassLoaderIsolationTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.collect.named; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; + +/** + * Test that reproduces ExtendedEnum failures in isolated classloader hierarchies. + *

+ * This program recreates the classloader structure found in application servers, + * OSGi containers, and multi-module applications where libraries are separated + * into parent (common/shared) and child (application-specific) classloaders: + *

+ * Parent ClassLoader (Common/Shared Libraries) + *

    + *
  • Joda-Convert (includes RenameHandler)
  • + *
  • Joda-Beans
  • + *
  • Guava
  • + *
+ *

+ * Child ClassLoader (Application Libraries) + *

    + *
  • Strata modules (ExtendedEnum, StandardDayCounts, etc.)
  • + *
  • Delegates to parent for common libraries
  • + *
+ *

+ * ForkJoinPool Worker Threads - Thread Context ClassLoader (TCCL) = null + *

+ * When ExtendedEnum.parseProviders() (loaded in child) calls RenameHandler.lookupType() + * (loaded in parent) to find StandardDayCounts: + *

    + *
  1. RenameHandler checks TCCL → null → unavailable
  2. + *
  3. RenameHandler uses its own classloader → parent
  4. + *
  5. Parent classloader cannot see child classes → ClassNotFoundException
  6. + *
+ *

+ * This reproduces the exact failure reported in production environments. + *

+ * Usage: + *

+ *   java ExtendedEnumClassLoaderIsolationTest <strata-module-jar> [<strata-module-jar> ...]
+ * 
+ *

+ * Arguments should be Strata module JARs to load in the child classloader (the code under test). + * Parent classloader dependencies (Joda-Convert, Guava, etc.) are automatically discovered from + * the local Maven repository to simulate production application server environments. + *

+ * This test is executed automatically during Maven's verify phase via exec-maven-plugin. + */ +public class ExtendedEnumClassLoaderIsolationTest { + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Usage: ExtendedEnumClassLoaderIsolationTest [ ...]"); + System.err.println(); + System.err.println("Arguments: Strata module JARs to load in CHILD classloader (the code under test)"); + System.err.println("Note: Parent classloader dependencies (Joda-Convert, Guava, etc.) are automatically"); + System.err.println(" discovered from the local Maven repository to simulate production environments."); + System.exit(1); + } + + System.out.println("ExtendedEnum ClassLoader Isolation Test"); + System.out.println("Testing ExtendedEnum initialization in isolated classloader hierarchy"); + + String m2Repo = System.getProperty("user.home") + "/.m2/repository"; + + // PARENT CLASSLOADER: Common/shared libraries (like in app server's lib/) + // This simulates the "common" classloader in Tomcat, or shared bundle in OSGi + List parentJars = new ArrayList<>(); + addJarsFromDir(parentJars, new File(m2Repo + "/org/joda/joda-convert")); + addJarsFromDir(parentJars, new File(m2Repo + "/org/joda/joda-beans")); + addJarsFromDir(parentJars, new File(m2Repo + "/com/google/guava")); + addJarsFromDir(parentJars, new File(m2Repo + "/com/google/code/findbugs")); + addJarsFromDir(parentJars, new File(m2Repo + "/com/google/errorprone")); + + System.out.println(" Parent classloader: " + parentJars.size() + " JARs (Joda-Convert, Joda-Beans, Guava)"); + + URLClassLoader parentCL = new URLClassLoader( + parentJars.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent()); + + // CHILD CLASSLOADER: Application classes (like in app server's webapps/) + // This simulates the "webapp" classloader in Tomcat, or application bundle in OSGi + List childJars = new ArrayList<>(); + for (String jarPath : args) { + addIfExists(childJars, jarPath); + } + + System.out.println(" Child classloader: " + childJars.size() + " JARs (Strata modules)"); + + URLClassLoader childCL = new URLClassLoader( + childJars.toArray(new URL[0]), parentCL); // Parent can see Joda-Convert, but child has Strata + + // Verify the hierarchy is correctly isolated + parentCL.loadClass("org.joda.convert.RenameHandler"); + childCL.loadClass("com.opengamma.strata.collect.named.ExtendedEnum"); + try { + parentCL.loadClass("com.opengamma.strata.basics.date.StandardDayCounts"); + throw new AssertionError("Parent classloader should not see StandardDayCounts"); + } catch (ClassNotFoundException ignored) { + // Expected - parent cannot see child classes + } + childCL.loadClass("com.opengamma.strata.basics.date.StandardDayCounts"); + System.out.println(" Classloader isolation verified"); + + ForkJoinPool pool = new ForkJoinPool( + 1, + poolArg -> { + ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(poolArg); + thread.setContextClassLoader(null); // NULL TCCL + return thread; + }, + null, + false); + + try { + Callable task = () -> { + try { + Class dayCountClass = childCL.loadClass("com.opengamma.strata.basics.date.DayCount"); + Method extendedEnumMethod = dayCountClass.getMethod("extendedEnum"); + extendedEnumMethod.invoke(null); + return null; + } catch (Exception e) { + // Extract root cause + Throwable cause = e; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + + if (cause instanceof ClassNotFoundException) { + throw new AssertionError( + "ClassNotFoundException in ForkJoinPool with isolated classloaders. " + + "This indicates issue #2748 has regressed. " + + "ExtendedEnum failed to load: " + cause.getMessage(), cause); + } + + throw e; + } + }; + + pool.submit(task).get(); + System.out.println(" ✓ ExtendedEnum initialization successful in ForkJoinPool with null TCCL"); + System.out.println(" ✓ Test passed"); + + } catch (Exception e) { + System.err.println("TEST FAILED:"); + e.printStackTrace(System.err); + System.exit(1); + } finally { + pool.shutdown(); + } + } + + private static void addIfExists(List urls, String path) { + File file = new File(path); + if (file.exists()) { + try { + urls.add(file.toURI().toURL()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static void addJarsFromDir(List urls, File dir) { + if (!dir.exists()) { + return; + } + addJarsRecursive(urls, dir); + } + + private static void addJarsRecursive(List urls, File dir) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + if (file.isDirectory()) { + addJarsRecursive(urls, file); + } else if (file.getName().endsWith(".jar")) { + try { + urls.add(file.toURI().toURL()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java new file mode 100644 index 0000000000..7b2734463c --- /dev/null +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ExtendedEnumThreadSafetyTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.collect.named; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +/** + * Test {@link ExtendedEnum} thread safety and initialization patterns in concurrent contexts. + *

+ * These tests verify that ExtendedEnum can be safely initialized from various threading + * constructs including ForkJoinPool, parallel streams, and CompletableFuture. This is + * particularly important in production environments where complex classloader hierarchies + * may cause initialization failures when the thread context classloader is unavailable. + *

+ * Important: These JUnit tests run within Maven's Surefire plugin, which uses a + * simplified classloader hierarchy where all dependencies (including Joda-Convert and + * Strata modules) are loaded by the same classloader. This means these tests cannot + * reproduce the production ClassNotFoundException reported in issue #2748. + *

+ * The actual bug reproduction requires a separate JVM process with an isolated parent/child + * classloader hierarchy. See {@link ExtendedEnumClassLoaderIsolationTest} which runs during + * the Maven verify phase to reproduce the issue. That test creates the exact classloader + * structure found in application servers (Tomcat, WebLogic) and OSGi containers where the + * bug occurs. + *

+ * These tests remain valuable for verifying thread-safety patterns and ensuring ExtendedEnum + * works correctly in various concurrent execution contexts. + *

+ * These tests use {@link ThreadSafeSampleNamed} to ensure clean initialization scenarios + * independent of other tests in the suite. + * + * @see Issue #2748 + */ +public class ExtendedEnumThreadSafetyTest { + + /** + * Test that ExtendedEnum can be initialized from within a ForkJoinPool. + *

+ * This test verifies the pattern that fails in production: when DayCounts (or any ExtendedEnum) + * is first accessed from a ForkJoinPool worker thread. However, this test cannot reproduce + * issue #2748 because Maven's classloader hierarchy doesn't match production environments. + * See the class-level javadoc for details on why {@link ExtendedEnumClassLoaderIsolationTest} + * is needed. + */ + @Test + public void test_initialization_in_forkjoin_pool() throws Exception { + ForkJoinPool pool = new ForkJoinPool(4); + try { + // Submit a task that accesses ExtendedEnum for the first time + ForkJoinTask> task = pool.submit( + () -> ExtendedEnum.of(ThreadSafeSampleNamed.class)); + + ExtendedEnum result = task.get(5, TimeUnit.SECONDS); + + // Verify the ExtendedEnum was properly initialized + assertThat(result).isNotNull(); + assertThat(result.lookupAll()).isNotEmpty(); + assertThat(result.lookup("ThreadStandard")).isNotNull(); + assertThat(result.lookup("ThreadStandard").getName()).isEqualTo("ThreadStandard"); + } finally { + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** + * Test that ExtendedEnum can be used in parallel streams. + *

+ * Parallel streams use ForkJoinPool.commonPool() which has the same classloader issue. + */ + @Test + public void test_initialization_in_parallel_stream() { + List names = Arrays.asList("ThreadStandard", "ThreadMore", "ThreadStandard"); + + // Use parallel stream which internally uses ForkJoinPool.commonPool() + List results = names.parallelStream() + .map(name -> ThreadSafeSampleNamed.extendedEnum().lookup(name)) + .collect(Collectors.toList()); + + assertThat(results).hasSize(3); + assertThat(results.get(0).getName()).isEqualTo("ThreadStandard"); + assertThat(results.get(1).getName()).isEqualTo("ThreadMore"); + assertThat(results.get(2).getName()).isEqualTo("ThreadStandard"); + } + + /** + * Test that ExtendedEnum can be initialized from CompletableFuture with ForkJoinPool. + *

+ * CompletableFuture.supplyAsync() by default uses ForkJoinPool.commonPool(). + */ + @Test + public void test_initialization_in_completable_future() throws Exception { + // CompletableFuture.supplyAsync() uses ForkJoinPool.commonPool() by default + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup("ThreadStandard"); + }); + + ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("ThreadStandard"); + } + + /** + * Test that ExtendedEnum can be initialized from CompletableFuture with explicit ForkJoinPool. + */ + @Test + public void test_initialization_in_completable_future_with_custom_pool() throws Exception { + ForkJoinPool pool = new ForkJoinPool(2); + try { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup("ThreadMore"); + }, pool); + + ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("ThreadMore"); + } finally { + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** + * Test concurrent access from multiple threads in ForkJoinPool. + *

+ * This tests that even under high concurrency, ExtendedEnum initialization + * works correctly when accessed from ForkJoinPool threads. + */ + @Test + public void test_concurrent_access_in_forkjoin_pool() throws Exception { + ForkJoinPool pool = new ForkJoinPool(10); + try { + List> tasks = new ArrayList<>(); + + // Create 100 concurrent tasks + for (int i = 0; i < 100; i++) { + String name = (i % 2 == 0) ? "ThreadStandard" : "ThreadMore"; + tasks.add(() -> ThreadSafeSampleNamed.extendedEnum().lookup(name)); + } + + // Submit all tasks and wait for completion + List> futures = new ArrayList<>(); + for (Callable task : tasks) { + futures.add(pool.submit(task)); + } + + // Verify all completed successfully + for (Future future : futures) { + ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS); + assertThat(result).isNotNull(); + assertThat(result.getName()).matches("Thread(Standard|More)"); + } + } finally { + pool.shutdown(); + pool.awaitTermination(10, TimeUnit.SECONDS); + } + } + + /** + * Test that ExtendedEnum initialization works in regular ExecutorService. + *

+ * This should work even before the fix, as regular ExecutorService threads + * typically have the correct context classloader. This test serves as a + * sanity check that the issue is specific to ForkJoinPool. + */ + @Test + public void test_initialization_in_executor_service() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + Future future = executor.submit(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup("ThreadStandard"); + }); + + ThreadSafeSampleNamed result = future.get(5, TimeUnit.SECONDS); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("ThreadStandard"); + } finally { + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** + * Test using parallel IntStream which also uses ForkJoinPool. + */ + @Test + public void test_initialization_in_parallel_int_stream() { + List results = IntStream.range(0, 50) + .parallel() + .mapToObj(i -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup(i % 2 == 0 ? "ThreadStandard" : "ThreadMore"); + }) + .collect(Collectors.toList()); + + assertThat(results).hasSize(50); + assertThat(results).allMatch(item -> item != null); + assertThat(results).allMatch(item -> + "ThreadStandard".equals(item.getName()) || "ThreadMore".equals(item.getName())); + } + + /** + * Test simulating the real-world usage pattern from the issue report. + *

+ * This simulates bond pricing calculations running in parallel via ForkJoinPool, + * where each calculation needs to access ExtendedEnum (e.g., DayCount). This is + * the exact usage pattern that triggered the ClassNotFoundException in production. + *

+ * Note: Uses ThreadSafeSampleNamed instead of actual DayCount to keep test isolated. + */ + @Test + public void test_real_world_scenario_bond_pricing() throws Exception { + ForkJoinPool pool = new ForkJoinPool(4); + try { + // Simulate processing multiple bonds in parallel + List bonds = Arrays.asList("Bond1", "Bond2", "Bond3", "Bond4"); + + List> futures = new ArrayList<>(); + for (String bond : bonds) { + futures.add(pool.submit(() -> calculateAccruedInterest(bond))); + } + + // Verify all calculations completed + for (Future future : futures) { + String result = future.get(5, TimeUnit.SECONDS); + assertThat(result).isNotNull(); + assertThat(result).contains("DayCount:"); + } + } finally { + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** + * Simulates a bond pricing calculation that uses ExtendedEnum. + *

+ * When called from ForkJoinPool, this triggers ExtendedEnum initialization in a thread + * where the context classloader may be null or inappropriate. In production, this would + * be code like: {@code DayCount dayCount = DayCounts.ACT_360;} + */ + private String calculateAccruedInterest(String bondId) { + // Simulates the real production code path that caused the issue + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + ThreadSafeSampleNamed dayCount = enumInstance.lookup("ThreadStandard"); + + return bondId + " DayCount: " + dayCount.getName(); + } + + /** + * Test that multiple ForkJoinPools can safely initialize different ExtendedEnum instances. + */ + @Test + public void test_multiple_pools_multiple_enums() throws Exception { + ForkJoinPool pool1 = new ForkJoinPool(2); + ForkJoinPool pool2 = new ForkJoinPool(2); + + try { + // Pool 1 initializes ThreadSafeSampleNamed + Future future1 = pool1.submit(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup("ThreadStandard"); + }); + + // Pool 2 also initializes ThreadSafeSampleNamed + Future future2 = pool2.submit(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + return enumInstance.lookup("ThreadMore"); + }); + + // Both should succeed + assertThat(future1.get(5, TimeUnit.SECONDS).getName()).isEqualTo("ThreadStandard"); + assertThat(future2.get(5, TimeUnit.SECONDS).getName()).isEqualTo("ThreadMore"); + } finally { + pool1.shutdown(); + pool2.shutdown(); + pool1.awaitTermination(5, TimeUnit.SECONDS); + pool2.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** + * Diagnostic test examining the thread context classloader (TCCL) in ForkJoinPool. + *

+ * In production environments with complex classloader hierarchies (application servers, + * OSGi), ForkJoinPool worker threads often have a null or inappropriate TCCL. This is + * the root cause of the ClassNotFoundException reported in issue #2748 when RenameHandler + * tries to load classes using the TCCL. + *

+ * In this Maven test environment, the TCCL may not be null because all classes share + * the same classloader, preventing reproduction of the issue. + */ + @Test + public void test_diagnostic_thread_context_classloader() throws Exception { + ForkJoinPool pool = new ForkJoinPool(1); + try { + Future future = pool.submit(() -> Thread.currentThread().getContextClassLoader()); + + ClassLoader tccl = future.get(5, TimeUnit.SECONDS); + + // In Maven's test environment, TCCL is typically not null, but in production + // application servers and OSGi containers, it often is null or points to the + // wrong classloader, causing ClassNotFoundException (issue #2748). + + // Verify ExtendedEnum initialization works in this test environment + assertThatCode(() -> { + ExtendedEnum enumInstance = ExtendedEnum.of(ThreadSafeSampleNamed.class); + enumInstance.lookup("ThreadStandard"); + }).doesNotThrowAnyException(); + } finally { + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.SECONDS); + } + } + +} + diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java new file mode 100644 index 0000000000..990eed0c9c --- /dev/null +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadMoreSampleNameds.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.collect.named; + +/** + * Additional mock named object provider for thread safety testing. + */ +public class ThreadMoreSampleNameds implements ThreadSafeSampleNamed { + + /** + * Another instance. + */ + public static final ThreadMoreSampleNameds MORE = new ThreadMoreSampleNameds(); + + @Override + public String getName() { + return "ThreadMore"; + } + +} diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java new file mode 100644 index 0000000000..97cc44e6b8 --- /dev/null +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNamed.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.collect.named; + +/** + * Mock named object for thread safety testing. + *

+ * This is a separate type from {@link SampleNamed} to ensure we can test + * fresh initialization in different threading contexts without interference + * from other tests. + */ +public interface ThreadSafeSampleNamed extends Named { + + /** + * Gets the extended enum helper for thread safety testing. + * + * @return the extended enum helper + */ + public static ExtendedEnum extendedEnum() { + return ExtendedEnum.of(ThreadSafeSampleNamed.class); + } + +} + diff --git a/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java new file mode 100644 index 0000000000..6146c7380a --- /dev/null +++ b/modules/collect/src/test/java/com/opengamma/strata/collect/named/ThreadSafeSampleNameds.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.collect.named; + +/** + * Mock named object provider for thread safety testing. + */ +public class ThreadSafeSampleNameds implements ThreadSafeSampleNamed { + + /** + * Standard instance. + */ + public static final ThreadSafeSampleNameds STANDARD = new ThreadSafeSampleNameds(); + + @Override + public String getName() { + return "ThreadStandard"; + } + +} + diff --git a/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini b/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini new file mode 100644 index 0000000000..d51d0dc155 --- /dev/null +++ b/modules/collect/src/test/resources/META-INF/com/opengamma/strata/config/base/ThreadSafeSampleNamed.ini @@ -0,0 +1,9 @@ +# ExtendedEnum configuration for thread safety testing + +[chain] +chainNextFile = false + +[providers] +com.opengamma.strata.collect.named.ThreadSafeSampleNameds = constants +com.opengamma.strata.collect.named.ThreadMoreSampleNameds = constants +