|
40 | 40 | */
|
41 | 41 | package com.oracle.truffle.api.test.polyglot;
|
42 | 42 |
|
| 43 | +import java.time.Duration; |
43 | 44 | import java.util.concurrent.atomic.AtomicLong;
|
44 | 45 |
|
| 46 | +import com.oracle.truffle.api.test.ThreadUtils; |
45 | 47 | import org.graalvm.polyglot.Context;
|
| 48 | +import org.graalvm.polyglot.Engine; |
46 | 49 | import org.junit.Assert;
|
47 | 50 | import org.junit.Test;
|
48 | 51 |
|
@@ -106,23 +109,74 @@ protected void disposeThread(ExecutableContext context, Thread thread) {
|
106 | 109 | }
|
107 | 110 | }
|
108 | 111 |
|
| 112 | + /** |
| 113 | + * A utility language used to wait for the termination of a specific thread identified by ID. |
| 114 | + * <p> |
| 115 | + * In the context of external isolates, the "mirror" thread created for a host thread terminates |
| 116 | + * after the host thread itself. This delay introduces a race condition in tests such as |
| 117 | + * {@code testDeadThreadDisposed}, which assume that once the host thread is no longer alive, it |
| 118 | + * must have been finalized. This language resolves the issue by actively waiting for the |
| 119 | + * "mirror" thread to terminate before continuing. |
| 120 | + */ |
| 121 | + @TruffleLanguage.Registration |
| 122 | + static final class WaitForThreadTermination extends AbstractExecutableTestLanguage { |
| 123 | + |
| 124 | + @Override |
| 125 | + @CompilerDirectives.TruffleBoundary |
| 126 | + protected Object execute(RootNode node, Env env, Object[] contextArguments, Object[] frameArguments) throws Exception { |
| 127 | + long threadId = (long) contextArguments[0]; |
| 128 | + long timeOutMillis = (long) contextArguments[1]; |
| 129 | + Thread thread = findThread(threadId); |
| 130 | + if (thread == null || !thread.isAlive()) { |
| 131 | + return true; |
| 132 | + } |
| 133 | + thread.join(timeOutMillis); |
| 134 | + return !thread.isAlive(); |
| 135 | + } |
| 136 | + |
| 137 | + private static Thread findThread(long threadId) { |
| 138 | + for (Thread thread : ThreadUtils.getAllThreads()) { |
| 139 | + if (thread.threadId() == threadId) { |
| 140 | + return thread; |
| 141 | + } |
| 142 | + } |
| 143 | + return null; |
| 144 | + } |
| 145 | + |
| 146 | + /** |
| 147 | + * Waits for the thread identified by {@code threadId} to terminate within the given |
| 148 | + * timeout. This call is executed in a separate {@link Context} to avoid affecting the |
| 149 | + * currently running test. |
| 150 | + * |
| 151 | + * @return {@code true} if the thread terminated within the timeout, {@code false} otherwise |
| 152 | + */ |
| 153 | + static boolean doWait(long threadId, Duration timeOut, Engine engine) { |
| 154 | + try (Context context = Context.newBuilder().engine(engine).build()) { |
| 155 | + return AbstractExecutableTestLanguage.evalTestLanguage(context, WaitForThreadTermination.class, "", threadId, timeOut.toMillis()).asBoolean(); |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + |
109 | 160 | @Test
|
110 | 161 | public void testDeadThreadDisposed() throws InterruptedException {
|
111 | 162 | /*
|
112 | 163 | * Context pre-initialization enters the context on the main thread, but we need the first
|
113 | 164 | * enter on the main thread to occur after the separate thread is joined, so that it causes
|
114 | 165 | * the separate thread's disposal.
|
115 | 166 | */
|
116 |
| - try (Context context = Context.newBuilder().allowExperimentalOptions(true).option("engine.UsePreInitializedContext", "false").build()) { |
117 |
| - AtomicLong threadId = new AtomicLong(); |
118 |
| - Thread t = new Thread(() -> threadId.set(AbstractExecutableTestLanguage.evalTestLanguage(context, DeadThreadDisposedTestLanguage.class, "").asLong())); |
119 |
| - t.start(); |
120 |
| - t.join(); |
121 |
| - Assert.assertFalse(t.isAlive()); |
122 |
| - long mainThreadId = AbstractExecutableTestLanguage.evalTestLanguage(context, DeadThreadDisposedTestLanguage.class, "").asLong(); |
123 |
| - Assert.assertEquals(threadId.get() + "," + mainThreadId, context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("initialized").asString()); |
124 |
| - Assert.assertEquals(String.valueOf(threadId.get()), context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("finalized").asString()); |
125 |
| - Assert.assertEquals(String.valueOf(threadId.get()), context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("disposed").asString()); |
| 167 | + try (Engine engine = Engine.newBuilder().allowExperimentalOptions(true).option("engine.UsePreInitializedContext", "false").build()) { |
| 168 | + try (Context context = Context.newBuilder().engine(engine).build()) { |
| 169 | + AtomicLong threadId = new AtomicLong(); |
| 170 | + Thread t = new Thread(() -> threadId.set(AbstractExecutableTestLanguage.evalTestLanguage(context, DeadThreadDisposedTestLanguage.class, "").asLong())); |
| 171 | + t.start(); |
| 172 | + t.join(); |
| 173 | + Assert.assertFalse(t.isAlive()); |
| 174 | + Assert.assertTrue(WaitForThreadTermination.doWait(threadId.get(), Duration.ofSeconds(10), engine)); |
| 175 | + long mainThreadId = AbstractExecutableTestLanguage.evalTestLanguage(context, DeadThreadDisposedTestLanguage.class, "").asLong(); |
| 176 | + Assert.assertEquals(threadId.get() + "," + mainThreadId, context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("initialized").asString()); |
| 177 | + Assert.assertEquals(String.valueOf(threadId.get()), context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("finalized").asString()); |
| 178 | + Assert.assertEquals(String.valueOf(threadId.get()), context.getBindings(DeadThreadDisposedTestLanguage.ID).getMember("disposed").asString()); |
| 179 | + } |
126 | 180 | }
|
127 | 181 | }
|
128 | 182 | }
|
0 commit comments