Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3696e7c
Splat based on #19824
MadLittleMods Jun 9, 2026
c63b40e
A little bit of iterating
MadLittleMods Jun 10, 2026
bb4c555
Adjust comment
MadLittleMods Jun 10, 2026
8f0768d
LLM attempt 1
MadLittleMods Jun 10, 2026
d9a111b
LLM attempt at simplifying
MadLittleMods Jun 10, 2026
6b5e9f2
Partial `Row`
MadLittleMods Jun 17, 2026
d3af0c4
LLM further `Row`
MadLittleMods Jun 17, 2026
65ba851
Concrete `Row` type
MadLittleMods Jun 17, 2026
0226a67
Add comments why row expectations
MadLittleMods Jun 17, 2026
7bf4b6a
Non-working: Add `test_versions.py`
MadLittleMods Jun 18, 2026
8270087
Fix test failing with `RuntimeError: Tokio runtime is not running`
MadLittleMods Jun 18, 2026
0b6a973
Passing `tests_versions.py`: Wait real time for tokio thread pool to …
MadLittleMods Jun 18, 2026
158b4a0
Revert "Passing `tests_versions.py`: Wait real time for tokio thread …
MadLittleMods Jun 18, 2026
e2ec3f1
`FakeChannel.await_rust_result`
MadLittleMods Jun 18, 2026
1dcf17d
Rename: `await_rust_result` -> `await_result_with_rust`
MadLittleMods Jun 18, 2026
357860c
Specify features to enable
MadLittleMods Jun 19, 2026
ca1371e
Refactor `deferred` and `tokio_runtime` to their own crates
MadLittleMods Jun 19, 2026
f76c8c1
Refactor to remove `SynapseConfig` from shared code
MadLittleMods Jun 19, 2026
29e5830
Fix Rust warnings
MadLittleMods Jun 19, 2026
5b1a1ae
WIP: `RoomCreationPreset`
MadLittleMods Jun 19, 2026
9688d48
Fix `RoomCreationPreset`
MadLittleMods Jun 19, 2026
941188c
Remove unused `msc4222`
MadLittleMods Jun 19, 2026
7c2790a
Serde `UnstableFeatureMap`
MadLittleMods Jun 19, 2026
7aec5a0
Stub remaining features
MadLittleMods Jun 19, 2026
d9a02d3
Merge branch 'develop' into madlittlemods/rust-db-access-using-python…
MadLittleMods Jun 22, 2026
19c0777
Add changelog
MadLittleMods Jun 22, 2026
5f23dc6
Fix lint
MadLittleMods Jun 22, 2026
1d6eb1e
Remove `await_result_with_rust` in favor of updating `await_result` w…
MadLittleMods Jun 22, 2026
4eec02c
Explain possible better future for async fn that need to be `Send`
MadLittleMods Jun 22, 2026
dc93a23
Fix grammar, add intended fix
MadLittleMods Jun 22, 2026
874178a
`poll_once` instead of `futures::executor::block_on`
MadLittleMods Jun 22, 2026
505b599
Panic for programming error
MadLittleMods Jun 22, 2026
d40adfa
Explain why `poll_once`
MadLittleMods Jun 22, 2026
065c5cb
Non-working: Try to save/restore logcontext
MadLittleMods Jun 24, 2026
441e580
Fix logcontext
MadLittleMods Jun 24, 2026
7466574
Less wordy
MadLittleMods Jun 24, 2026
37ff660
Align imports on `OnceCell` which has `get_or_try_init`
MadLittleMods Jun 24, 2026
f42ba67
Avoid circular import issues
MadLittleMods Jun 24, 2026
c21dbbd
Rename `Value` -> `DbValue`
MadLittleMods Jun 24, 2026
ce68c0f
Merge branch 'develop' into madlittlemods/rust-db-access-using-python…
MadLittleMods Jun 24, 2026
2860e4e
LLM attempt at switching back to `Store` with `dyn DatabasePool`
MadLittleMods Jun 24, 2026
683f264
LLM simplify `dyn DatabasePool`
MadLittleMods Jun 24, 2026
adeb039
Sync trait not needed on `run_interaction`
MadLittleMods Jun 24, 2026
a4e4af7
"Dyn-compatibility" is the new name for "object safety"
MadLittleMods Jun 24, 2026
87ca67d
Update comments
MadLittleMods Jun 24, 2026
590fd2b
Fill in the rest of `rust/src/handlers/versions.rs`
MadLittleMods Jun 24, 2026
2a609a5
Fill in missing experimental config
MadLittleMods Jun 24, 2026
59228e5
Fill in the rest of the config
MadLittleMods Jun 24, 2026
55d89f9
Use `time.sleep(0)` to align with decision in #19871
MadLittleMods Jun 24, 2026
7ed525d
Rename `SynapseConfig` -> `SynapseHomeServerConfig`
MadLittleMods Jun 24, 2026
8ddab4f
Iterate on Database trait comments
MadLittleMods Jun 24, 2026
79a8cc1
Rename `Row` -> `DbRow`
MadLittleMods Jun 24, 2026
a2e68ad
Explain more about `python_db_pool`
MadLittleMods Jun 24, 2026
0090330
Iterate on `python_db_pool` comments
MadLittleMods Jun 24, 2026
6195074
Only return runInteraction result if it succeeded
MadLittleMods Jun 24, 2026
b5ea2f7
Avoid `String` allocation
MadLittleMods Jun 24, 2026
5ce6b8a
Better `failure_to_pyerr`
MadLittleMods Jun 24, 2026
74ad3b2
Merge branch 'develop' into madlittlemods/rust-db-access-using-python…
MadLittleMods Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
855 changes: 803 additions & 52 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions changelog.d/19846.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow Rust code to have database access via Python database connection pool.
7 changes: 7 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ crate-type = ["lib", "cdylib"]
name = "synapse.synapse_rust"

