mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-14 15:50:19 +00:00
Add a typed HomeServer wrapper on the Rust side
Same pattern as the Reactor wrapper: a newtype around the Python object that is the single place naming the HomeServer API surface Rust relies on (config extraction, get_rust_runtime, get_clock, the main database pool). RustHandlers and both rendezvous handlers now take it instead of a bare Py<PyAny>, and public_baseurl moves into the extracted ServerConfig view rather than a getattr chain. Also drop the runtime duck-type validation from Reactor: hasattr only proves a name exists, not that the signature matches, and the Python side of these boundaries is already type-checked by mypy via the .pyi stubs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyuiV3m44ZbK9o24EnMMES
This commit is contained in:
co-authored by
Claude Opus 5
parent
00c35955ee
commit
efb0e1cce4
@@ -48,6 +48,7 @@ pub struct AuthConfig {
|
||||
#[derive(FromPyObject, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub msc4140_enabled: bool,
|
||||
pub public_baseurl: String,
|
||||
}
|
||||
|
||||
#[derive(FromPyObject, Clone)]
|
||||
|
||||
@@ -21,8 +21,7 @@ use pyo3::{
|
||||
Bound, PyResult, Python,
|
||||
};
|
||||
|
||||
use crate::config::SynapseHomeServerConfig;
|
||||
use crate::runtime::RustRuntime;
|
||||
use crate::homeserver::HomeServer;
|
||||
use crate::storage::db::python_db_pool::PythonDatabasePoolWrapper;
|
||||
use crate::storage::store::Store;
|
||||
|
||||
@@ -37,22 +36,17 @@ struct RustHandlers {
|
||||
impl RustHandlers {
|
||||
#[new]
|
||||
#[pyo3(signature = (homeserver))]
|
||||
pub fn py_new(py: Python<'_>, homeserver: &Bound<'_, PyAny>) -> PyResult<RustHandlers> {
|
||||
let config: SynapseHomeServerConfig = homeserver.getattr("config")?.extract()?;
|
||||
pub fn py_new(py: Python<'_>, homeserver: HomeServer) -> PyResult<RustHandlers> {
|
||||
let config = homeserver.config(py)?;
|
||||
|
||||
// The per-homeserver Rust state, which gives us the tokio runtime
|
||||
// and the Twisted reactor.
|
||||
let runtime: Bound<'_, RustRuntime> =
|
||||
homeserver.call_method0("get_rust_runtime")?.extract()?;
|
||||
let runtime = Arc::clone(runtime.get().inner());
|
||||
let runtime = homeserver.rust_runtime(py)?;
|
||||
|
||||
// hs.get_datastores().main.db_pool
|
||||
let db_pool_py = homeserver
|
||||
.call_method0("get_datastores")?
|
||||
.getattr("main")?
|
||||
.getattr("db_pool")?;
|
||||
let db_pool =
|
||||
PythonDatabasePoolWrapper::new(&db_pool_py, runtime.reactor().clone_ref(py))?;
|
||||
let db_pool = PythonDatabasePoolWrapper::new(
|
||||
&homeserver.main_database_pool(py)?,
|
||||
runtime.reactor().clone_ref(py),
|
||||
)?;
|
||||
|
||||
// Store is shared across all of the handlers so let's use an `Arc`
|
||||
let store = Arc::new(Store {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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>.
|
||||
*
|
||||
*/
|
||||
|
||||
//! A typed wrapper around the Python `HomeServer`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use pyo3::{intern, prelude::*};
|
||||
|
||||
use crate::config::SynapseHomeServerConfig;
|
||||
use crate::runtime::{RustRuntime, RustRuntimeInner};
|
||||
|
||||
/// The Python `HomeServer`, as seen from Rust.
|
||||
///
|
||||
/// Like [`crate::reactor::Reactor`], this is a typed facade over a foreign
|
||||
/// Python object: it does no validation of the object it wraps (the Python
|
||||
/// side is type-checked by mypy), but it is the single place that names the
|
||||
/// `HomeServer` API surface that Rust code depends on.
|
||||
pub struct HomeServer(Py<PyAny>);
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for HomeServer {
|
||||
type Error = PyErr;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
||||
Ok(HomeServer(obj.to_owned().unbind()))
|
||||
}
|
||||
}
|
||||
|
||||
impl HomeServer {
|
||||
/// The per-homeserver Rust state (`hs.get_rust_runtime()`), which gives
|
||||
/// access to the tokio runtime and the reactor.
|
||||
pub fn rust_runtime(&self, py: Python<'_>) -> PyResult<Arc<RustRuntimeInner>> {
|
||||
let runtime: Bound<'_, RustRuntime> = self
|
||||
.0
|
||||
.bind(py)
|
||||
.call_method0(intern!(py, "get_rust_runtime"))?
|
||||
.extract()?;
|
||||
|
||||
Ok(Arc::clone(runtime.get().inner()))
|
||||
}
|
||||
|
||||
/// The Rust-side view of `hs.config`.
|
||||
pub fn config(&self, py: Python<'_>) -> PyResult<SynapseHomeServerConfig> {
|
||||
self.0.bind(py).getattr(intern!(py, "config"))?.extract()
|
||||
}
|
||||
|
||||
/// The Synapse `Clock` (`hs.get_clock()`).
|
||||
// TODO: give the clock a typed wrapper of its own.
|
||||
pub fn clock(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
Ok(self
|
||||
.0
|
||||
.bind(py)
|
||||
.call_method0(intern!(py, "get_clock"))?
|
||||
.unbind())
|
||||
}
|
||||
|
||||
/// The main database pool (`hs.get_datastores().main.db_pool`).
|
||||
pub fn main_database_pool<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.0
|
||||
.bind(py)
|
||||
.call_method0(intern!(py, "get_datastores"))?
|
||||
.getattr(intern!(py, "main"))?
|
||||
.getattr(intern!(py, "db_pool"))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod duration;
|
||||
pub mod errors;
|
||||
pub mod events;
|
||||
pub mod handlers;
|
||||
pub mod homeserver;
|
||||
pub mod http;
|
||||
pub mod http_client;
|
||||
pub mod identifier;
|
||||
|
||||
@@ -21,7 +21,7 @@ use http::StatusCode;
|
||||
use pyo3::{
|
||||
pyclass, pymethods,
|
||||
types::{PyAnyMethods, PyModule, PyModuleMethods},
|
||||
Bound, IntoPyObject, Py, PyAny, PyResult, Python,
|
||||
Bound, Py, PyAny, PyResult, Python,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
@@ -30,9 +30,9 @@ use self::session::Session;
|
||||
use crate::{
|
||||
duration::SynapseDuration,
|
||||
errors::{NotFoundError, SynapseError},
|
||||
homeserver::HomeServer,
|
||||
http::http_request_from_twisted,
|
||||
msc4388_rendezvous::session::{GetResponse, PostResponse, PutResponse},
|
||||
UnwrapInfallible,
|
||||
};
|
||||
|
||||
mod session;
|
||||
@@ -92,25 +92,21 @@ impl MSC4388RendezvousHandler {
|
||||
#[pyo3(signature = (homeserver, /, soft_limit=100, hard_limit=200,max_content_length=4*1024, eviction_interval=60*1000, ttl=2*60*1000))]
|
||||
fn new(
|
||||
py: Python<'_>,
|
||||
homeserver: &Bound<'_, PyAny>,
|
||||
homeserver: HomeServer,
|
||||
soft_limit: usize,
|
||||
hard_limit: usize,
|
||||
max_content_length: u64,
|
||||
eviction_interval: u64,
|
||||
ttl: u64,
|
||||
) -> PyResult<Py<Self>> {
|
||||
let clock = homeserver
|
||||
.call_method0("get_clock")?
|
||||
.into_pyobject(py)
|
||||
.unwrap_infallible()
|
||||
.unbind();
|
||||
let clock = homeserver.clock(py)?;
|
||||
|
||||
// Construct a Python object so that we can get a reference to the
|
||||
// evict method and schedule it to run.
|
||||
let self_ = Py::new(
|
||||
py,
|
||||
Self {
|
||||
clock,
|
||||
clock: clock.clone_ref(py),
|
||||
sessions: BTreeMap::new(),
|
||||
soft_limit,
|
||||
hard_limit,
|
||||
@@ -122,11 +118,9 @@ impl MSC4388RendezvousHandler {
|
||||
let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval);
|
||||
|
||||
let evict = self_.getattr(py, "_evict")?;
|
||||
homeserver.call_method0("get_clock")?.call_method(
|
||||
"looping_call",
|
||||
(evict, &eviction_duration),
|
||||
None,
|
||||
)?;
|
||||
clock
|
||||
.bind(py)
|
||||
.call_method("looping_call", (evict, &eviction_duration), None)?;
|
||||
|
||||
Ok(self_)
|
||||
}
|
||||
|
||||
+5
-21
@@ -15,36 +15,20 @@
|
||||
|
||||
//! A typed wrapper around the Twisted reactor.
|
||||
|
||||
use pyo3::{call::PyCallArgs, exceptions::PyTypeError, intern, prelude::*};
|
||||
|
||||
/// The reactor methods Rust code relies on.
|
||||
///
|
||||
/// Extracting a [`Reactor`] from a Python object fails if any of these are
|
||||
/// missing, so mistakes surface as a `TypeError` at the FFI boundary rather
|
||||
/// than as an `AttributeError` on a tokio worker thread later.
|
||||
const REQUIRED_METHODS: &[&str] = &["callFromThread", "addSystemEventTrigger"];
|
||||
use pyo3::{call::PyCallArgs, intern, prelude::*};
|
||||
|
||||
/// The Twisted reactor, as seen from Rust.
|
||||
///
|
||||
/// This is not a static guarantee that the object behaves like a reactor (it
|
||||
/// is a foreign Python object), but the duck type is checked once at
|
||||
/// extraction, and this module is the single place that names the Twisted
|
||||
/// API surface that Rust code depends on.
|
||||
/// A typed facade over a foreign Python object: it does no validation of the
|
||||
/// object it wraps (the Python side is type-checked by mypy), but it is the
|
||||
/// single place that names the Twisted API surface that Rust code depends
|
||||
/// on.
|
||||
pub struct Reactor(Py<PyAny>);
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for Reactor {
|
||||
type Error = PyErr;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
||||
for name in REQUIRED_METHODS {
|
||||
if !obj.hasattr(*name)? {
|
||||
return Err(PyTypeError::new_err(format!(
|
||||
"expected a Twisted reactor, but {} has no `{name}` method",
|
||||
obj.get_type()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Reactor(obj.to_owned().unbind()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use pyo3::{
|
||||
exceptions::PyValueError,
|
||||
pyclass, pymethods,
|
||||
types::{PyAnyMethods, PyModule, PyModuleMethods},
|
||||
Bound, IntoPyObject, Py, PyAny, PyResult, Python,
|
||||
Bound, Py, PyAny, PyResult, Python,
|
||||
};
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -37,8 +37,8 @@ use self::session::Session;
|
||||
use crate::{
|
||||
duration::SynapseDuration,
|
||||
errors::{NotFoundError, SynapseError},
|
||||
homeserver::HomeServer,
|
||||
http::{http_request_from_twisted, http_response_to_twisted, HeaderMapPyExt},
|
||||
UnwrapInfallible,
|
||||
};
|
||||
|
||||
mod session;
|
||||
@@ -113,25 +113,17 @@ impl RendezvousHandler {
|
||||
#[pyo3(signature = (homeserver, /, capacity=100, max_content_length=4*1024, eviction_interval=60*1000, ttl=60*1000))]
|
||||
fn new(
|
||||
py: Python<'_>,
|
||||
homeserver: &Bound<'_, PyAny>,
|
||||
homeserver: HomeServer,
|
||||
capacity: usize,
|
||||
max_content_length: u64,
|
||||
eviction_interval: u64,
|
||||
ttl: u64,
|
||||
) -> PyResult<Py<Self>> {
|
||||
let base: String = homeserver
|
||||
.getattr("config")?
|
||||
.getattr("server")?
|
||||
.getattr("public_baseurl")?
|
||||
.extract()?;
|
||||
let base = homeserver.config(py)?.server.public_baseurl;
|
||||
let base = Uri::try_from(format!("{base}_synapse/client/rendezvous"))
|
||||
.map_err(|_| PyValueError::new_err("Invalid base URI"))?;
|
||||
|
||||
let clock = homeserver
|
||||
.call_method0("get_clock")?
|
||||
.into_pyobject(py)
|
||||
.unwrap_infallible()
|
||||
.unbind();
|
||||
let clock = homeserver.clock(py)?;
|
||||
|
||||
let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval);
|
||||
|
||||
@@ -141,7 +133,7 @@ impl RendezvousHandler {
|
||||
py,
|
||||
Self {
|
||||
base,
|
||||
clock,
|
||||
clock: clock.clone_ref(py),
|
||||
sessions: BTreeMap::new(),
|
||||
capacity,
|
||||
max_content_length,
|
||||
@@ -150,11 +142,9 @@ impl RendezvousHandler {
|
||||
)?;
|
||||
|
||||
let evict = self_.getattr(py, "_evict")?;
|
||||
homeserver.call_method0("get_clock")?.call_method(
|
||||
"looping_call",
|
||||
(evict, &eviction_duration),
|
||||
None,
|
||||
)?;
|
||||
clock
|
||||
.bind(py)
|
||||
.call_method("looping_call", (evict, &eviction_duration), None)?;
|
||||
|
||||
Ok(self_)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user