You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

python.rs 15 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #![allow(clippy::borrow_deref_ref)] // clippy warns about code generated by #[pymethods]
  2. use super::{OperatorEvent, StopReason};
  3. use dora_core::{
  4. config::{NodeId, OperatorId},
  5. descriptor::{source_is_url, Descriptor, PythonSource},
  6. };
  7. use dora_download::download_file;
  8. use dora_node_api::Event;
  9. use dora_operator_api_python::PyEvent;
  10. use dora_operator_api_types::DoraStatus;
  11. use eyre::{bail, eyre, Context, Result};
  12. use pyo3::{
  13. pyclass,
  14. types::{IntoPyDict, PyDict},
  15. Py, PyAny, Python,
  16. };
  17. use std::{
  18. panic::{catch_unwind, AssertUnwindSafe},
  19. path::Path,
  20. };
  21. use tokio::sync::{mpsc::Sender, oneshot};
  22. use tracing::{error, field, span, warn};
  23. fn traceback(err: pyo3::PyErr) -> eyre::Report {
  24. let traceback = Python::with_gil(|py| err.traceback(py).and_then(|t| t.format().ok()));
  25. if let Some(traceback) = traceback {
  26. eyre::eyre!("{traceback}\n{err}")
  27. } else {
  28. eyre::eyre!("{err}")
  29. }
  30. }
  31. #[tracing::instrument(skip(events_tx, incoming_events), level = "trace")]
  32. pub fn run(
  33. node_id: &NodeId,
  34. operator_id: &OperatorId,
  35. python_source: &PythonSource,
  36. events_tx: Sender<OperatorEvent>,
  37. incoming_events: flume::Receiver<Event>,
  38. init_done: oneshot::Sender<Result<()>>,
  39. dataflow_descriptor: &Descriptor,
  40. ) -> eyre::Result<()> {
  41. let path = if source_is_url(&python_source.source) {
  42. let target_path = Path::new("build")
  43. .join(node_id.to_string())
  44. .join(format!("{}.py", operator_id));
  45. // try to download the shared library
  46. let rt = tokio::runtime::Builder::new_current_thread()
  47. .enable_all()
  48. .build()?;
  49. rt.block_on(download_file(&python_source.source, &target_path))
  50. .wrap_err("failed to download Python operator")?;
  51. target_path
  52. } else {
  53. Path::new(&python_source.source).to_owned()
  54. };
  55. if !path.exists() {
  56. bail!("No python file exists at {}", path.display());
  57. }
  58. let path = path
  59. .canonicalize()
  60. .wrap_err_with(|| format!("no file found at `{}`", path.display()))?;
  61. let module_name = path
  62. .file_stem()
  63. .ok_or_else(|| eyre!("module path has no file stem"))?
  64. .to_str()
  65. .ok_or_else(|| eyre!("module file stem is not valid utf8"))?;
  66. let path_parent = path.parent();
  67. let send_output = SendOutputCallback {
  68. events_tx: events_tx.clone(),
  69. };
  70. let init_operator = move |py: Python| {
  71. if let Some(parent_path) = path_parent {
  72. let parent_path = parent_path
  73. .to_str()
  74. .ok_or_else(|| eyre!("module path is not valid utf8"))?;
  75. let sys = py.import("sys").wrap_err("failed to import `sys` module")?;
  76. let sys_path = sys
  77. .getattr("path")
  78. .wrap_err("failed to import `sys.path` module")?;
  79. let sys_path_append = sys_path
  80. .getattr("append")
  81. .wrap_err("`sys.path.append` was not found")?;
  82. sys_path_append
  83. .call1((parent_path,))
  84. .wrap_err("failed to append module path to python search path")?;
  85. }
  86. let module = py.import(module_name).map_err(traceback)?;
  87. let operator_class = module
  88. .getattr("Operator")
  89. .wrap_err("no `Operator` class found in module")?;
  90. let locals = [("Operator", operator_class)].into_py_dict(py);
  91. let operator = py
  92. .eval("Operator()", None, Some(locals))
  93. .map_err(traceback)?;
  94. operator.setattr(
  95. "dataflow_descriptor",
  96. pythonize::pythonize(py, dataflow_descriptor)?,
  97. )?;
  98. Result::<_, eyre::Report>::Ok(Py::from(operator))
  99. };
  100. let python_runner = move || {
  101. let mut operator =
  102. match Python::with_gil(init_operator).wrap_err("failed to init python operator") {
  103. Ok(op) => {
  104. let _ = init_done.send(Ok(()));
  105. op
  106. }
  107. Err(err) => {
  108. let _ = init_done.send(Err(err));
  109. bail!("Could not init python operator")
  110. }
  111. };
  112. let mut reload = false;
  113. let reason = loop {
  114. #[allow(unused_mut)]
  115. let Ok(mut event) = incoming_events.recv() else {
  116. break StopReason::InputsClosed;
  117. };
  118. if let Event::Reload { .. } = event {
  119. reload = true;
  120. // Reloading method
  121. #[allow(clippy::blocks_in_conditions)]
  122. match Python::with_gil(|py| -> Result<Py<PyAny>> {
  123. // Saving current state
  124. let current_state = operator
  125. .getattr(py, "__dict__")
  126. .wrap_err("Could not retrieve current operator state")?;
  127. let current_state = current_state
  128. .extract::<&PyDict>(py)
  129. .wrap_err("could not extract operator state as a PyDict")?;
  130. // Reload module
  131. let module = py
  132. .import(module_name)
  133. .map_err(traceback)
  134. .wrap_err(format!("Could not retrieve {module_name} while reloading"))?;
  135. let importlib = py
  136. .import("importlib")
  137. .wrap_err("failed to import `importlib` module")?;
  138. let module = importlib
  139. .call_method("reload", (module,), None)
  140. .wrap_err(format!("Could not reload {module_name} while reloading"))?;
  141. let reloaded_operator_class = module
  142. .getattr("Operator")
  143. .wrap_err("no `Operator` class found in module")?;
  144. // Create a new reloaded operator
  145. let locals = [("Operator", reloaded_operator_class)].into_py_dict(py);
  146. let operator: Py<pyo3::PyAny> = py
  147. .eval("Operator()", None, Some(locals))
  148. .map_err(traceback)
  149. .wrap_err("Could not initialize reloaded operator")?
  150. .into();
  151. // Replace initialized state with current state
  152. operator
  153. .getattr(py, "__dict__")
  154. .wrap_err("Could not retrieve new operator state")?
  155. .extract::<&PyDict>(py)
  156. .wrap_err("could not extract new operator state as a PyDict")?
  157. .update(current_state.as_mapping())
  158. .wrap_err("could not restore operator state")?;
  159. Ok(operator)
  160. }) {
  161. Ok(reloaded_operator) => {
  162. operator = reloaded_operator;
  163. }
  164. Err(err) => {
  165. error!("Failed to reload operator.\n {err}");
  166. }
  167. }
  168. }
  169. let status = Python::with_gil(|py| -> Result<i32> {
  170. let span = span!(tracing::Level::TRACE, "on_event", input_id = field::Empty);
  171. let _ = span.enter();
  172. // We need to create a new scoped `GILPool` because the dora-runtime
  173. // is currently started through a `start_runtime` wrapper function,
  174. // which is annotated with `#[pyfunction]`. This attribute creates an
  175. // initial `GILPool` that lasts for the entire lifetime of the `dora-runtime`.
  176. // However, we want the `PyBytes` created below to be freed earlier.
  177. // creating a new scoped `GILPool` tied to this closure, will free `PyBytes`
  178. // at the end of the closure.
  179. // See https://github.com/PyO3/pyo3/pull/2864 and
  180. // https://github.com/PyO3/pyo3/issues/2853 for more details.
  181. let pool = unsafe { py.new_pool() };
  182. let py = pool.python();
  183. // Add metadata context if we have a tracer and
  184. // incoming input has some metadata.
  185. #[cfg(feature = "telemetry")]
  186. if let Event::Input {
  187. id: input_id,
  188. metadata,
  189. ..
  190. } = &mut event
  191. {
  192. use dora_tracing::telemetry::{deserialize_context, serialize_context};
  193. use tracing_opentelemetry::OpenTelemetrySpanExt;
  194. span.record("input_id", input_id.as_str());
  195. let cx = deserialize_context(&metadata.parameters.open_telemetry_context);
  196. span.set_parent(cx);
  197. let cx = span.context();
  198. let string_cx = serialize_context(&cx);
  199. metadata.parameters.open_telemetry_context = string_cx;
  200. }
  201. let py_event = PyEvent::from(event);
  202. let status_enum = operator
  203. .call_method1(py, "on_event", (py_event, send_output.clone()))
  204. .map_err(traceback);
  205. match status_enum {
  206. Ok(status_enum) => {
  207. let status_val = Python::with_gil(|py| status_enum.getattr(py, "value"))
  208. .wrap_err("on_event must have enum return value")?;
  209. Python::with_gil(|py| status_val.extract(py))
  210. .wrap_err("on_event has invalid return value")
  211. }
  212. Err(err) => {
  213. if reload {
  214. // Allow error in hot reloading environment to help development.
  215. warn!("{err}");
  216. Ok(DoraStatus::Continue as i32)
  217. } else {
  218. Err(err)
  219. }
  220. }
  221. }
  222. })?;
  223. match status {
  224. s if s == DoraStatus::Continue as i32 => {} // ok
  225. s if s == DoraStatus::Stop as i32 => break StopReason::ExplicitStop,
  226. s if s == DoraStatus::StopAll as i32 => break StopReason::ExplicitStopAll,
  227. other => bail!("on_event returned invalid status {other}"),
  228. }
  229. };
  230. // Dropping the operator using Python garbage collector.
  231. // Locking the GIL for immediate release.
  232. Python::with_gil(|_py| {
  233. drop(operator);
  234. });
  235. Result::<_, eyre::Report>::Ok(reason)
  236. };
  237. let closure = AssertUnwindSafe(|| {
  238. python_runner().wrap_err_with(|| format!("error in Python module at {}", path.display()))
  239. });
  240. match catch_unwind(closure) {
  241. Ok(Ok(reason)) => {
  242. let _ = events_tx.blocking_send(OperatorEvent::Finished { reason });
  243. }
  244. Ok(Err(err)) => {
  245. let _ = events_tx.blocking_send(OperatorEvent::Error(err));
  246. }
  247. Err(panic) => {
  248. let _ = events_tx.blocking_send(OperatorEvent::Panic(panic));
  249. }
  250. }
  251. Ok(())
  252. }
  253. #[pyclass]
  254. #[derive(Clone)]
  255. struct SendOutputCallback {
  256. events_tx: Sender<OperatorEvent>,
  257. }
  258. #[allow(unsafe_op_in_unsafe_fn)]
  259. mod callback_impl {
  260. use crate::operator::OperatorEvent;
  261. use super::SendOutputCallback;
  262. use aligned_vec::{AVec, ConstAlign};
  263. use arrow::{array::ArrayData, pyarrow::FromPyArrow};
  264. use dora_core::message::ArrowTypeInfo;
  265. use dora_node_api::{
  266. arrow_utils::{copy_array_into_sample, required_data_size},
  267. ZERO_COPY_THRESHOLD,
  268. };
  269. use dora_operator_api_python::pydict_to_metadata;
  270. use dora_tracing::telemetry::deserialize_context;
  271. use eyre::{eyre, Context, Result};
  272. use pyo3::{
  273. pymethods,
  274. types::{PyBytes, PyDict},
  275. PyObject, Python,
  276. };
  277. use tokio::sync::oneshot;
  278. use tracing::{field, span};
  279. use tracing_opentelemetry::OpenTelemetrySpanExt;
  280. /// Send an output from the operator:
  281. /// - the first argument is the `output_id` as defined in your dataflow.
  282. /// - the second argument is the data as either bytes or pyarrow.Array for zero copy.
  283. /// - the third argument is dora metadata if you want ot link the tracing from one input into an output.
  284. /// `e.g.: send_output("bbox", pa.array([100], type=pa.uint8()), dora_event["metadata"])`
  285. #[pymethods]
  286. impl SendOutputCallback {
  287. fn __call__(
  288. &mut self,
  289. output: &str,
  290. data: PyObject,
  291. metadata: Option<&PyDict>,
  292. py: Python,
  293. ) -> Result<()> {
  294. let parameters = pydict_to_metadata(metadata)
  295. .wrap_err("failed to parse metadata")?
  296. .into_owned();
  297. let span = span!(
  298. tracing::Level::TRACE,
  299. "send_output",
  300. output_id = field::Empty
  301. );
  302. span.record("output_id", output);
  303. let cx = deserialize_context(&parameters.open_telemetry_context);
  304. span.set_parent(cx);
  305. let _ = span.enter();
  306. let allocate_sample = |data_len| {
  307. if data_len > ZERO_COPY_THRESHOLD {
  308. let (tx, rx) = oneshot::channel();
  309. self.events_tx
  310. .blocking_send(OperatorEvent::AllocateOutputSample {
  311. len: data_len,
  312. sample: tx,
  313. })
  314. .map_err(|_| eyre!("failed to send output to runtime"))?;
  315. rx.blocking_recv()
  316. .wrap_err("failed to request output sample")?
  317. .wrap_err("failed to allocate output sample")
  318. } else {
  319. let avec: AVec<u8, ConstAlign<128>> = AVec::__from_elem(128, 0, data_len);
  320. Ok(avec.into())
  321. }
  322. };
  323. let (sample, type_info) = if let Ok(py_bytes) = data.downcast::<PyBytes>(py) {
  324. let data = py_bytes.as_bytes();
  325. let mut sample = allocate_sample(data.len())?;
  326. sample.copy_from_slice(data);
  327. (sample, ArrowTypeInfo::byte_array(data.len()))
  328. } else if let Ok(arrow_array) = ArrayData::from_pyarrow(data.as_ref(py)) {
  329. let total_len = required_data_size(&arrow_array);
  330. let mut sample = allocate_sample(total_len)?;
  331. let type_info = copy_array_into_sample(&mut sample, &arrow_array);
  332. (sample, type_info)
  333. } else {
  334. eyre::bail!("invalid `data` type, must by `PyBytes` or arrow array")
  335. };
  336. py.allow_threads(|| {
  337. let event = OperatorEvent::Output {
  338. output_id: output.to_owned().into(),
  339. type_info,
  340. parameters,
  341. data: Some(sample),
  342. };
  343. self.events_tx
  344. .blocking_send(event)
  345. .map_err(|_| eyre!("failed to send output to runtime"))
  346. })?;
  347. Ok(())
  348. }
  349. }
  350. }