[dependencies]
async-trait = "0.1.89"
anyhow = "1.0.63"
base64 = "0.22.1"
bytes = "1.6.0"
Expand Down Expand Up @@ -64,6 +65,12 @@ tokio = { version = "1.44.2", features = ["rt", "rt-multi-thread"] }
once_cell = "1.18.0"
itertools = "0.14.0"

# TODO: Remove: These are just used to make sure a tokio-postgres backed database pool makes sense
# with our interfaces
bb8 = "0.8.3"
bb8-postgres = "0.8.1"
postgres-native-tls = "0.5.0"

[build-dependencies]
blake2 = "0.10.4"
hex = "0.4.3"
Expand Down
103 changes: 103 additions & 0 deletions rust/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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::collections::BTreeSet;
use std::str::FromStr;

use pyo3::{exceptions::PyRuntimeError, prelude::*};

/// A Rust-side view of Synapse's Python `HomeServerConfig`.
///
/// This only mirrors the subset of config that the Rust handlers need, rather
/// than the whole thing. Thanks to `#[derive(FromPyObject)]`, each field is
/// pulled directly off the corresponding attribute of the Python `config`
/// object, so you can populate it in one shot with
/// `homeserver.getattr("config")?.extract()?`.
#[derive(FromPyObject, Clone)]
pub struct SynapseHomeServerConfig {
pub room: RoomConfig,
pub auth: AuthConfig,
pub server: ServerConfig,
pub experimental: ExperimentalConfig,
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum RoomCreationPreset {
PrivateChat,
PublicChat,
TrustedPrivateChat,
}

impl FromStr for RoomCreationPreset {
type Err = PyErr;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"private_chat" => RoomCreationPreset::PrivateChat,
"public_chat" => RoomCreationPreset::PublicChat,
"trusted_private_chat" => RoomCreationPreset::TrustedPrivateChat,
other => {
return Err(PyRuntimeError::new_err(format!(
"Unknown variant {other:?} does not translate to `RoomCreationPreset`. \
This is a Synapse programming error."
)))
}
})
}
}

impl<'a, 'py> FromPyObject<'a, 'py> for RoomCreationPreset {
type Error = PyErr;

fn extract(value: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
value.extract::<&str>()?.parse()
}
}

#[derive(FromPyObject, Clone)]
pub struct RoomConfig {
pub encryption_enabled_by_default_for_room_presets: BTreeSet<RoomCreationPreset>,
}

#[derive(FromPyObject, Clone)]
pub struct AuthConfig {
pub login_via_existing_enabled: bool,
}
#[derive(FromPyObject, Clone)]
pub struct ServerConfig {
pub max_event_delay_ms: Option<u64>,
}

#[derive(FromPyObject, Clone)]
pub struct ExperimentalConfig {
pub msc3026_enabled: bool,
pub msc3773_enabled: bool,
pub msc2815_enabled: bool,
pub msc3881_enabled: bool,
pub msc3874_enabled: bool,
pub msc3912_enabled: bool,
pub msc3391_enabled: bool,
pub msc4069_profile_inhibit_propagation: bool,
pub msc4028_push_encrypted_events: bool,
pub msc4108_enabled: bool,
pub msc4108_delegation_endpoint: Option<String>,
pub msc3575_enabled: bool,
pub msc4133_enabled: bool,
pub msc4155_enabled: bool,
pub msc4306_enabled: bool,
pub msc4169_enabled: bool,
pub msc4354_enabled: bool,
pub msc4222_enabled: bool,
}
225 changes: 215 additions & 10 deletions rust/src/deferred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@
*
*/

