Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-41435: Add sys._current_exceptions() function #21689

Merged
merged 1 commit into from
Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
bpo-41435: Add sys._current_exceptions() function
This adds a new function named sys._current_exceptions() which is equivalent ot
sys._current_frames() except that it returns the exceptions currently handled
by other threads. It is equivalent to calling sys.exc_info() for each running
thread.
  • Loading branch information
jd committed Oct 14, 2020
commit 0726d17353201828c16e06d0de41603ae0a90f90
12 changes: 12 additions & 0 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ always available.

.. audit-event:: sys._current_frames "" sys._current_frames

.. function:: _current_exceptions()

Return a dictionary mapping each thread's identifier to the topmost exception
currently active in that thread at the time the function is called.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear that threads with no exception are omitted in this dictionary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

If a thread is not currently handling an exception, it is not included in
the result dictionary.

This is most useful for statistical profiling.

This function should be used for internal and specialized purposes only.

.. audit-event:: sys._current_exceptions "" sys._current_exceptions

.. function:: breakpointhook()

Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ PyAPI_FUNC(PyInterpreterState *) _PyGILState_GetInterpreterStateUnsafe(void);
*/
PyAPI_FUNC(PyObject *) _PyThread_CurrentFrames(void);

/* The implementation of sys._current_exceptions() Returns a dict mapping
thread id to that thread's current exception.
*/
PyAPI_FUNC(PyObject *) _PyThread_CurrentExceptions(void);

/* Routines for advanced debuggers, requested by David Beazley.
Don't use unless you know what you are doing! */
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);
Expand Down
67 changes: 67 additions & 0 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,73 @@ def g456():
leave_g.set()
t.join()

@threading_helper.reap_threads
def test_current_exceptions(self):
import threading
import traceback

# Spawn a thread that blocks at a known place. Then the main
# thread does sys._current_frames(), and verifies that the frames
# returned make sense.
entered_g = threading.Event()
leave_g = threading.Event()
thread_info = [] # the thread's id

def f123():
g456()

def g456():
thread_info.append(threading.get_ident())
entered_g.set()
while True:
try:
raise ValueError("oops")
except ValueError:
if leave_g.wait(timeout=support.LONG_TIMEOUT):
break

t = threading.Thread(target=f123)
t.start()
entered_g.wait()

# At this point, t has finished its entered_g.set(), although it's
# impossible to guess whether it's still on that line or has moved on
# to its leave_g.wait().
self.assertEqual(len(thread_info), 1)
thread_id = thread_info[0]

d = sys._current_exceptions()
for tid in d:
self.assertIsInstance(tid, int)
self.assertGreater(tid, 0)

main_id = threading.get_ident()
self.assertIn(main_id, d)
self.assertIn(thread_id, d)
self.assertEqual((None, None, None), d.pop(main_id))

# Verify that the captured thread frame is blocked in g456, called
# from f123. This is a litte tricky, since various bits of
# threading.py are also in the thread's call stack.
exc_type, exc_value, exc_tb = d.pop(thread_id)
stack = traceback.extract_stack(exc_tb.tb_frame)
for i, (filename, lineno, funcname, sourceline) in enumerate(stack):
if funcname == "f123":
break
else:
self.fail("didn't find f123() on thread's call stack")

self.assertEqual(sourceline, "g456()")

# And the next record must be for g456().
filename, lineno, funcname, sourceline = stack[i+1]
self.assertEqual(funcname, "g456")
self.assertTrue(sourceline.startswith("if leave_g.wait("))

# Reap the spawned thread.
leave_g.set()
t.join()

def test_attributes(self):
self.assertIsInstance(sys.api_version, int)
self.assertIsInstance(sys.argv, list)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `sys._current_exceptions()` function to retrieve a dictionary mapping each thread's identifier to the topmost exception currently active in that thread at the time the function is called.
22 changes: 21 additions & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,69 @@ _PyThread_CurrentFrames(void)
return result;
}

PyObject *
_PyThread_CurrentExceptions(void)
{
PyThreadState *tstate = _PyThreadState_GET();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to ensure that the GIL is held. For example, call _Py_EnsureTstateNotNULL(tstate).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


_Py_EnsureTstateNotNULL(tstate);

if (_PySys_Audit(tstate, "sys._current_exceptions", NULL) < 0) {
return NULL;
}

PyObject *result = PyDict_New();
if (result == NULL) {
return NULL;
}

/* for i in all interpreters:
* for t in all of i's thread states:
* if t's frame isn't NULL, map t's id to its frame
* Because these lists can mutate even when the GIL is held, we
* need to grab head_mutex for the duration.
*/
_PyRuntimeState *runtime = tstate->interp->runtime;
HEAD_LOCK(runtime);
PyInterpreterState *i;
for (i = runtime->interpreters.head; i != NULL; i = i->next) {
PyThreadState *t;
for (t = i->tstate_head; t != NULL; t = t->next) {
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
if (err_info == NULL) {
continue;
}
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single thread can be associated to more than one Python thread state. It is possible that two interpreters run in the same thread, just not at the same time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a limitation in _current_frames then, right?
I think it'd be fine in this case to mimic what _current_frames does in this regard.

if (id == NULL) {
goto fail;
}
PyObject *exc_info = PyTuple_Pack(
3,
err_info->exc_type != NULL ? err_info->exc_type : Py_None,
err_info->exc_value != NULL ? err_info->exc_value : Py_None,
err_info->exc_traceback != NULL ? err_info->exc_traceback : Py_None);
if (exc_info == NULL) {
Py_DECREF(id);
goto fail;
}
int stat = PyDict_SetItem(result, id, exc_info);
jd marked this conversation as resolved.
Show resolved Hide resolved
Py_DECREF(id);
Py_DECREF(exc_info);
if (stat < 0) {
goto fail;
}
}
}
goto done;

fail:
Py_CLEAR(result);

done:
HEAD_UNLOCK(runtime);
return result;
}

/* Python "auto thread state" API. */

/* Keep this as a static, as it is not reliable! It can only
Expand Down
16 changes: 16 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,21 @@ sys__current_frames_impl(PyObject *module)
return _PyThread_CurrentFrames();
}

/*[clinic input]
sys._current_exceptions

Return a dict mapping each thread's identifier to its current raised exception.

This function should be used for specialized purposes only.
[clinic start generated code]*/

static PyObject *
sys__current_exceptions_impl(PyObject *module)
/*[clinic end generated code: output=2ccfd838c746f0ba input=0e91818fbf2edc1f]*/
{
return _PyThread_CurrentExceptions();
}

/*[clinic input]
sys.call_tracing

Expand Down Expand Up @@ -1939,6 +1954,7 @@ static PyMethodDef sys_methods[] = {
METH_FASTCALL | METH_KEYWORDS, breakpointhook_doc},
SYS__CLEAR_TYPE_CACHE_METHODDEF
SYS__CURRENT_FRAMES_METHODDEF
SYS__CURRENT_EXCEPTIONS_METHODDEF
SYS_DISPLAYHOOK_METHODDEF
SYS_EXC_INFO_METHODDEF
SYS_EXCEPTHOOK_METHODDEF
Expand Down