Skip to content

Commit c7ff9b3

Browse files
committed
[GR-65684] Invariant contract violation for receiver.
PullRequest: graal/21046
2 parents 61f3a21 + 163ae1c commit c7ff9b3

File tree

2 files changed

+137
-10
lines changed

2 files changed

+137
-10
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.truffle.api.test;
42+
43+
import java.util.Arrays;
44+
45+
/**
46+
* Utility class providing common thread-related helpers for use in unit tests.
47+
*/
48+
public final class ThreadUtils {
49+
50+
private ThreadUtils() {
51+
}
52+
53+
/**
54+
* Returns an array containing all threads that were alive at the time of the call.
55+
* <p>
56+
* Note that some threads in the returned array may terminate immediately after being captured,
57+
* so it is possible that they are no longer alive by the time the array is iterated.
58+
*/
59+
public static Thread[] getAllThreads() {
60+
ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
61+
ThreadGroup parentGroup;
62+
while ((parentGroup = rootGroup.getParent()) != null) {
63+
rootGroup = parentGroup;
64+
}
65+
66+
Thread[] threads = new Thread[rootGroup.activeCount()];
67+
int cnt;
68+
while ((cnt = rootGroup.enumerate(threads, true)) == threads.length) {
69+
threads = new Thread[threads.length * 2];
70+
}
71+
return Arrays.copyOf(threads, cnt);
72+
}
73+
}

truffle/src/com.oracle.truffle.api.test/src/com/oracle/truffle/api/test/polyglot/ThreadInitializationAndDisposalTest.java

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@
4040
*/
4141
package com.oracle.truffle.api.test.polyglot;
4242

43+
import java.time.Duration;
4344
import java.util.concurrent.atomic.AtomicLong;
4445

46+
import com.oracle.truffle.api.test.ThreadUtils;
4547
import org.graalvm.polyglot.Context;
48+
import org.graalvm.polyglot.Engine;
4649
import org.junit.Assert;
4750
import org.junit.Test;
4851

@@ -106,23 +109,74 @@ protected void disposeThread(ExecutableContext context, Thread thread) {
106109
}
107110
}
108111

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+
109160
@Test
110161
public void testDeadThreadDisposed() throws InterruptedException {
111162
/*
112163
* Context pre-initialization enters the context on the main thread, but we need the first
113164
* enter on the main thread to occur after the separate thread is joined, so that it causes
114165
* the separate thread's disposal.
115166
*/
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+
}
126180
}
127181
}
128182
}

0 commit comments

Comments
 (0)