use std::{future::Future, sync::OnceLock};
use std::{
future::Future,
sync::{Arc, Mutex},
};

use once_cell::sync::OnceCell;
use pyo3::{create_exception, exceptions::PyException, prelude::*};
use pyo3::{
create_exception, exceptions::PyException, exceptions::PyRuntimeError, intern, prelude::*,
types::PyCFunction,
};
use tokio::sync::oneshot;

use crate::tokio_runtime::runtime;

Expand Down Expand Up @@ -51,10 +58,46 @@ fn defer(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
.bind(py))
}

/// A reference to the `synapse.logging.context` module.
static LOGGING_CONTEXT_MODULE: OnceCell<Py<PyAny>> = OnceCell::new();

/// Access to the `synapse.logging.context` module.
fn logging_context_module(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
Ok(LOGGING_CONTEXT_MODULE
.get_or_try_init(|| py.import("synapse.logging.context").map(Into::into))?
.bind(py))
}

/// Set the Synapse logcontext active on the current (reactor) thread, returning
/// the context that was previously active so it can be restored afterwards.
fn set_current_logging_context<'py>(
py: Python<'py>,
context: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
logging_context_module(py)?.call_method1(intern!(py, "set_current_context"), (context,))
}

tokio::task_local! {
/// The Synapse `LoggingContext` that was active on the reactor thread when
/// the Rust future was created (see [`create_deferred`]).
///
/// Synapse attributes per-request CPU/DB usage via a thread-local
/// `LoggingContext`, but our work runs on a Tokio worker thread that is
/// detached from it. We stash the originating context here so that whenever
/// we hop back onto the reactor thread to drive Python work (see
/// [`run_python_awaitable`]) we can re-activate it; otherwise that work runs in
/// the sentinel context and its resource usage (e.g. database time) is lost
/// instead of being charged to the request.
static LOGGING_CONTEXT: Py<PyAny>;
}

/// Creates a twisted deferred from the given future, spawning the task on the
/// tokio runtime.
///
/// Does not handle deferred cancellation or contextvars.
/// Captures the Synapse logcontext active on the reactor thread (so work driven
/// by the future is attributed to the originating request, see
/// [`LOGGING_CONTEXT`]), but does not handle deferred cancellation or
/// contextvars.
pub fn create_deferred<'py, F, O>(
py: Python<'py>,
reactor: &Bound<'py, PyAny>,
Expand All @@ -68,9 +111,16 @@ where
let deferred_callback = deferred.getattr("callback")?.unbind();
let deferred_errback = deferred.getattr("errback")?.unbind();

// Capture the logcontext active on the reactor thread now, while we're still on it,
// so the spawned task can keep track of what logcontext its associated with and can
// re-apply it when it hops back to drive Python work.
let logging_context = logging_context_module(py)?
.call_method0(intern!(py, "current_context"))?
.unbind();

let rt = runtime(reactor)?;
let handle = rt.handle()?;
let task = handle.spawn(fut);
let task = handle.spawn(LOGGING_CONTEXT.scope(logging_context, fut));

// Unbind the reactor so that we can pass it to the task
let reactor = reactor.clone().unbind();
Expand Down Expand Up @@ -110,18 +160,171 @@ where
make_deferred_yieldable(py, &deferred)
}

