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 +