mirror of
https://github.com/element-hq/synapse.git
synced 2026-09-02 11:23:47 +00:00
Spawning from https://github.com/element-hq/synapse/pull/19824 / https://github.com/element-hq/synapse/pull/19846 and wanting to use `create_deferred` in more than just the `http_client.rs`
139 lines
4.6 KiB
Rust
139 lines
4.6 KiB
Rust
/*
|
|
* 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:
|
|
* <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
*
|
|
*/
|
|
|
|
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>() {
|
|
string
|
|
} else {
|
|
"unknown error"
|
|
};
|
|
Self::new_err(panic_message.to_owned())
|
|
}
|
|
}
|
|
|
|
/// A reference to the `twisted.internet.defer` module.
|
|
static DEFER: OnceCell<Py<PyAny>> = 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<Bound<'py, PyAny>>
|
|
where
|
|
F: Future<Output = PyResult<O>> + 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<pyo3::Py<pyo3::PyAny>> = 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<Bound<'py, PyAny>> {
|
|
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(())
|
|
}
|