/* * This file is licensed under the Affero General Public License (AGPL) version 3. * * Copyright (C) 2026 Element Creations Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * See the GNU Affero General Public License for more details: * . * */ use std::{future::Future, sync::OnceLock}; use once_cell::sync::OnceCell; use pyo3::{create_exception, exceptions::PyException, prelude::*}; use crate::tokio_runtime::runtime; create_exception!( synapse.synapse_rust.http_client, RustPanicError, PyException, "A panic which happened in a Rust future" ); impl RustPanicError { fn from_panic(panic_err: &(dyn std::any::Any + Send + 'static)) -> PyErr { // Apparently this is how you extract the panic message from a panic let panic_message = if let Some(str_slice) = panic_err.downcast_ref::<&str>() { str_slice } else if let Some(string) = panic_err.downcast_ref::() { string } else { "unknown error" }; Self::new_err(panic_message.to_owned()) } } /// A reference to the `twisted.internet.defer` module. static DEFER: OnceCell> = OnceCell::new(); /// Access to the `twisted.internet.defer` module. fn defer(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { Ok(DEFER .get_or_try_init(|| py.import("twisted.internet.defer").map(Into::into))? .bind(py)) } /// Creates a twisted deferred from the given future, spawning the task on the /// tokio runtime. /// /// Does not handle deferred cancellation or contextvars. pub fn create_deferred<'py, F, O>( py: Python<'py>, reactor: &Bound<'py, PyAny>, fut: F, ) -> PyResult> where F: Future> + Send + 'static, for<'a> O: IntoPyObject<'a> + Send + 'static, { let deferred = defer(py)?.call_method0("Deferred")?; let deferred_callback = deferred.getattr("callback")?.unbind(); let deferred_errback = deferred.getattr("errback")?.unbind(); let rt = runtime(reactor)?; let handle = rt.handle()?; let task = handle.spawn(fut); // Unbind the reactor so that we can pass it to the task let reactor = reactor.clone().unbind(); handle.spawn(async move { let res = task.await; Python::attach(move |py| { // Flatten the panic into standard python error let res = match res { Ok(r) => r, Err(join_err) => match join_err.try_into_panic() { Ok(panic_err) => Err(RustPanicError::from_panic(&panic_err)), Err(err) => Err(PyException::new_err(format!("Task cancelled: {err}"))), }, }; // Re-bind the reactor let reactor = reactor.bind(py); // Send the result to the deferred, via `.callback(..)` or `.errback(..)` match res { Ok(obj) => { reactor .call_method("callFromThread", (deferred_callback, obj), None) .expect("callFromThread should not fail"); // There's nothing we can really do with errors here } Err(err) => { reactor .call_method("callFromThread", (deferred_errback, err), None) .expect("callFromThread should not fail"); // There's nothing we can really do with errors here } } }); }); // Make the deferred follow the Synapse logcontext rules make_deferred_yieldable(py, &deferred) } static MAKE_DEFERRED_YIELDABLE: OnceLock> = OnceLock::new(); /// Given a deferred, make it follow the Synapse logcontext rules fn make_deferred_yieldable<'py>( py: Python<'py>, deferred: &Bound<'py, PyAny>, ) -> PyResult> { let make_deferred_yieldable = MAKE_DEFERRED_YIELDABLE.get_or_init(|| { let sys = PyModule::import(py, "synapse.logging.context").unwrap(); let func = sys.getattr("make_deferred_yieldable").unwrap().unbind(); func }); make_deferred_yieldable .call1(py, (deferred,))? .extract(py) .map_err(Into::into) } /// Called when registering modules with python. pub fn register_module(py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> { // Make sure we fail early if we can't load some modules defer(py)?; Ok(()) }