Skip to content

Commit 086558b

Browse files
committed
[GR-60092] Implement PyThreadState_SetAsyncExc.
PullRequest: graalpython/3589
2 parents efde0fb + 0ad6f3e commit 086558b

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_pystate.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,47 @@ def test_PyThreadState_GetFrame(self):
6868
tp_methods='{"get_frame", (PyCFunction)get_frame, METH_NOARGS | METH_STATIC, NULL}',
6969
)
7070
assert Tester.get_frame() is sys._getframe(0)
71+
72+
# This seems to get the native extensions into some inconsistent state on GraalPy, giving:
73+
# refcnt below zero during managed adjustment for 0000aaae18fca780 (9 0000000000000009 - 10)
74+
def test_SetAsyncExc(self):
75+
SetAsyncExcCaller = CPyExtType(
76+
"SetAsyncExcCaller",
77+
"""
78+
static PyObject* trigger_ex(PyObject *cls, PyObject *args) {
79+
long thread_id;
80+
PyObject *ex;
81+
if (!PyArg_ParseTuple(args, "lO", &thread_id, &ex)) {
82+
return NULL;
83+
}
84+
PyThreadState_SetAsyncExc(thread_id, ex);
85+
return PyLong_FromLong(42);
86+
}
87+
""",
88+
tp_methods='{"trigger_ex", (PyCFunction)trigger_ex, METH_VARARGS | METH_STATIC, ""}',
89+
)
90+
91+
import threading
92+
start = threading.Barrier(2, timeout=20)
93+
94+
caught_ex = None
95+
def other_thread():
96+
try:
97+
start.wait() # ensure we are in the try, before raising
98+
r = 0
99+
for i in range(1, 1000000000):
100+
for j in range(i, 1000000000):
101+
r += j / i
102+
except Exception as e:
103+
nonlocal caught_ex
104+
caught_ex = e
105+
106+
107+
t = threading.Thread(target=other_thread)
108+
t.start()
109+
110+
start.wait()
111+
SetAsyncExcCaller.trigger_ex(t.ident, Exception("test my message"))
112+
t.join()
113+
114+
assert "test my message" in str(caught_ex)

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,27 @@
4545
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Int;
4646
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Pointer;
4747
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyFrameObjectTransfer;
48+
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObject;
4849
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyObjectBorrowed;
4950
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.PyThreadState;
5051
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Py_ssize_t;
5152
import static com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor.Void;
5253

54+
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBinaryBuiltinNode;
5355
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiBuiltin;
5456
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiNullaryBuiltinNode;
5557
import com.oracle.graal.python.builtins.modules.cext.PythonCextBuiltins.CApiUnaryBuiltinNode;
5658
import com.oracle.graal.python.builtins.objects.PNone;
59+
import com.oracle.graal.python.builtins.objects.cext.capi.CApiContext;
5760
import com.oracle.graal.python.builtins.objects.cext.capi.PThreadState;
61+
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.ArgDescriptor;
5862
import com.oracle.graal.python.builtins.objects.dict.PDict;
5963
import com.oracle.graal.python.builtins.objects.frame.PFrame;
6064
import com.oracle.graal.python.builtins.objects.ints.PInt;
65+
import com.oracle.graal.python.builtins.objects.thread.PThread;
66+
import com.oracle.graal.python.nodes.PGuards;
67+
import com.oracle.graal.python.nodes.PRaiseNode;
68+
import com.oracle.graal.python.nodes.PRootNode;
6169
import com.oracle.graal.python.nodes.frame.GetCurrentFrameRef;
6270
import com.oracle.graal.python.nodes.frame.ReadCallerFrameNode;
6371
import com.oracle.graal.python.nodes.util.CannotCastException;
@@ -67,12 +75,15 @@
6775
import com.oracle.graal.python.runtime.object.PythonObjectFactory;
6876
import com.oracle.graal.python.util.OverflowException;
6977
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
78+
import com.oracle.truffle.api.ThreadLocalAction;
79+
import com.oracle.truffle.api.TruffleLogger;
7080
import com.oracle.truffle.api.dsl.Bind;
7181
import com.oracle.truffle.api.dsl.Cached;
7282
import com.oracle.truffle.api.dsl.Specialization;
7383
import com.oracle.truffle.api.interop.InteropLibrary;
7484
import com.oracle.truffle.api.library.CachedLibrary;
7585
import com.oracle.truffle.api.nodes.Node;
86+
import com.oracle.truffle.api.nodes.RootNode;
7687

