Files
synapse/tests/federation/_rust_transactions.py
T
Erik JohnstonandClaude Opus 5.5 0ebe577640 Run the federation queues in Rust at rust_federation_sending: queues
`FederationSender` makes a `RustPerDestinationQueue` for each
destination at the new `queues` level. It forwards the queue methods to
Rust and provides the hooks: catch-up, the sticky backlog, the device
EDUs, and recording a sent transaction (stream positions, the module
callback, the metrics).

The catch-up loop moves into `_CatchUpLoop`, which both queues share.
`StickyEventBacklogTracker.load_backlog` splits the database part out of
`prepare_transaction`.

The sender, catch-up and sticky event tests also run at this level, with
every reactor advance waiting for the Rust loops to settle.
`test_send_receipts_thread` is skipped there: it depends on coroutine
order, which the Rust loop changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5b7Ruy4cgt27Mjq6Ptxbb
2026-10-15 14:54:44 +00:00

188 lines
6.4 KiB
Python

#
# 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>.
#
"""Run federation sender tests with Rust building the transactions, or at
`queues`, running the queues too.
The tests mock `TransportLayerClient.send_transaction`, which the Rust sender
does not call. `RustTransactionsMixin` puts a stand-in for the Rust sender in
its place: Rust builds each transaction, the mock is called with it as it
would be with a Python `Transaction`, and Rust reads the mock's response. The
HTTP half of the Rust sender is tested in `tests.http.test_rust_transactions`.
"""
import time
from typing import TYPE_CHECKING, Any, Callable, Sequence
from twisted.internet.testing import MemoryReactor, MemoryReactorClock
from synapse.events import EventBase
from synapse.federation.sender import FederationSender
from synapse.server import HomeServer
from synapse.synapse_rust.federation import (
RustDestinationQueues,
RustTransaction,
RustTransactionManager,
)
from synapse.types import JsonDict
from synapse.util.clock import Clock
from tests.utils import RUST_FED_STRICT_FOR_TESTS
if TYPE_CHECKING:
from unittest.mock import Mock
from tests.unittest import HomeserverTestCase
_Base = HomeserverTestCase
else:
_Base = object
class _MockedRustTransactions:
"""Stands in for a `RustTransactionManager`, sending through the test's
mocked `send_transaction`."""
def __init__(self, rust: RustTransactionManager, transport: "Mock") -> None:
self._rust = rust
self._transport = transport
async def send_transaction(
self,
destination: str,
pdus: Sequence[EventBase],
edus: Sequence[JsonDict],
*,
extra_headers: Sequence[tuple[str, str]] = (),
want_acknowledged: bool = False,
check: Callable[[RustTransaction], None] | None = None,
) -> list[str] | None:
transaction = self._rust.build_transaction(destination, pdus, edus)
if check is not None:
check(transaction)
response = await self._transport.send_transaction(
transaction, transaction.get_dict
)
if not isinstance(response, dict):
response = {}
acknowledged = self._rust.acknowledged_pdus(transaction, response)
return acknowledged if want_acknowledged else None
class RustTransactionsMixin(_Base):
"""Run a test case at `rust_federation_sending: transactions`. The test
case must mock the federation transport client as
`self.federation_transport_client`."""
federation_transport_client: "Mock"
def default_config(self) -> JsonDict:
config = super().default_config()
config.setdefault("experimental_features", {})["rust_federation_sending"] = (
"transactions"
)
return config
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
super().prepare(reactor, clock, hs)
sender = hs.get_federation_sender()
assert isinstance(sender, FederationSender)
manager = sender._transaction_manager
assert manager._rust is not None
manager._rust_strict = RUST_FED_STRICT_FOR_TESTS
manager._rust = _MockedRustTransactions( # type: ignore[assignment]
manager._rust, self.federation_transport_client
)
def wait_for_rust_queues(
reactor: MemoryReactorClock, queues: RustDestinationQueues
) -> None:
"""Advance the reactor by nothing until every Rust transmission loop has
finished or waits on something that needs time to pass.
The loops run on tokio, and the reactor does not wait for them. A loop is
done for now when it is idle and the reactor has nothing due.
"""
for i in range(5000):
reactor.advance(0)
due = any(
call.getTime() <= reactor.seconds() for call in reactor.getDelayedCalls()
)
if queues.idle() and not due:
return
time.sleep(0 if i < 10 else 0.001)
raise AssertionError("the Rust federation queues did not settle")
class RustQueuesMixin(_Base):
"""Run a test case at `rust_federation_sending: queues`. The test case
must mock the federation transport client as
`self.federation_transport_client`.
The Rust loops send through the mocked `send_transaction`, as the Python
loop does. Every `reactor.advance` waits for the loops to settle, so the
test's checks after one see what the loops did.
"""
federation_transport_client: "Mock"
def default_config(self) -> JsonDict:
config = super().default_config()
config.setdefault("experimental_features", {})["rust_federation_sending"] = (
"queues"
)
return config
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
super().prepare(reactor, clock, hs)
sender = hs.get_federation_sender()
assert isinstance(sender, FederationSender)
queues = sender._rust_queues
assert queues is not None
transport = self.federation_transport_client
async def send(transaction: RustTransaction) -> Any:
return await transport.send_transaction(transaction, transaction.get_dict)
queues.set_send_for_test(send)
# Catch-up still sends through the transaction manager.
manager = sender._transaction_manager
assert manager._rust is not None
manager._rust_strict = RUST_FED_STRICT_FOR_TESTS
manager._rust = _MockedRustTransactions( # type: ignore[assignment]
manager._rust, transport
)
reactor = self.reactor
advance = reactor.advance
waiting = False
def advance_and_wait(amount: float) -> None:
nonlocal waiting
advance(amount)
if waiting:
return
waiting = True
try:
wait_for_rust_queues(reactor, queues)
finally:
waiting = False
reactor.advance = advance_and_wait # type: ignore[method-assign]