Skip to content

Commit f22d8c8

Browse files
committed
Poll TruffleSafepoints in all polyglot threads
1 parent 209e2ef commit f22d8c8

File tree

12 files changed

+561
-129
lines changed

12 files changed

+561
-129
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright (c) 2017, 2024, 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.graal.python.test.integration.advanced;
42+
43+
import org.graalvm.polyglot.Context;
44+
import org.graalvm.polyglot.PolyglotException;
45+
import org.junit.Assert;
46+
import org.junit.Test;
47+
48+
import com.oracle.graal.python.test.integration.PythonTests;
49+
50+
import java.util.concurrent.CountDownLatch;
51+
import java.util.concurrent.TimeUnit;
52+
import java.util.concurrent.atomic.AtomicReference;
53+
54+
public class ShutdownTest extends PythonTests {
55+
@Test
56+
public void testCloseWithBackgroundThreadsRunningSucceeds() {
57+
Context context = Context.newBuilder().allowExperimentalOptions(true).allowAllAccess(true).build();
58+
try {
59+
loadNativeExtension(context);
60+
asyncStartPythonThreadsThatSleep(context);
61+
} finally {
62+
context.close(true);
63+
}
64+
}
65+
66+
@Test
67+
public void testCloseFromAnotherThreadThrowsCancelledEx() {
68+
Context context = Context.newBuilder().allowExperimentalOptions(true).allowAllAccess(true).build();
69+
PolyglotException thrownEx = null;
70+
try {
71+
loadNativeExtension(context);
72+
asyncStartPythonThreadsThatSleep(context);
73+
CountDownLatch latch = new CountDownLatch(1);
74+
new Thread(() -> {
75+
try {
76+
latch.await();
77+
Thread.sleep(10);
78+
} catch (InterruptedException e) {
79+
throw new RuntimeException(e);
80+
}
81+
// close from another thread
82+
context.close(true);
83+
}).start();
84+
countDownLatchAndSleepInPython(context, latch);
85+
} catch (PolyglotException ex) {
86+
thrownEx = ex;
87+
} finally {
88+
context.close(true);
89+
Assert.assertNotNull("PolyglotException was not thrown upon cancellation ", thrownEx);
90+
Assert.assertTrue(thrownEx.toString(), thrownEx.isCancelled());
91+
}
92+
}
93+
94+
@Test
95+
public void testJavaThreadGetsCancelledException() throws InterruptedException {
96+
Context context = Context.newBuilder().allowExperimentalOptions(true).allowAllAccess(true).build();
97+
AtomicReference<PolyglotException> thrownEx = new AtomicReference<>();
98+
CountDownLatch gotException = new CountDownLatch(1);
99+
try {
100+
asyncStartPythonThreadsThatSleep(context);
101+
CountDownLatch latch = new CountDownLatch(1);
102+
new Thread(() -> {
103+
try {
104+
// this thread will get stuck sleeping in python
105+
countDownLatchAndSleepInPython(context, latch);
106+
} catch (PolyglotException ex) {
107+
thrownEx.set(ex);
108+
gotException.countDown();
109+
}
110+
}).start();
111+
try {
112+
loadNativeExtension(context);
113+
latch.await();
114+
Thread.sleep(10); // make sure the other thread really starts sleeping
115+
} catch (InterruptedException e) {
116+
throw new RuntimeException(e);
117+
}
118+
} finally {
119+
context.close(true);
120+
}
121+
Assert.assertTrue("other thread did not get any exception in time limit", gotException.await(2, TimeUnit.SECONDS));
122+
Assert.assertNotNull("PolyglotException was not thrown upon cancellation ", thrownEx.get());
123+
Assert.assertTrue(thrownEx.toString(), thrownEx.get().isCancelled());
124+
}
125+
126+
@Test
127+
public void testJavaThreadNotExecutingPythonAnymore() throws InterruptedException {
128+
Context context = Context.newBuilder().allowExperimentalOptions(true).allowAllAccess(true).build();
129+
var javaThreadDone = new CountDownLatch(1);
130+
var javaThreadCanEnd = new CountDownLatch(1);
131+
var javaThread = new Thread(() -> {
132+
Assert.assertEquals(42, context.eval("python", "42").asInt());
133+
javaThreadDone.countDown();
134+
try {
135+
javaThreadCanEnd.await();
136+
} catch (InterruptedException e) {
137+
throw new RuntimeException(e);
138+
}
139+
});
140+
AtomicReference<Throwable> uncaughtEx = new AtomicReference<>();
141+
javaThread.setUncaughtExceptionHandler((t, e) -> uncaughtEx.set(e));
142+
try {
143+
javaThread.start();
144+
loadNativeExtension(context);
145+
javaThreadDone.await();
146+
} finally {
147+
// we can close although the Java thread is still running
148+
context.close(true);
149+
}
150+
javaThreadCanEnd.countDown();
151+
javaThread.join();
152+
Assert.assertNull(uncaughtEx.get());
153+
}
154+
155+
private static void loadNativeExtension(Context context) {
156+
context.eval("python", "import _cpython_sre\nassert _cpython_sre.ascii_tolower(88) == 120\n");
157+
}
158+
159+
private static void asyncStartPythonThreadsThatSleep(Context context) {
160+
for (int i = 0; i < 10; i++) {
161+
context.eval("python", "import threading; import time; threading.Thread(target=lambda: time.sleep(10000)).start()");
162+
}
163+
}
164+
165+
private static void countDownLatchAndSleepInPython(Context context, CountDownLatch latch) {
166+
context.eval("python", "def run(latch): import time; latch.countDown(); time.sleep(100000)");
167+
context.getBindings("python").getMember("run").execute(latch);
168+
}
169+
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,20 @@ private static boolean mimeTypesComplete(ArrayList<String> mimeJavaStrings) {
313313

314314
private static final LanguageReference<PythonLanguage> REFERENCE = LanguageReference.create(PythonLanguage.class);
315315

316+
// Should be removed when GR-59720 - allow null location for safepoint is implemented
317+
@CompilationFinal public RootNode unavailableSafepointLocation;
318+
319+
private static final class DummyRootNode extends RootNode {
320+
protected DummyRootNode(TruffleLanguage<?> language) {
321+
super(language);
322+
}
323+
324+
@Override
325+
public Object execute(VirtualFrame frame) {
326+
return null;
327+
}
328+
}
329+
316330
/**
317331
* This assumption will be valid if no context set a trace or profile function at any point.
318332
* Calling sys.settrace(None) or sys.setprofile(None) will not invalidate it
@@ -534,6 +548,7 @@ protected void initializeContext(PythonContext context) {
534548
private synchronized void initializeLanguage() {
535549
if (!isLanguageInitialized) {
536550
TpSlots.initializeBuiltinSlots(this);
551+
unavailableSafepointLocation = new DummyRootNode(this);
537552
isLanguageInitialized = true;
538553
}
539554
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import com.oracle.graal.python.runtime.exception.PException;
116116
import com.oracle.graal.python.util.Function;
117117
import com.oracle.graal.python.util.PythonUtils;
118+
import com.oracle.graal.python.util.PythonSystemThreadTask;
118119
import com.oracle.graal.python.util.Supplier;
119120
import com.oracle.graal.python.util.SuppressFBWarnings;
120121
import com.oracle.graal.python.util.WeakIdentityHashMap;
@@ -613,9 +614,10 @@ public void untrackObject(Object ptr, PFrame.Reference curFrame, TruffleString c
613614
// TODO(fa): implement untracking of container objects
614615
}
615616

616-
private static final class BackgroundGCTask implements Runnable {
617+
private static final class BackgroundGCTask extends PythonSystemThreadTask {
617618

618619
private BackgroundGCTask(PythonContext context) {
620+
super("Python GC", LOGGER);
619621
this.ctx = new WeakReference<>(context);
620622
this.rssInterval = context.getOption(PythonOptions.BackgroundGCTaskInterval);
621623
this.gcRSSThreshold = context.getOption(PythonOptions.BackgroundGCTaskThreshold) / (double) 100;
@@ -667,15 +669,23 @@ Long getCurrentRSS() {
667669
}
668670

669671
@Override
670-
public void run() {
671-
try {
672-
while (true) {
673-
Thread.sleep(rssInterval);
674-
perform();
675-
}
676-
} catch (InterruptedException e) {
677-
Thread.currentThread().interrupt();
672+
protected void doRun() {
673+
Node location = getSafepointLocation();
674+
if (location == null) {
675+
return;
676+
}
677+
while (true) {
678+
TruffleSafepoint.setBlockedThreadInterruptible(location, Thread::sleep, rssInterval);
679+
perform();
680+
}
681+
}
682+
683+
private Node getSafepointLocation() {
684+
PythonContext context = ctx.get();
685+
if (context == null) {
686+
return null;
678687
}
688+
return context.getLanguage().unavailableSafepointLocation;
679689
}
680690

681691
private void perform() {
@@ -764,7 +774,7 @@ void runBackgroundGCTask(PythonContext context) {
764774
|| !context.getOption(PythonOptions.BackgroundGCTask)) {
765775
return;
766776
}
767-
backgroundGCTaskThread = context.getEnv().newTruffleThreadBuilder(gcTask).context(context.getEnv().getContext()).build();
777+
backgroundGCTaskThread = context.createSystemThread(gcTask);
768778
backgroundGCTaskThread.setDaemon(true);
769779
backgroundGCTaskThread.setName("python-gc-task");
770780
backgroundGCTaskThread.start();
@@ -865,7 +875,7 @@ public static CApiContext ensureCapiWasLoaded(Node node, PythonContext context,
865875
Object finalizeSignature = env.parseInternal(Source.newBuilder(J_NFI_LANGUAGE, "():POINTER", "exec").build()).call();
866876
Object finalizingPointer = SignatureLibrary.getUncached().call(finalizeSignature, finalizeFunction);
867877
try {
868-
cApiContext.addNativeFinalizer(env, finalizingPointer);
878+
cApiContext.addNativeFinalizer(context, finalizingPointer);
869879
cApiContext.runBackgroundGCTask(context);
870880
} catch (RuntimeException e) {
871881
// This can happen when other languages restrict multithreading
@@ -898,14 +908,15 @@ public static CApiContext ensureCapiWasLoaded(Node node, PythonContext context,
898908
* We need to do it in a VM shutdown hook to make sure C atexit won't crash even if our context
899909
* finalization didn't run.
900910
*/
901-
private void addNativeFinalizer(Env env, Object finalizingPointerObj) {
902-
final Unsafe unsafe = getContext().getUnsafe();
911+
private void addNativeFinalizer(PythonContext context, Object finalizingPointerObj) {
912+
final Unsafe unsafe = context.getUnsafe();
903913
InteropLibrary lib = InteropLibrary.getUncached(finalizingPointerObj);
904914
if (!lib.isNull(finalizingPointerObj) && lib.isPointer(finalizingPointerObj)) {
905915
try {
906916
long finalizingPointer = lib.asPointer(finalizingPointerObj);
907-
nativeFinalizerRunnable = () -> unsafe.putInt(finalizingPointer, 1);
908-
nativeFinalizerShutdownHook = env.newTruffleThreadBuilder(nativeFinalizerRunnable).build();
917+
// We are writing off heap memory and registering a VM shutdown hook, there is no
918+
// point in creating this thread via Truffle sandbox at this point
919+
nativeFinalizerShutdownHook = new Thread(() -> unsafe.putInt(finalizingPointer, 1));
909920
Runtime.getRuntime().addShutdownHook(nativeFinalizerShutdownHook);
910921
} catch (UnsupportedMessageException e) {
911922
throw new RuntimeException(e);

0 commit comments

Comments
 (0)