static MAKE_DEFERRED_YIELDABLE: OnceLock<pyo3::Py<pyo3::PyAny>> = OnceLock::new();
/// Runs a Python awaitable to completion on the Twisted reactor and resolves
/// with its result.
///
/// This is the inverse of [`create_deferred`]: where that turns a Rust future
/// into a Twisted `Deferred`, this turns a Python awaitable into a Rust future.
///
/// Despite returning a future, the awaitable is kicked off in the background running in
/// the Twisted reactor and runs to completion regardless of whether the returned Rust
/// future is ever polled; awaiting it only observes the result.
///
/// Calls `make_awaitable` with the current logcontext associated with this async Rust
/// task. Follows the Synapse logcontext rules.
pub(crate) async fn run_python_awaitable<F>(
reactor: Py<PyAny>,
make_awaitable: F,
) -> PyResult<Py<PyAny>>
where
F: for<'py> Fn(Python<'py>) -> PyResult<Bound<'py, PyAny>> + Send + 'static,
{
// Resolves when the awaitable completes; carries the resolved value or error.
let (tx, rx) = oneshot::channel::<PyResult<Py<PyAny>>>();
// Shared between the success and error callbacks (only one ever fires).
let sender = Arc::new(Mutex::new(Some(tx)));

// The logcontext that is active for this async task
let current_rust_task_logging_context = LOGGING_CONTEXT
.try_with(|ctx| Python::attach(|py| ctx.clone_ref(py)))
.ok();

Python::attach(|py| -> PyResult<()> {
// Create some deferred success/error callback functions that we will use to get
// the result from Python to Rust.
let success_sender = Arc::clone(&sender);
let on_success = PyCFunction::new_closure(
py,
None,
None,
move |args, _kwargs| -> PyResult<Py<PyAny>> {
let value = args.get_item(0)?.unbind();
if let Some(tx) = success_sender.lock().unwrap().take() {
let _ = tx.send(Ok(value));
}
Ok(args.py().None())
},
)?
.unbind();

let error_sender = Arc::clone(&sender);
let on_error = PyCFunction::new_closure(
py,
None,
None,
move |args, _kwargs| -> PyResult<Py<PyAny>> {
let err = failure_to_pyerr(&args.get_item(0)?);
if let Some(tx) = error_sender.lock().unwrap().take() {
let _ = tx.send(Err(err));
}
Ok(args.py().None())
},
)?
.unbind();

// Wrap `make_awaitable` as a Python callable so we can hand it to
// `run_in_background`, which calls it (in the active logcontext) to produce
// the awaitable it then drives.
let awaitable_factory = PyCFunction::new_closure(
py,
None,
None,
move |args, _kwargs| -> PyResult<Py<PyAny>> {
let py = args.py();
Ok(make_awaitable(py)?.unbind())
},
)?
.unbind();

// Create a function that we will run with the Twisted reactor that will drive
// the Python awaitable.
let starter = PyCFunction::new_closure(
py,
None,
None,
move |args, _kwargs| -> PyResult<Py<PyAny>> {
let py = args.py();

// Activate the captured Rust task-local logging context. This way we
// properly record metrics/logging for the thing being run.
let calling_logging_context = current_rust_task_logging_context
.as_ref()
.map(|ctx| set_current_logging_context(py, ctx.bind(py)))
.transpose()?
.expect("No `LoggingContext` returned from `set_current_logging_context(...)`. This is a Synapse programming error.");

// We fire-and-forget using `run_in_background`. Re-using
// `run_in_background` also makes sure the awaitable gets run with the
// current logcontext (the one we just activated) while following the
// logcontext rules.
let deferred = logging_context_module(py)?.call_method1(
intern!(py, "run_in_background"),
(awaitable_factory.bind(py),),
);

// Restore the `calling_logging_context` after we kick off the
// background task.
//
// Our goal is to have the caller logcontext unchanged after firing off
// the background task and returning.
set_current_logging_context(py, &calling_logging_context)?;

let deferred = deferred?;
deferred.call_method1(
intern!(py, "addCallbacks"),
(on_success.bind(py), on_error.bind(py)),
)?;
Ok(py.None())
},
)?;

reactor
.bind(py)
.call_method1(intern!(py, "callFromThread"), (starter,))?;

Ok(())
})?;

match rx.await {
Ok(result) => result,
Err(_) => Err(PyRuntimeError::new_err(
"run_python_awaitable channel closed before the awaitable completed",
)),
}
}

/// Convert a Twisted `Failure` (as passed to an Deferred errback) into a [`PyErr`].
///
/// A Twisted `Failure` carries the original exception instance in its `.value`
/// attribute, which we re-raise so callers see the real error. If the `Failure` is
/// mangled, we fallback to raising a generic [`PyRuntimeError`] explaining what we saw
/// instead.
fn failure_to_pyerr(failure: &Bound<'_, PyAny>) -> PyErr {
match failure.getattr(intern!(failure.py(), "value")) {
Ok(value) => PyErr::from_value(value),
Err(_) => PyRuntimeError::new_err(format!(
"Expected Python object passed here to be a Twisted `Failure` with a `value` attribute \
but saw something else: {}",
failure
.str()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|_| "<failed to stringify Python object>".to_owned()),
)),
}
}

static MAKE_DEFERRED_YIELDABLE: OnceCell<Py<PyAny>> = OnceCell::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
});
let make_deferred_yieldable = MAKE_DEFERRED_YIELDABLE.get_or_try_init(|| {
logging_context_module(py)?
.getattr("make_deferred_yieldable")
.map(Into::into)
})?;

make_deferred_yieldable
.call1(py, (deferred,))?
Expand All @@ -133,6 +336,8 @@ fn make_deferred_yieldable<'py>(
pub fn register_module(py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> {
// Make sure we fail early if we can't load some modules
defer(py)?;
// We can't check this here because of circular import issues
// logging_context_module(py)?;
Comment on lines +339 to +340

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this will get fixed by #19876 (comment)


Ok(())
}
Loading
Loading