Skip to content

Commit 49be63a

Browse files
zdevitofacebook-github-bot
authored andcommitted
Improve Python Stack retention (#213)
Summary: Pull Request resolved: #213 This makes SerializedPyError match the exact formatting of python exceptions by using TracebackException to format it. It also ensures that it prepends any current members of the python stack to the Exceptions traceback before formatting. Normally exceptions are only reported after being completely unwound to the root of the python stack. But in most cases when we are capturing a Python exception it is because we are going to move it across the network. We might not have competely unwound the stack at the point we do the serialization, but this unwound stack is still potentially useful. So we also include these frames by adding them to the traceback object. This also updates two places in PythonActor where exceptions would lose stack traces by going from PyErr -> anyhow::Error. By changing the result type to Result<_, SerialiablePyErr> we force it to capture the stack trace. Reviewed By: suo, mariusae Differential Revision: D76316820 fbshipit-source-id: 8093ea29af4061fa90d484d31088f40be4a8cc2a
1 parent 7d62658 commit 49be63a

File tree

2 files changed

+56
-23
lines changed

2 files changed

+56
-23
lines changed

monarch_hyperactor/src/actor.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use hyperactor::message::IndexedErasedUnbound;
2525
use hyperactor::message::Unbind;
2626
use hyperactor_mesh::actor_mesh::Cast;
2727
use monarch_types::PickledPyObject;
28+
use monarch_types::SerializablePyErr;
2829
use pyo3::exceptions::PyBaseException;
2930
use pyo3::exceptions::PyRuntimeError;
3031
use pyo3::prelude::*;
@@ -276,7 +277,7 @@ impl Actor for PythonActor {
276277
type Params = PickledPyObject;
277278

278279
async fn new(actor_type: PickledPyObject) -> Result<Self, anyhow::Error> {
279-
Ok(Python::with_gil(|py| -> PyResult<Self> {
280+
Ok(Python::with_gil(|py| -> Result<Self, SerializablePyErr> {
280281
let unpickled = actor_type.unpickle(py)?;
281282
let class_type: &Bound<'_, PyType> = unpickled.downcast()?;
282283
let actor: PyObject = class_type.call0()?.to_object(py);
@@ -411,7 +412,7 @@ impl Handler<PythonMessage> for PythonActor {
411412
// See [Panics in async endpoints].
412413
let (sender, receiver) = oneshot::channel();
413414

414-
let future = Python::with_gil(|py| -> PyResult<_> {
415+
let future = Python::with_gil(|py| -> Result<_, SerializablePyErr> {
415416
let mailbox = PyMailbox {
416417
inner: this.mailbox_for_py().clone(),
417418
};
@@ -438,6 +439,7 @@ impl Handler<PythonMessage> for PythonActor {
438439
awaitable.into_bound(py),
439440
)
440441
.map(Some)
442+
.map_err(|err| err.into())
441443
})?;
442444

443445
if let Some(future) = future {

monarch_types/src/python.rs

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use pyo3::ToPyObject;
1616
use pyo3::prelude::*;
1717
use pyo3::types::PyDict;
1818
use pyo3::types::PyList;
19+
use pyo3::types::PyNone;
1920
use pyo3::types::PyTuple;
2021
use serde::Deserialize;
2122
use serde::Serialize;
@@ -110,39 +111,68 @@ where
110111
/// A wrapper around `PyErr` that contains a serialized traceback.
111112
#[derive(Debug, Clone, Serialize, Deserialize, derive_more::Error)]
112113
pub struct SerializablePyErr {
113-
pub etype: String,
114-
pub value: String,
115-
pub traceback: Option<Result<String, String>>,
114+
pub message: String,
116115
}
117116

118117
impl SerializablePyErr {
119118
pub fn from(py: Python, err: &PyErr) -> Self {
120-
let etype = format!("{}", err.get_type_bound(py));
121-
let value = format!("{}", err.value_bound(py));
122-
let traceback = err
123-
.traceback_bound(py)
124-
.map(|tb| tb.format().map_err(|e| format!("{}", e)));
125-
Self {
126-
etype,
127-
value,
128-
traceback,
119+
// first construct the full traceback including any python frames that were used
120+
// to invoke where we currently are. This is pre-pended to the traceback of the
121+
// currently unwinded frames (err.traceback_bound())
122+
let inspect = py.import_bound("inspect").unwrap();
123+
let types = py.import_bound("types").unwrap();
124+
let traceback_type = types.getattr("TracebackType").unwrap();
125+
let traceback = py.import_bound("traceback").unwrap();
126+
127+
let mut f = inspect
128+
.call_method0("currentframe")
129+
.unwrap_or(PyNone::get_bound(py).to_owned().into_any());
130+
let mut tb: Bound<'_, PyAny> = err.traceback_bound(py).unwrap().as_any().clone();
131+
while !f.is_none() {
132+
let lasti = f.getattr("f_lasti").unwrap();
133+
let lineno = f.getattr("f_lineno").unwrap();
134+
let back = f.getattr("f_back").unwrap();
135+
tb = traceback_type.call1((tb, f, lasti, lineno)).unwrap();
136+
f = back;
129137
}
138+
139+
let traceback_exception = traceback.getattr("TracebackException").unwrap();
140+
141+
let tb = traceback_exception
142+
.call1((err.get_type_bound(py), err.value_bound(py), tb))
143+
.unwrap();
144+
145+
let message: String = tb
146+
.getattr("format")
147+
.unwrap()
148+
.call0()
149+
.unwrap()
150+
.iter()
151+
.unwrap()
152+
.map(|x| -> String { x.unwrap().extract().unwrap() })
153+
.collect::<Vec<String>>()
154+
.join("");
155+
156+
Self { message }
130157
}
131158

132-
pub fn from_fn<'py>(py: Python<'py>) -> impl Fn(PyErr) -> Self + use<'py> {
159+
pub fn from_fn<'py>(py: Python<'py>) -> impl Fn(PyErr) -> Self + 'py {
133160
move |err| Self::from(py, &err)
134161
}
135162
}
136163

137164
impl std::fmt::Display for SerializablePyErr {
138165
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139-
if let Some(tb_res) = &self.traceback {
140-
match tb_res {
141-
Ok(tb) => write!(f, "{}", tb)?,
142-
Err(err) => write!(f, "Failed to extract traceback: {}", err)?,
143-
}
144-
}
145-
write!(f, "{}: {}", self.etype, self.value)
166+
write!(f, "{}", self.message)
167+
}
168+
}
169+
170+
impl<T> From<T> for SerializablePyErr
171+
where
172+
T: Into<PyErr>,
173+
{
174+
fn from(value: T) -> Self {
175+
Python::with_gil(|py| SerializablePyErr::from(py, &value.into()))
146176
}
147177
}
148178

@@ -177,12 +207,13 @@ mod tests {
177207

178208
let err = SerializablePyErr::from(py, &module.call_method0("func3").unwrap_err());
179209
assert_eq!(
180-
err.traceback.unwrap().unwrap().as_str(),
210+
err.message.as_str(),
181211
indoc! {r#"
182212
Traceback (most recent call last):
183213
File "test_helpers.py", line 8, in func3
184214
File "test_helpers.py", line 5, in func2
185215
File "test_helpers.py", line 2, in func1
216+
Exception: test
186217
"#}
187218
);
188219

0 commit comments

Comments
 (0)