7788
public final class PythonCextPyStateBuiltins {
7889

@@ -137,6 +148,58 @@ PDict get(@Cached PythonObjectFactory factory) {
137148
}
138149
}
139150

151+
@CApiBuiltin(ret = Int, args = {ArgDescriptor.UNSIGNED_LONG, PyObject}, call = Direct)
152+
abstract static class PyThreadState_SetAsyncExc extends CApiBinaryBuiltinNode {
153+
public static final TruffleLogger LOGGER = CApiContext.getLogger(PyThreadState_SetAsyncExc.class);
154+
155+
@Specialization
156+
@TruffleBoundary
157+
int doIt(long id, Object exceptionObject) {
158+
for (Thread thread : getContext().getThreads()) {
159+
if (PThread.getThreadId(thread) == id) {
160+
if (PGuards.isNoValue(exceptionObject)) {
161+
LOGGER.warning("The application used PyThreadState_SetAsyncExc to clear an exception on another thread. " +
162+
"This is not supported and ignored by GraalPy.");
163+
return 1;
164+
}
165+
ThreadLocalAction action = new ThreadLocalAction(true, false) {
166+
static final int MAX_MISSED_COUNT = 20;
167+
int missedCount = 0;
168+
169+
@Override
170+
protected void perform(Access access) {
171+
if (missedCount == MAX_MISSED_COUNT) {
172+
throw PRaiseNode.raiseExceptionObject(null, exceptionObject);
173+
}
174+
// If possible, we do not want to raise in some internal code, it could
175+
// corrupt internal data structures.
176+
Node location = access.getLocation();
177+
if (location != null) {
178+
RootNode rootNode = location.getRootNode();
179+
if (rootNode instanceof PRootNode && !rootNode.isInternal()) {
180+
throw PRaiseNode.raiseExceptionObject(null, exceptionObject);
181+
}
182+
}
183+
// Heuristic fabricated out of thin air:
184+
if (missedCount++ < MAX_MISSED_COUNT) {
185+
if (missedCount % 2 == 0) {
186+
try {
187+
Thread.sleep(1);
188+
} catch (InterruptedException ignored) {
189+
}
190+
}
191+
getContext().getEnv().submitThreadLocal(new Thread[]{thread}, this);
192+
}
193+
}
194+
};
195+
getContext().getEnv().submitThreadLocal(new Thread[]{thread}, action);
196+
return 1;
197+
}
198+
}
199+
return 0;
200+
}
201+
}
202+
140203
@CApiBuiltin(ret = PyFrameObjectTransfer, args = {PyThreadState}, call = Direct)
141204
abstract static class PyThreadState_GetFrame extends CApiUnaryBuiltinNode {
142205
@Specialization

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,6 @@ public final class CApiFunction {
879879
@CApiBuiltin(name = "PyThreadState_LeaveTracing", ret = Void, args = {PyThreadState}, call = NotImplemented)
880880
@CApiBuiltin(name = "PyThreadState_New", ret = PyThreadState, args = {PyInterpreterState}, call = NotImplemented)
881881
@CApiBuiltin(name = "PyThreadState_Next", ret = PyThreadState, args = {PyThreadState}, call = NotImplemented)
882-
@CApiBuiltin(name = "PyThreadState_SetAsyncExc", ret = Int, args = {UNSIGNED_LONG, PyObject}, call = NotImplemented)
883882
@CApiBuiltin(name = "PyThreadState_Swap", ret = PyThreadState, args = {PyThreadState}, call = NotImplemented)
884883
@CApiBuiltin(name = "PyThread_GetInfo", ret = PyObject, args = {}, call = NotImplemented)
885884
@CApiBuiltin(name = "PyThread_ReInitTLS", ret = Void, args = {}, call = NotImplemented)

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/AsyncHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -363,7 +363,7 @@ public void run() {
363363
* Some other thread will run and eventually get another gil release
364364
* request.
365365
*/
366-
ctx.getEnv().submitThreadLocal(new Thread[]{gilOwner}, new ThreadLocalAction(false, false) {
366+
ctx.getEnv().submitThreadLocal(new Thread[]{gilOwner}, new ThreadLocalAction(true, false) {
367367
@Override
368368
protected void perform(ThreadLocalAction.Access access) {
369369
// it may happen that we request a GIL release and no thread is

0 commit comments

Comments
 (0)