This is an automated email from the ASF dual-hosted git repository. ifesdjeen pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/cassandra-simulator.git
commit 9b9876680c64f60cc5e0c4ed3b7fafed8ff4a0eb Author: Alex Petrov <[email protected]> AuthorDate: Mon Jul 13 13:43:46 2026 +0000 Refine simulator class loading and isolation Introduce a dedicated ClassLoadingPolicy to centralize shared-vs-isolated class resolution, including explicit shared/isolated overrides, annotation-based decisions, and default shared filtering. Rename InstanceClassLoader to IsolatedClassLoader to better reflect its role, and update SimulationCluster and Simulator to construct and use the new policy consistently. Also move simulator tests into the simulator_test package, relocate SimulatorProperties into the simulator package, and expand coverage around class routing and park/unpark behavior under simulation. --- .../cassandra/simulator_test/StepApiTest.java | 2 +- .../simulator/context/ClassLoadingPolicy.java | 143 +++++++++++++++++++++ .../simulator/context/IClassTransformer.java | 4 +- ...ceClassLoader.java => IsolatedClassLoader.java} | 55 +++----- .../simulator/context/SharedTestState.java | 2 +- .../simulator/context/SimulationCluster.java | 30 ++--- .../simulator/context/SimulationContext.java | 8 +- .../SimulationContextClassLoaderOwnershipTest.java | 38 ++++-- .../SimulationContextClassRoutingTest.java | 6 +- .../SimulationContextIntegrationTest.java | 2 +- .../SimulationContextReturnTransferTest.java | 4 +- .../SimulationContextSharedClassTest.java | 43 ++++++- .../tests => simulator_test}/TestUtils.java | 2 +- .../org/apache/cassandra/simulator/Action.java | 2 +- .../apache/cassandra/simulator/ActionSchedule.java | 2 +- .../org/apache/cassandra/simulator/Ordered.java | 2 +- .../org/apache/cassandra/simulator/Simulator.java | 42 +++--- .../{config => simulator}/SimulatorProperties.java | 2 +- .../simulator/systems/InterceptingExecutor.java | 2 +- .../systems/InterceptingGlobalMethods.java | 5 +- .../simulator/systems/InterceptingMonitors.java | 2 +- .../cassandra/simulator_test/ParkUnparkTest.java | 112 +++++++++++++--- 22 files changed, 376 insertions(+), 134 deletions(-) diff --git a/integration-test/src/test/java/org/apache/cassandra/simulator_test/StepApiTest.java b/integration-test/src/test/java/org/apache/cassandra/simulator_test/StepApiTest.java index 1c7f4f1..8b09607 100644 --- a/integration-test/src/test/java/org/apache/cassandra/simulator_test/StepApiTest.java +++ b/integration-test/src/test/java/org/apache/cassandra/simulator_test/StepApiTest.java @@ -52,7 +52,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; * <h2>Shared state</h2> * * {@link SharedTestState} lives in {@code org.apache.cassandra.simulator.context}, which - * {@link org.apache.cassandra.simulator.context.InstanceClassLoader#DEFAULT_SHARED_FILTER} + * {@link org.apache.cassandra.simulator.context.IsolatedClassLoader#DEFAULT_SHARED_FILTER} * marks as always-shared. The <em>same</em> {@code Class} object (and therefore the same * static {@code AtomicInteger} instances) is seen from both the outer test thread and from * code running inside the instance classloader. No serialization barrier applies to diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/ClassLoadingPolicy.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/ClassLoadingPolicy.java new file mode 100644 index 0000000..1666ac3 --- /dev/null +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/ClassLoadingPolicy.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.simulator.context; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Shared-vs-isolated classloading policy for IsolatedClassLoader. + * + * Precedence: + * 1. explicit isolated override + * 2. explicit shared override + * 3. @Isolated annotation + * 4. @Shared annotation + * 5. default shared filter + * 6. otherwise isolated + */ +public final class ClassLoadingPolicy +{ + public enum Decision { SHARED, ISOLATED } + + private final ClassLoader sharedClassLoader; + private final Set<String> explicitShared; + private final Set<String> explicitIsolated; + private final Predicate<String> defaultShared; + private ClassLoadingPolicy(ClassLoader sharedClassLoader, + Set<String> explicitShared, + Set<String> explicitIsolated, + Predicate<String> defaultShared) + { + this.sharedClassLoader = sharedClassLoader; + this.explicitShared = explicitShared; + this.explicitIsolated = explicitIsolated; + this.defaultShared = defaultShared; + } + + public Decision decide(String className) + { + return computeDecision(className); + } + + public boolean isShared(String className) + { + return decide(className) == Decision.SHARED; + } + + private Decision computeDecision(String className) + { + if (explicitIsolated.contains(className)) return Decision.ISOLATED; + if (explicitShared.contains(className)) return Decision.SHARED; + + Class<?> maybeShared = tryLoadOnSharedSide(className); + if (maybeShared != null) + { + if (maybeShared.getAnnotation(Isolated.class) != null) + return Decision.ISOLATED; + if (maybeShared.getAnnotation(Shared.class) != null) + return Decision.SHARED; + } + + return defaultShared.test(className) ? Decision.SHARED : Decision.ISOLATED; + } + + private Class<?> tryLoadOnSharedSide(String className) + { + try + { + return Class.forName(className, false, sharedClassLoader); + } + catch (ClassNotFoundException | LinkageError e) + { + return null; + } + } + + public static Builder builder(ClassLoader sharedClassLoader) + { + return new Builder(sharedClassLoader); + } + + public static final class Builder + { + private final ClassLoader sharedClassLoader; + private final Set<String> explicitShared = new LinkedHashSet<>(); + private final Set<String> explicitIsolated = new LinkedHashSet<>(); + private Predicate<String> defaultShared = name -> false; + + private Builder(ClassLoader sharedClassLoader) + { + this.sharedClassLoader = sharedClassLoader; + } + + public Builder withSharedClasses(Set<String> classNames) + { + explicitShared.addAll(classNames); + return this; + } + + public Builder withIsolatedClasses(Set<String> classNames) + { + explicitIsolated.addAll(classNames); + return this; + } + + public Builder withDefaultSharedFilter(Predicate<String> defaultShared) + { + this.defaultShared = defaultShared; + return this; + } + + public ClassLoadingPolicy build() + { + Set<String> overlap = new LinkedHashSet<>(explicitShared); + overlap.retainAll(explicitIsolated); + if (!overlap.isEmpty()) + throw new IllegalArgumentException("classes appear in both shared and isolated sets: " + overlap); + + return new ClassLoadingPolicy(sharedClassLoader, + Collections.unmodifiableSet(new LinkedHashSet<>(explicitShared)), + Collections.unmodifiableSet(new LinkedHashSet<>(explicitIsolated)), + defaultShared); + } + } +} diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IClassTransformer.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IClassTransformer.java index ff31c70..e42314d 100644 --- a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IClassTransformer.java +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IClassTransformer.java @@ -19,8 +19,8 @@ package org.apache.cassandra.simulator.context; /** - * Optional bytecode transformer applied when an {@link InstanceClassLoader} defines a class. - * Returning {@code null} or the original bytes leaves the class unmodified. + * Optional bytecode transformer applied when an IsolatedClassLoader defines a class. + * Returning null or the original bytes leaves the class unmodified. */ public interface IClassTransformer { diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/InstanceClassLoader.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IsolatedClassLoader.java similarity index 64% rename from simulator-context/src/main/java/org/apache/cassandra/simulator/context/InstanceClassLoader.java rename to simulator-context/src/main/java/org/apache/cassandra/simulator/context/IsolatedClassLoader.java index 417b06a..68025f0 100644 --- a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/InstanceClassLoader.java +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/IsolatedClassLoader.java @@ -27,32 +27,17 @@ import java.util.function.Predicate; /** * Per-context classloader that isolates application classes from other contexts and from * the host JVM. - * - * Application classes are loaded independently from {@code urls} and passed through the - * ASM transformer, giving each context its own copy with independent static state and - * rewritten call sites. - * - * JDK classes, the simulator framework ({@code org.apache.cassandra.simulator.context.*}), - * and logging classes are excluded: they are delegated to {@code sharedClassLoader} and - * reach the JVM unmodified, so every context and the host thread share a single - * {@code Class} object for them. - * - * {@link #getDefaultSharedFilter()} enumerates the packages that are always shared. */ -public class InstanceClassLoader extends URLClassLoader +public class IsolatedClassLoader extends URLClassLoader { - /** Generation counter — incremented on context restart so restarted contexts get a fresh loader. */ public final int generation; - - /** 1-based context identifier. */ public final int id; private final ClassLoader sharedClassLoader; - private final Predicate<String> loadShared; + private final ClassLoadingPolicy policy; private final IClassTransformer transformer; - // Packages that are always loaded from the shared/bootstrap classloader. - private static final Predicate<String> DEFAULT_SHARED_FILTER = name -> + static final Predicate<String> DEFAULT_SHARED_FILTER = name -> name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("jdk.") @@ -60,29 +45,28 @@ public class InstanceClassLoader extends URLClassLoader || name.startsWith("com.sun.") || name.startsWith("org.xml.") || name.startsWith("org.w3c.") - // simulator framework classes must be shared so cross-CL method calls work || name.startsWith("org.apache.cassandra.simulator.context.") - // logging is shared so output goes to one place || name.startsWith("org.slf4j.") || name.startsWith("ch.qos.logback."); - public InstanceClassLoader(int generation, int id, URL[] urls, + public IsolatedClassLoader(int generation, int id, URL[] urls, ClassLoader sharedClassLoader, - Predicate<String> loadShared, + ClassLoadingPolicy policy, IClassTransformer transformer) { super(urls, null /* no parent — we route explicitly */); - assert urls != null && urls.length > 0 : "InstanceClassLoader for context " + id + " has empty classpath"; + assert urls != null && urls.length > 0 : "IsolatedClassLoader for context " + id + " has empty classpath"; assert sharedClassLoader != null : "sharedClassLoader must not be null"; + assert policy != null : "policy must not be null"; assert generation >= 0 : "generation must be non-negative, got " + generation; assert id >= 1 : "context id must be 1-based, got " + id; - this.generation = generation; - this.id = id; + this.generation = generation; + this.id = id; this.sharedClassLoader = sharedClassLoader; - this.loadShared = loadShared; - this.transformer = transformer; + this.policy = policy; + this.transformer = transformer; } @Override @@ -94,10 +78,7 @@ public class InstanceClassLoader extends URLClassLoader if (cached != null) return cached; - // Delegate to the shared classloader when the predicate says so. - // The predicate already incorporates DEFAULT_SHARED_FILTER; explicit - // isolation always takes priority over package-prefix defaults. - if (loadShared.test(name)) + if (policy.isShared(name)) { Class<?> c = sharedClassLoader.loadClass(name); assert isSharedOrBootstrap(c) @@ -105,10 +86,9 @@ public class InstanceClassLoader extends URLClassLoader return c; } - // Load from our own URLs, applying optional bytecode transformation. Class<?> c = findAndDefine(name); assert c.getClassLoader() == this - : "class '" + name + "' routed to instance CL but loaded by " + c.getClassLoader(); + : "class '" + name + "' routed to isolated CL but loaded by " + c.getClassLoader(); if (resolve) resolveClass(c); @@ -140,20 +120,15 @@ public class InstanceClassLoader extends URLClassLoader } } - /** Returns true if {@code c} was loaded by the shared or a bootstrap/platform classloader. */ private boolean isSharedOrBootstrap(Class<?> c) { ClassLoader cl = c.getClassLoader(); - return cl == null // bootstrap + return cl == null || cl == ClassLoader.getPlatformClassLoader() || cl == ClassLoader.getSystemClassLoader() || cl == sharedClassLoader; } - /** - * Returns the predicate that is always applied before the caller-supplied one. - * Covers JVM internals and the simulator framework itself. - */ public static Predicate<String> getDefaultSharedFilter() { return DEFAULT_SHARED_FILTER; @@ -162,6 +137,6 @@ public class InstanceClassLoader extends URLClassLoader @Override public String toString() { - return "InstanceClassLoader{id=" + id + ", generation=" + generation + "}"; + return "IsolatedClassLoader{id=" + id + ", generation=" + generation + '}'; } } diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java index 568ae9e..861771c 100644 --- a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SharedTestState.java @@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; * Shared mutable state for step-by-step simulator tests. * * This class MUST live in the {@code org.apache.cassandra.simulator.context} package - * because {@link InstanceClassLoader#DEFAULT_SHARED_FILTER} hard-codes that package + * because IsolatedClassLoader.DEFAULT_SHARED_FILTER hard-codes that package * as always-shared. When instrumented code running inside the instance classloader * touches {@code SharedTestState.threadRan}, the JVM delegates the class lookup to * the shared (system) classloader, returning the same {@code Class} object -- and diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationCluster.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationCluster.java index c70c4a8..6af3fc7 100644 --- a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationCluster.java +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationCluster.java @@ -27,14 +27,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.function.Predicate; import java.util.stream.Collectors; /** * Creates and manages a group of {@link SimulationContext} instances. * * All contexts share one parent classloader for explicitly-shared and default-shared classes. - * Each context gets its own {@link InstanceClassLoader} for isolated classes. + * Each context gets its own IsolatedClassLoader for isolated classes. * Contexts are numbered 1..n. * * @param <C> context type @@ -45,7 +44,7 @@ public class SimulationCluster<C extends SimulationContext> implements AutoClose @FunctionalInterface public interface ContextFactory<C extends SimulationContext> { - C create(int id, InstanceClassLoader classLoader, ThreadGroup threadGroup); + C create(int id, IsolatedClassLoader classLoader, ThreadGroup threadGroup); } private final List<C> contexts; @@ -86,18 +85,11 @@ public class SimulationCluster<C extends SimulationContext> implements AutoClose Set<String> isolatedNames = classNames(isolatedClasses); Set<String> sharedNames = classNames(sharedClasses); - // Incorporate the default filter so that explicit isolation always wins over - // package-prefix defaults. Order of precedence: - // 1. isolatedClasses[] / @Isolated → never shared - // 2. sharedClasses[] / @Shared → always shared - // 3. DEFAULT_SHARED_FILTER → JVM + framework defaults - // 4. anything else → isolated (own copy per context) - Predicate<String> defaultFilter = InstanceClassLoader.getDefaultSharedFilter(); - Predicate<String> loadShared = name -> { - if (isolatedNames.contains(name)) return false; - if (sharedNames.contains(name)) return true; - return defaultFilter.test(name); - }; + ClassLoadingPolicy policy = ClassLoadingPolicy.builder(sharedClassLoader) + .withIsolatedClasses(isolatedNames) + .withSharedClasses(sharedNames) + .withDefaultSharedFilter(IsolatedClassLoader.getDefaultSharedFilter()) + .build(); List<C> mutable = new ArrayList<>(); ThreadGroup clusterGroup = new ThreadGroup(UUID.randomUUID().toString()); @@ -106,7 +98,7 @@ public class SimulationCluster<C extends SimulationContext> implements AutoClose { int id = i; ThreadGroup threadGroup = new ThreadGroup(clusterGroup, "context" + id); - InstanceClassLoader cl = new InstanceClassLoader(1, id, classpath, sharedClassLoader, loadShared, null); + IsolatedClassLoader cl = new IsolatedClassLoader(1, id, classpath, sharedClassLoader, policy, null); mutable.add(factory.create(id, cl, threadGroup)); } @@ -117,7 +109,7 @@ public class SimulationCluster<C extends SimulationContext> implements AutoClose .map(SimulationContext::instanceClassLoader) .distinct() .count() == n - : "each context must have its own distinct InstanceClassLoader"; + : "each context must have its own distinct IsolatedClassLoader"; return result; } @@ -179,5 +171,9 @@ public class SimulationCluster<C extends SimulationContext> implements AutoClose for (Class<?> c : sharedClasses) assert c.getAnnotation(Isolated.class) == null : "class " + c.getName() + " is annotated @Isolated but listed in sharedClasses[]"; + + for (Class<?> c : isolatedClasses) + assert c.getAnnotation(Shared.class) == null + : "class " + c.getName() + " is annotated @Shared but listed in isolatedClasses[]"; } } diff --git a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationContext.java b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationContext.java index 010fb55..c2ebcc0 100644 --- a/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationContext.java +++ b/simulator-context/src/main/java/org/apache/cassandra/simulator/context/SimulationContext.java @@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit; /** * A single isolated execution context. * - * Each context owns a dedicated {@link InstanceClassLoader} and a thread pool + * Each context owns a dedicated IsolatedClassLoader and a thread pool * whose threads have that classloader set as their context classloader (CCL). * Code dispatched via {@link #runOn} is serialized into this classloader and * executed there; the return value is serialized back so the caller receives @@ -47,7 +47,7 @@ public class SimulationContext extends IsolatedExecutor // app lives inside the isolated CL after startup private SimulatedApp app; - public SimulationContext(int id, InstanceClassLoader classLoader, ThreadGroup threadGroup) + public SimulationContext(int id, IsolatedClassLoader classLoader, ThreadGroup threadGroup) { super("context" + id, classLoader, createExecutor(id, classLoader, threadGroup), DEFAULT_SHUTDOWN_EXECUTOR); this.id = id; @@ -143,9 +143,9 @@ public class SimulationContext extends IsolatedExecutor // ── Internal ────────────────────────────────────────────────────────────── - public InstanceClassLoader instanceClassLoader() + public IsolatedClassLoader instanceClassLoader() { - return (InstanceClassLoader) classLoader; + return (IsolatedClassLoader) classLoader; } private static ExecutorService createExecutor(int id, ClassLoader classLoader, ThreadGroup threadGroup) diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassLoaderOwnershipTest.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassLoaderOwnershipTest.java similarity index 77% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassLoaderOwnershipTest.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassLoaderOwnershipTest.java index 8907bbd..be3b384 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassLoaderOwnershipTest.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassLoaderOwnershipTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; @@ -34,9 +34,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; * Guarantee 1: isolated classes are owned by the context's classloader. * * TC-1.1 Static fields are independent between contexts. - * TC-1.2 Objects created inside a context report InstanceClassLoader as their class's loader. - * TC-1.3 Each context has a distinct classloader. - * TC-1.4 The executor thread's CCL matches the instance classloader. + * TC-1.2 Host and context see different static state for an isolated class. + * TC-1.3 Objects created inside a context report IsolatedClassLoader as their class's loader. + * TC-1.4 Each context has a distinct classloader. + * TC-1.5 The executor thread's CCL matches the instance classloader. */ public class SimulationContextClassLoaderOwnershipTest { @@ -61,6 +62,7 @@ public class SimulationContextClassLoaderOwnershipTest SimulationContext::new); cluster.get(1).startup(TestUtils.NoopApp::new); cluster.get(2).startup(TestUtils.NoopApp::new); + IsolatedCounter.value = 0; } @AfterEach @@ -87,19 +89,35 @@ public class SimulationContextClassLoaderOwnershipTest // ── TC-1.2 ─────────────────────────────────────────────────────────────── @Test - public void objectsCreatedInsideContextReportInstanceClassLoader() + public void hostAndContextSeeDifferentStaticState() + { + IsolatedCounter.value = 11; + + cluster.get(1).runOn(() -> { IsolatedCounter.value = 21; return null; }); + int v1 = cluster.get(1).runOn(() -> IsolatedCounter.value); + int v2 = cluster.get(2).runOn(() -> IsolatedCounter.value); + + assertEquals(11, IsolatedCounter.value, "host must keep its own isolated static state"); + assertEquals(21, v1, "context1 must keep its own isolated static state"); + assertEquals(0, v2, "context2 must keep its own isolated static state"); + } + + // ── TC-1.3 ─────────────────────────────────────────────────────────────── + + @Test + public void objectsCreatedInsideContextReportIsolatedClassLoader() { String loaderName = cluster.get(1).runOn(() -> new IsolatedCounter().getClass().getClassLoader().toString() ); - assertTrue(loaderName.contains("InstanceClassLoader"), - "class should be loaded by InstanceClassLoader, got: " + loaderName); + assertTrue(loaderName.contains("IsolatedClassLoader"), + "class should be loaded by IsolatedClassLoader, got: " + loaderName); assertTrue(loaderName.contains("id=1"), "loader should identify context 1, got: " + loaderName); } - // ── TC-1.3 ─────────────────────────────────────────────────────────────── + // ── TC-1.4 ─────────────────────────────────────────────────────────────── @Test public void eachContextHasDistinctClassLoader() @@ -110,10 +128,10 @@ public class SimulationContextClassLoaderOwnershipTest assertNotEquals(cl1, cl2, "contexts should have distinct classloaders"); } - // ── TC-1.4 ─────────────────────────────────────────────────────────────── + // ── TC-1.5 ─────────────────────────────────────────────────────────────── @Test - public void executorThreadCCLMatchesInstanceClassLoader() + public void executorThreadCCLMatchesIsolatedClassLoader() { String threadCL = cluster.get(1).runOn(() -> Thread.currentThread().getContextClassLoader().toString()); String instanceCL = cluster.get(1).instanceClassLoader().toString(); diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassRoutingTest.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassRoutingTest.java similarity index 97% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassRoutingTest.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassRoutingTest.java index ccbd6fe..a11193c 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextClassRoutingTest.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextClassRoutingTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; @@ -77,8 +77,8 @@ public class SimulationContextClassRoutingTest { String loaderName = cluster.get(1).runOn(() -> ForceIsolated.class.getClassLoader().toString()); - assertTrue(loaderName.contains("InstanceClassLoader"), - "ForceIsolated must be loaded by InstanceClassLoader, got: " + loaderName); + assertTrue(loaderName.contains("IsolatedClassLoader"), + "ForceIsolated must be loaded by IsolatedClassLoader, got: " + loaderName); } // ── TC-4.2 ─────────────────────────────────────────────────────────────── diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextIntegrationTest.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextIntegrationTest.java similarity index 99% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextIntegrationTest.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextIntegrationTest.java index a36e5d8..b572014 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextIntegrationTest.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextIntegrationTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextReturnTransferTest.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextReturnTransferTest.java similarity index 98% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextReturnTransferTest.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextReturnTransferTest.java index 9b7bb97..e7c3ffb 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextReturnTransferTest.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextReturnTransferTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; @@ -93,7 +93,7 @@ public class SimulationContextReturnTransferTest // ── TC-2.2 ─────────────────────────────────────────────────────────────── @Test - public void returnValueClassLoaderIsNotInstanceClassLoader() + public void returnValueClassLoaderIsNotIsolatedClassLoader() { Box result = cluster.get(1).runOn(() -> new Box(7)); ClassLoader instanceCL = cluster.get(1).instanceClassLoader(); diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextSharedClassTest.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextSharedClassTest.java similarity index 79% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextSharedClassTest.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextSharedClassTest.java index cccd830..4a038f8 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/SimulationContextSharedClassTest.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/SimulationContextSharedClassTest.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; @@ -31,15 +31,18 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotSame; /** * Guarantee 3: shared classes are loaded exactly once, with static state visible everywhere. * * TC-3.1 The same Class object is returned from all contexts and the test thread. * TC-3.2 Shared static state written in one context is readable in another. - * TC-3.3 The shared class's classloader is the shared CL, not any instance CL. - * TC-3.4 JVM bootstrap classes (e.g. ArrayList) are always shared. - * TC-3.5 Explicit sharedClasses[] list (no annotation) behaves identically to @Shared. + * TC-3.3 Shared static state written on the host is readable in a context. + * TC-3.4 Shared static state written in a context is readable on the host. + * TC-3.5 The shared class's classloader is the shared CL, not any instance CL. + * TC-3.6 JVM bootstrap classes (e.g. ArrayList) are always shared. + * TC-3.7 Explicit sharedClasses[] list (no annotation) behaves identically to @Shared. */ public class SimulationContextSharedClassTest { @@ -65,7 +68,7 @@ public class SimulationContextSharedClassTest URL[] cp = TestUtils.runtimeClasspath(); cluster = SimulationCluster.create(3, cp, new Class<?>[0], - new Class<?>[]{ SharedCounter.class, ExplicitlyShared.class }, + new Class<?>[]{ ExplicitlyShared.class }, SimulationContext::new); cluster.get(1).startup(TestUtils.NoopApp::new); cluster.get(2).startup(TestUtils.NoopApp::new); @@ -106,6 +109,32 @@ public class SimulationContextSharedClassTest // ── TC-3.3 ─────────────────────────────────────────────────────────────── + @Test + public void hostWriteIsVisibleInContext() + { + SharedCounter.value.set(17); + + int v1 = cluster.get(1).runOn(() -> SharedCounter.value.get()); + int v2 = cluster.get(2).runOn(() -> SharedCounter.value.get()); + + assertEquals(17, v1, "host write must be visible in context1"); + assertEquals(17, v2, "host write must be visible in context2"); + } + + // ── TC-3.4 ─────────────────────────────────────────────────────────────── + + @Test + public void contextWriteIsVisibleOnHost() + { + SharedCounter.value.set(0); + + cluster.get(1).runOn(() -> { SharedCounter.value.set(23); return null; }); + + assertEquals(23, SharedCounter.value.get(), "context write must be visible on the host"); + } + + // ── TC-3.5 ─────────────────────────────────────────────────────────────── + @Test public void sharedClassLoaderIsSharedCL() { @@ -122,7 +151,7 @@ public class SimulationContextSharedClassTest : "SharedCounter must not be loaded by an instance CL"; } - // ── TC-3.4 ─────────────────────────────────────────────────────────────── + // ── TC-3.6 ─────────────────────────────────────────────────────────────── @Test public void jvmBootstrapClassesAreAlwaysShared() @@ -134,7 +163,7 @@ public class SimulationContextSharedClassTest assertNull(ArrayList.class.getClassLoader(), "ArrayList must be bootstrap-loaded"); } - // ── TC-3.5 ─────────────────────────────────────────────────────────────── + // ── TC-3.7 ─────────────────────────────────────────────────────────────── @Test public void explicitSharedListBehavesLikeAnnotation() diff --git a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/TestUtils.java b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/TestUtils.java similarity index 97% rename from simulator-context/src/test/java/org/apache/cassandra/simulator/tests/TestUtils.java rename to simulator-context/src/test/java/org/apache/cassandra/simulator_test/TestUtils.java index 5c7ea32..a71d585 100644 --- a/simulator-context/src/test/java/org/apache/cassandra/simulator/tests/TestUtils.java +++ b/simulator-context/src/test/java/org/apache/cassandra/simulator_test/TestUtils.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.simulator.tests; +package org.apache.cassandra.simulator_test; import org.apache.cassandra.simulator.context.*; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java index 00e323d..ad3aa0e 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Action.java @@ -37,7 +37,7 @@ import io.netty.util.internal.DefaultPriorityQueue; import io.netty.util.internal.PriorityQueue; import io.netty.util.internal.PriorityQueueNode; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DEBUG; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DEBUG; import static org.apache.cassandra.simulator.Action.Modifier.DAEMON; import static org.apache.cassandra.simulator.Action.Modifier.ORPHAN; import static org.apache.cassandra.simulator.Action.Modifier.ORPHAN_TO_GRANDPARENT; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/ActionSchedule.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/ActionSchedule.java index 6f9aabf..72bd411 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/ActionSchedule.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/ActionSchedule.java @@ -45,7 +45,7 @@ import org.apache.cassandra.simulator.utils.Throwables; import io.netty.util.internal.DefaultPriorityQueue; import io.netty.util.internal.PriorityQueue; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DEBUG; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DEBUG; import static org.apache.cassandra.simulator.Action.Modifier.DAEMON; import static org.apache.cassandra.simulator.Action.Modifier.STREAM; import static org.apache.cassandra.simulator.Action.Phase.CONSEQUENCE; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/Ordered.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/Ordered.java index 754ae6f..9b3ebf5 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Ordered.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Ordered.java @@ -33,7 +33,7 @@ import org.apache.cassandra.simulator.utils.IntrusiveLinkedList; import org.apache.cassandra.simulator.utils.IntrusiveLinkedListNode; import static java.util.Collections.newSetFromMap; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DEBUG; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DEBUG; /** * Represents an action that may not run before certain other actions diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java index b90facc..feb5d6a 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java @@ -39,9 +39,10 @@ import java.util.function.Supplier; import org.apache.cassandra.simulator.asm.InterceptAsClassTransformer; import org.apache.cassandra.simulator.asm.InterceptRule; +import org.apache.cassandra.simulator.context.ClassLoadingPolicy; import org.apache.cassandra.simulator.context.IClassTransformer; import org.apache.cassandra.simulator.context.IIsolatedExecutor; -import org.apache.cassandra.simulator.context.InstanceClassLoader; +import org.apache.cassandra.simulator.context.IsolatedClassLoader; import org.apache.cassandra.simulator.context.IsolatedExecutor; import org.apache.cassandra.simulator.step.Session; import org.apache.cassandra.simulator.systems.InterceptedExecution; @@ -420,7 +421,7 @@ public class Simulator implements AutoCloseable /** * Run the simulation by transferring each body into an ASM-transformed - * {@link InstanceClassLoader}, wrapping each in a simulated thread, and executing + * {@link IsolatedClassLoader}, wrapping each in a simulated thread, and executing * them under scheduler control. * * <p>Inside the bodies, standard Java concurrency primitives are transparently @@ -485,7 +486,7 @@ public class Simulator implements AutoCloseable /** * Prepare a simulation for step-by-step execution. * - * <p>The bodies are serialized into an {@link InstanceClassLoader}, wrapped in + * <p>The bodies are serialized into an {@link IsolatedClassLoader}, wrapped in * simulated thread actions, and queued in an {@link ActionSchedule} - but nothing * executes until the caller drives the returned {@link Session}. * @@ -547,7 +548,7 @@ public class Simulator implements AutoCloseable * objects are instances of the simulation classloader's version of the impl class, * matching the CHECKCAST target in the transformed bytecode. */ - private void registerDeferredFactories(InstanceClassLoader instanceCL) + private void registerDeferredFactories(IsolatedClassLoader instanceCL) { for (DeferredReg reg : deferredRegistrations) { @@ -595,16 +596,19 @@ public class Simulator implements AutoCloseable Collections.unmodifiableSet(new LinkedHashSet<>(customRules)) ); - Predicate<String> loadShared = name -> - InstanceClassLoader.getDefaultSharedFilter().test(name) - || (name.startsWith("org.apache.cassandra.simulator.") - && !name.startsWith("org.apache.cassandra.simulator.test.")) - || name.startsWith("org.apache.cassandra.utils."); + ClassLoader sharedClassLoader = ClassLoader.getSystemClassLoader(); + ClassLoadingPolicy policy = ClassLoadingPolicy.builder(sharedClassLoader) + .withDefaultSharedFilter(name -> + IsolatedClassLoader.getDefaultSharedFilter().test(name) + || (name.startsWith("org.apache.cassandra.simulator.") + && !name.startsWith("org.apache.cassandra.simulator.test.")) + || name.startsWith("org.apache.cassandra.utils.")) + .build(); - InstanceClassLoader instanceCL = new InstanceClassLoader( + IsolatedClassLoader instanceCL = new IsolatedClassLoader( 0, 1, urls, - ClassLoader.getSystemClassLoader(), - loadShared, + sharedClassLoader, + policy, transformer ); @@ -628,14 +632,12 @@ public class Simulator implements AutoCloseable actions.add(threadAction("thread-" + i, transferred)); } - return new ActionSchedule( - simulatedTime, - () -> 0L, - () -> 0L, - new RunnableActionScheduler.Sequential(), - new ActionSchedule.Work(ActionSchedule.Mode.FINITE, - Collections.singletonList(ActionList.of(actions))) - ); + return new ActionSchedule(simulatedTime, + () -> 0L, + () -> 0L, + new RunnableActionScheduler.Sequential(), + new ActionSchedule.Work(ActionSchedule.Mode.FINITE, + Collections.singletonList(ActionList.of(actions)))); } /** diff --git a/simulator-core/src/main/java/org/apache/cassandra/config/SimulatorProperties.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/SimulatorProperties.java similarity index 97% rename from simulator-core/src/main/java/org/apache/cassandra/config/SimulatorProperties.java rename to simulator-core/src/main/java/org/apache/cassandra/simulator/SimulatorProperties.java index 153fb30..dfd23ee 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/config/SimulatorProperties.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/SimulatorProperties.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.config; +package org.apache.cassandra.simulator; import java.util.function.Function; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingExecutor.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingExecutor.java index 2d13db0..cbde83f 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingExecutor.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingExecutor.java @@ -53,7 +53,7 @@ import static java.util.Collections.newSetFromMap; import static java.util.Collections.synchronizedMap; import static java.util.Collections.synchronizedSet; import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DEBUG; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DEBUG; import static org.apache.cassandra.simulator.systems.InterceptibleThread.runDeterministic; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.SCHEDULED_DAEMON; import static org.apache.cassandra.simulator.systems.SimulatedAction.Kind.SCHEDULED_TASK; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java index a09ddcc..c98e5c5 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingGlobalMethods.java @@ -27,15 +27,12 @@ import java.util.function.LongConsumer; import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.cassandra.simulator.RandomSource; import org.apache.cassandra.simulator.systems.InterceptedWait.CaptureSites.Capture; import org.apache.cassandra.simulator.systems.InterceptedWait.InterceptedConditionWait; import org.apache.cassandra.simulator.utils.Clock; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DETERMINISM_CHECK; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DETERMINISM_CHECK; import static org.apache.cassandra.simulator.SimulatorUtils.failWithOOM; import static org.apache.cassandra.simulator.systems.InterceptedWait.Kind.NEMESIS; import static org.apache.cassandra.simulator.systems.NonInterceptible.Permit.OPTIONAL; diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java index 6626c8e..9a73d8f 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/systems/InterceptingMonitors.java @@ -37,7 +37,7 @@ import org.apache.cassandra.simulator.utils.Threads; import org.apache.cassandra.simulator.utils.UncheckedInterruptedException; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.apache.cassandra.config.SimulatorProperties.TEST_SIMULATOR_DEBUG; +import static org.apache.cassandra.simulator.SimulatorProperties.TEST_SIMULATOR_DEBUG; import static org.apache.cassandra.simulator.SimulatorUtils.failWithOOM; import static org.apache.cassandra.simulator.systems.InterceptedWait.Kind.NEMESIS; import static org.apache.cassandra.simulator.systems.InterceptedWait.Kind.SLEEP_UNTIL; diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java index beaadfc..3f0e166 100644 --- a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/ParkUnparkTest.java @@ -18,30 +18,53 @@ package org.apache.cassandra.simulator_test; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.LockSupport; import org.apache.cassandra.simulator.Simulator; +import org.apache.cassandra.simulator.step.Session; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable; +import org.apache.cassandra.simulator.context.Shared; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Verifies that {@link LockSupport#park()} and {@link LockSupport#unpark(Thread)} are - * intercepted by the ASM transformer and work correctly under simulation. + * Verifies that LockSupport.park() and LockSupport.unpark(Thread) are intercepted and work + * correctly under simulation. * - * <p>Important: when {@code new Thread(...).start()} is ASM-rewritten to - * {@link org.apache.cassandra.simulator.systems.InterceptorOfGlobalMethods.Global#startThread}, - * the original {@code Thread} object is <em>not</em> the running thread — the simulator - * creates a new {@code InterceptibleThread} to run the body. {@code LockSupport.unpark} - * must be called with the <em>running</em> thread reference, so we capture - * {@code Thread.currentThread()} from inside the parker body and share it with the - * unparker via a {@code CountDownLatch}-guarded reference slot. + * Important: after ASM rewrites new Thread(...).start(), the original Thread object is not the + * running thread. The simulator creates an InterceptibleThread to run the body instead. + * + * LockSupport.unpark() must therefore receive the running thread reference captured from inside + * the parker body via Thread.currentThread(). */ public class ParkUnparkTest { + @Shared + public static class SharedState + { + public static final Set<String> events = Collections.synchronizedSet(new LinkedHashSet<>()); + + public static void reset() + { + events.clear(); + } + } + + @BeforeEach + void reset() + { + SharedState.reset(); + } + @Test void unparkWakesParkedThread() { @@ -49,24 +72,83 @@ public class ParkUnparkTest int[] result = { 0 }; CountDownLatch parkerReady = new CountDownLatch(1); CountDownLatch done = new CountDownLatch(1); - // Slot for the *running* InterceptibleThread — set from inside the parker body. Thread[] parkerThread = { null }; new Thread(() -> { - parkerThread[0] = Thread.currentThread(); // the InterceptibleThread - parkerReady.countDown(); // signal: ref is ready + parkerThread[0] = Thread.currentThread(); + parkerReady.countDown(); LockSupport.park(); result[0] = 1; done.countDown(); }, "parker").start(); new Thread(() -> { - try { parkerReady.await(); } catch (InterruptedException e) { return; } - LockSupport.unpark(parkerThread[0]); // wake the InterceptibleThread + try { parkerReady.await(); } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + LockSupport.unpark(parkerThread[0]); }, "unparker").start(); - try { done.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + try { done.await(); } + catch (InterruptedException e) { Thread.currentThread().interrupt(); } assertEquals(1, result[0], "parked thread should have been woken by unpark"); }); } + + @Test + void unparkWakesParkedThread_byStep() + { + try (Simulator sim = new Simulator(42L); + Session session = sim.byStep((SerializableRunnable) () -> { + CountDownLatch parkerReady = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(2); + Thread[] parkedThread = { null }; + + new Thread(() -> { + parkedThread[0] = Thread.currentThread(); + parkerReady.countDown(); + SharedState.events.add("parking"); + LockSupport.park(); + SharedState.events.add("unparked"); + done.countDown(); + }, "parker").start(); + + new Thread(() -> { + try { parkerReady.await(); } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + LockSupport.unpark(parkedThread[0]); + done.countDown(); + }, "unparker").start(); + + try { done.await(); } + catch (InterruptedException e) { Thread.currentThread().interrupt(); } + })) + { + assertEquals(Set.of(), SharedState.events); + + assertTrue(session.step()); + assertEquals(Set.of(), SharedState.events); + + assertTrue(session.step()); + assertEquals(Set.of("parking"), SharedState.events, "parker reached park() and is suspended"); + + assertTrue(session.step()); + assertEquals(Set.of("parking"), SharedState.events, "unparker ran, but wakeup has not fired yet"); + + assertTrue(session.step()); + assertEquals(Set.of("parking", "unparked"), SharedState.events, "parker resumed after unpark"); + + assertTrue(session.step()); + assertEquals(Set.of("parking", "unparked"), SharedState.events); + assertFalse(session.hasNext()); + } + } + } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
