Files
synapse/tests/federation/test_rust_queues.py
T
Erik JohnstonandClaude Opus 5.5 50bc702e76 Fix review findings in Rust catch-up and the sticky backlog
- The server filter read each restricted event's state group before it
  checked for partial state, and through a cache that a prefill never
  reached. After an event got its full state, a retry could use the old
  partial group. It now checks first, then reads those groups from the
  database. A prefill of a Python cache now invalidates its Rust mirrors.
- Catch-up ended in two steps, so a PDU that send_pdu skipped in between
  was lost until the next catch-up. One lock now covers both.
- The sticky backlog query reads a fresh snapshot per statement, so
  events could expire between its cleanup and its retry. It now cleans
  up again, a few times, before giving up.
- Tests for each, plus catch-up's batch limit, a failed catch-up send,
  module notification, a filtered-out sticky batch, the hand-off to
  Python, the backlog cleanup boundary and paging outstanding
  destinations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5b7Ruy4cgt27Mjq6Ptxbb
2026-10-16 09:27:35 +00:00

763 lines
29 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>.
#
"""Tests for the per-destination queues at `rust_federation_sending: queues`.
The sender, catch-up and sticky event tests also run at this level, through
`RustQueuesMixin`, with their mocked `send_transaction`. These tests send
through the Rust request layer instead, with a Python callable in place of the
network, as `tests.http.test_rust_federation_client` does.
"""
import json
from typing import Any
from unittest import SkipTest
from unittest.mock import AsyncMock
from twisted.internet.testing import MemoryReactor
from synapse.api.presence import UserPresenceState
from synapse.events import EventBase
from synapse.federation.sender import FederationSender
from synapse.federation.sender.per_destination_queue import PerDestinationQueue
from synapse.federation.sender.rust_queue import (
RustPerDestinationQueue,
use_native_store,
)
from synapse.federation.units import Edu
from synapse.metrics import REGISTRY, SERVER_NAME_LABEL, sent_transactions_counter
from synapse.module_api.callbacks.federation import (
FederatedEventDeliveryMethod,
FederationEventDeliveryEvent,
)
from synapse.rest import admin
from synapse.rest.client import login, room
from synapse.server import HomeServer
from synapse.storage.rust_pool import RustConnectionPool
from synapse.synapse_rust.federation import (
RustDestinationQueues,
RustFederationClient,
RustTransactionManager,
federation_gil_stats,
)
from synapse.util.clock import Clock
from synapse.util.duration import Duration
from tests import unittest
from tests.federation._rust_transactions import wait_for_rust_queues
from tests.http.test_rust_federation_client import (
DEST,
FakeTransport,
json_response,
)
class RustQueuesTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
room.register_servlets,
]
def default_config(self) -> dict[str, Any]:
config = super().default_config()
config.setdefault("experimental_features", {})["rust_federation_sending"] = (
"queues"
)
config["federation_sender_instances"] = None
config["federation_metrics_domains"] = [DEST]
return config
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
cl = hs.get_federation_http_client()
self.transport = FakeTransport()
rust_client = RustFederationClient(
hs,
hs.version_string,
hs.version_string,
cl._write_retry_timings,
cl._record_request_metrics,
self.transport,
)
# Modules register their callbacks before the queues are made, which
# read whether there are any.
self.delivered = AsyncMock(return_value=None)
hs.get_module_api_callbacks().federation.register_callbacks(
on_event_delivered_over_federation=self.delivered
)
sender = hs.get_federation_sender()
assert isinstance(sender, FederationSender)
self.sender = sender
self.queues = self._make_queues(RustTransactionManager(rust_client))
sender._rust_queues = self.queues
user = self.register_user("user", "pass")
tok = self.login("user", "pass")
room_id = self.helper.create_room_as(user, tok=tok)
self.room_id = room_id
self.tok = tok
self.store = hs.get_datastores().main
self.event = self._send_event("hello")
queue = sender._get_per_destination_queue(DEST)
assert isinstance(queue, RustPerDestinationQueue)
self.queue = queue
def _make_queues(
self, transactions: RustTransactionManager
) -> RustDestinationQueues:
return RustDestinationQueues(
transactions, self.hs.config.experimental.msc4354_enabled, {DEST}
)
def _send_event(self, body: str) -> EventBase:
event_id = self.helper.send(self.room_id, body, tok=self.tok)["event_id"]
return self.get_success(self.store.get_event(event_id))
def _settle(self) -> None:
wait_for_rust_queues(self.reactor, self.queues)
def _queue_calls(self, before: dict[str, tuple[int, float]]) -> dict[str, int]:
"""The GIL crossings of the queues since `before`."""
after = federation_gil_stats()
return {
site: after[site][0] - before.get(site, (0, 0.0))[0]
for site in after
if site.startswith("queue_")
and after[site][0] != before.get(site, (0, 0.0))[0]
}
def _sent_transactions(self) -> float:
self.sender._flush_rust_queue_metrics()
return sent_transactions_counter.labels(
**{SERVER_NAME_LABEL: self.hs.hostname}
)._value.get()
def test_send_pdu(self) -> None:
"""The queue sends the PDU through the Rust client and records it."""
self.transport.responses.append(
json_response(200, {"pdus": {self.event.event_id: {}}})
)
sent_before = self._sent_transactions()
self.queue.send_pdu(self.event)
self._settle()
[request] = self.transport.requests
self.assertEqual(request.method, "PUT")
self.assertRegex(request.path, r"^/_matrix/federation/v1/send/\d+$")
body = json.loads(request.body)
self.assertEqual(body["pdus"], [self.event.get_pdu_json()])
self.assertNotIn("edus", body)
self.assertEqual(self.queue.pending_pdu_count(), 0)
self.assertFalse(self.queue.transmission_loop_running)
self.assertEqual(
self.get_success(
self.store.get_destination_last_successful_stream_ordering(DEST)
),
self.event.internal_metadata.stream_ordering,
)
self.delivered.assert_awaited_once_with(
FederationEventDeliveryEvent(
server_name=DEST,
events=[self.event],
method=FederatedEventDeliveryMethod.SEND,
)
)
self.assertEqual(self._sent_transactions(), sent_before + 1)
def test_edus(self) -> None:
"""Keyed EDUs replace each other, and go with plain ones."""
self.transport.responses.append(json_response(200, {}))
def typing(room_id: str, typing: bool) -> Edu:
return Edu(
edu_type="m.typing",
content={"room_id": room_id, "user_id": "@u:test", "typing": typing},
origin=self.hs.hostname,
destination=DEST,
)
other = Edu(
edu_type="m.other", content={}, origin=self.hs.hostname, destination=DEST
)
# The loop cannot get past its backoff check until the reactor runs,
# so these all go in the first transaction.
self.queue.send_edu(other)
self.queue.send_keyed_edu(typing("!a", True), ("!a",))
self.queue.send_keyed_edu(typing("!a", False), ("!a",))
self._settle()
[request] = self.transport.requests
body = json.loads(request.body)
self.assertEqual(
body["edus"], [other.get_dict(), typing("!a", False).get_dict()]
)
def test_failed_send_keeps_pdu(self) -> None:
"""An error response leaves the PDU queued and backs off."""
self.transport.responses.append(
json_response(403, {"errcode": "M_FORBIDDEN", "error": "no"})
)
self.queue.send_pdu(self.event)
self._settle()
self.assertEqual(len(self.transport.requests), 1)
self.assertEqual(self.queue._rust.pending_pdu_ids(), [self.event.event_id])
timings = self.get_success(self.store.get_destination_retry_timings(DEST))
self.assertIsNotNone(timings)
def test_long_backoff_catches_up(self) -> None:
"""Backing off for over an hour drops the volatile EDUs and the PDUs,
and leaves catch-up to send the PDUs later."""
now = self.clock.time_msec()
self.get_success(
self.store.set_destination_retry_timings(DEST, now, now, 2 * 60 * 60 * 1000)
)
self.queue.send_presence(
[UserPresenceState.default("@u:test")], start_loop=False
)
self.assertEqual(self.queue.pending_edu_count(), 1)
self.queue.send_pdu(self.event)
self._settle()
self.assertEqual(self.transport.requests, [])
self.assertTrue(self.queue._catching_up)
self.assertEqual(self.queue.pending_pdu_count(), 0)
self.assertEqual(self.queue.pending_edu_count(), 0)
def test_gil_crossings(self) -> None:
"""Once caught up, a transaction takes the GIL twice in the loop: for
its device EDUs and to record it. The loop then looks for more to
send, which asks for device EDUs again, as Python's does."""
self.transport.responses.extend([json_response(200, {})] * 2)
self.queue.send_pdu(self.event)
self._settle()
before = federation_gil_stats()
self.queue.send_pdu(self.event)
self._settle()
self.assertEqual(
self._queue_calls(before), {"queue_device_edus": 2, "queue_complete": 1}
)
def test_shutdown(self) -> None:
self.sender.shutdown()
self.queue.send_pdu(self.event)
self._settle()
self.assertEqual(self.transport.requests, [])
self.assertFalse(self.queue.transmission_loop_running)
class RustQueuesNativeTestCase(RustQueuesTestCase):
"""The same, with the database work done through the native driver, and
sticky events on, so the loops look at the sticky event backlog."""
def default_config(self) -> dict[str, Any]:
config = super().default_config()
config["experimental_features"]["msc4354_enabled"] = True
return config
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
hs = self.setup_test_homeserver()
if not isinstance(
hs.get_datastores().main.db_pool._db_pool, RustConnectionPool
):
raise SkipTest("needs the native Rust Postgres driver")
return hs
def _make_queues(
self, transactions: RustTransactionManager
) -> RustDestinationQueues:
queues = super()._make_queues(transactions)
self.assertTrue(use_native_store(self.hs, queues))
return queues
def _catch_up(self) -> None:
"""Send a first transaction, which leaves catch-up and reads the
device streams for the first time."""
self.transport.responses.append(json_response(200, {}))
self.queue.send_pdu(self.event)
self._settle()
self.transport.requests.clear()
def test_gil_crossings(self) -> None:
"""Once caught up, a transaction with no device updates takes the GIL
only to tell the module about the delivered event."""
self._catch_up()
self.transport.responses.append(
json_response(200, {"pdus": {self.event.event_id: {}}})
)
before = federation_gil_stats()
self.queue.send_pdu(self.event)
self._settle()
self.assertEqual(len(self.transport.requests), 1)
self.assertEqual(self._queue_calls(before), {"queue_complete": 1})
def test_gil_crossings_without_modules(self) -> None:
"""With no module to tell, it takes the GIL nowhere in the loop."""
self.hs.get_module_api_callbacks().federation._on_event_delivered_over_federation_callbacks.clear()
queue = self.sender._get_per_destination_queue("other.example.com")
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.extend([json_response(200, {})] * 2)
queue.send_pdu(self.event)
self._settle()
self.transport.requests.clear()
before = federation_gil_stats()
queue.send_pdu(self.event)
self._settle()
self.assertEqual(len(self.transport.requests), 1)
self.assertEqual(self._queue_calls(before), {})
def _catch_up_destination(self, destination: str, missed: EventBase) -> None:
"""Make `destination` one we sent nothing to since before `missed`."""
stream_ordering = missed.internal_metadata.stream_ordering
assert stream_ordering is not None
self.get_success(
self.store.set_destination_last_successful_stream_ordering(
destination, stream_ordering - 1
)
)
self.get_success(
self.store.store_destination_rooms_entries(
[destination], missed.room_id, stream_ordering
)
)
def test_catch_up(self) -> None:
"""Catch-up runs in Rust: it sends the room's latest event in place of
the one the destination missed, and records how far it got, without
taking the GIL."""
self.hs.get_module_api_callbacks().federation._on_event_delivered_over_federation_callbacks.clear()
destination = "catchup.example.com"
self._catch_up_destination(destination, self.event)
latest = self._send_event("newer")
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.append(json_response(200, {}))
before = federation_gil_stats()
queue.attempt_new_transaction()
self._settle()
[request] = self.transport.requests
body = json.loads(request.body)
self.assertEqual(
[pdu["content"].get("body") for pdu in body["pdus"]],
[latest.content["body"]],
)
self.assertEqual(
self.get_success(
self.store.get_destination_last_successful_stream_ordering(destination)
),
self.event.internal_metadata.stream_ordering,
)
self.assertFalse(queue._catching_up)
self.assertNotIn("queue_catch_up", self._queue_calls(before))
def test_catch_up_falls_back(self) -> None:
"""With history visible to members only, the destination, which has
no members, may not see the room's latest events, so catch-up sends
the event it missed."""
destination = "catchup.example.com"
self._catch_up_destination(destination, self.event)
self.helper.send_state(
self.room_id,
"m.room.history_visibility",
{"history_visibility": "joined"},
tok=self.tok,
)
self._send_event("newer")
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.append(json_response(200, {}))
queue.attempt_new_transaction()
self._settle()
[request] = self.transport.requests
body = json.loads(request.body)
self.assertEqual(
[pdu["content"].get("body") for pdu in body["pdus"]],
[self.event.content["body"]],
)
def test_catch_up_notifies_module(self) -> None:
"""The module hears of the events catch-up sent."""
destination = "catchup.example.com"
self._catch_up_destination(destination, self.event)
latest = self._send_event("newer")
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.append(
json_response(200, {"pdus": {latest.event_id: {}}})
)
queue.attempt_new_transaction()
self._settle()
# The module gets a new copy of the event, so compare by ID.
self.delivered.assert_awaited_once()
assert self.delivered.await_args is not None
[delivery] = self.delivered.await_args.args
self.assertEqual(delivery.server_name, destination)
self.assertEqual([e.event_id for e in delivery.events], [latest.event_id])
self.assertEqual(delivery.method, FederatedEventDeliveryMethod.SEND)
def test_catch_up_send_fails(self) -> None:
"""A failed catch-up send records nothing, and leaves the destination
catching up."""
destination = "catchup.example.com"
self._catch_up_destination(destination, self.event)
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.append(
json_response(403, {"errcode": "M_FORBIDDEN", "error": "no"})
)
queue.attempt_new_transaction()
self._settle()
self.assertEqual(len(self.transport.requests), 1)
stream_ordering = self.event.internal_metadata.stream_ordering
assert stream_ordering is not None
self.assertEqual(
self.get_success(
self.store.get_destination_last_successful_stream_ordering(destination)
),
stream_ordering - 1,
)
self.assertTrue(queue._catching_up)
self.delivered.assert_not_awaited()
def _backlog_sticky(self, destination: str, body: str) -> EventBase:
"""Leave `destination` to catch up past a sticky event, which is
returned, and a later plain one."""
self._catch_up_destination(destination, self.event)
event_id = self.helper.send_sticky_event(
self.room_id,
"m.room.message",
duration=Duration(minutes=10),
content={"body": body, "msgtype": "m.text"},
tok=self.tok,
)["event_id"]
latest = self._send_event("latest")
stream_ordering = latest.internal_metadata.stream_ordering
assert stream_ordering is not None
self.get_success(
self.store.store_destination_rooms_entries(
[destination], self.room_id, stream_ordering
)
)
return self.get_success(self.store.get_event(event_id))
def _backlog_rows(self, destination: str) -> list[tuple[str]]:
return self.get_success(
self.store.db_pool.simple_select_list(
"destination_rooms_sticky_events_backlog",
{"destination": destination},
["room_id"],
)
)
def _sent_bodies(self) -> list[list[str]]:
return [
[pdu["content"].get("body") for pdu in json.loads(request.body)["pdus"]]
for request in self.transport.requests
]
def test_sticky_backlog_filtered_out(self) -> None:
"""A sticky batch the destination may not see is not sent, and the
backlog still moves on, in Rust."""
self.hs.get_module_api_callbacks().federation._on_event_delivered_over_federation_callbacks.clear()
destination = "sticky.example.com"
self._backlog_sticky(destination, "sticky")
# The sender's events are hidden once they are erased.
self.get_success(self.store.mark_user_erased(self.event.sender))
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.extend([json_response(200, {})] * 2)
before = federation_gil_stats()
queue.attempt_new_transaction()
self._settle()
# Catch-up sends the event the destination missed, which is the
# room's latest, unfiltered. The sticky event is not sent.
self.assertEqual(self._sent_bodies(), [["latest"]])
self.assertEqual(self._backlog_rows(destination), [])
self.assertFalse(queue._rust.sticky_backlogged)
calls = self._queue_calls(before)
self.assertNotIn("queue_sticky_backlog", calls)
self.assertNotIn("queue_complete", calls)
def _set_partial_state(self, event: EventBase) -> None:
"""Mark `event`, and so its room, as having partial state."""
self.get_success(
self.store.db_pool.simple_insert(
"partial_state_rooms", {"room_id": self.room_id}
)
)
self.store.is_partial_state_room.invalidate((self.room_id,))
self.get_success(
self.store.db_pool.simple_insert(
"partial_state_events",
{"room_id": self.room_id, "event_id": event.event_id},
)
)
self.store.is_partial_state_event.invalidate((event.event_id,))
def test_sticky_backlog_needs_full_state(self) -> None:
"""A sticky batch that needs the full state to filter goes to the
Python hook, which waits for it."""
self.hs.get_module_api_callbacks().federation._on_event_delivered_over_federation_callbacks.clear()
destination = "sticky.example.com"
self.helper.send_state(
self.room_id,
"m.room.history_visibility",
{"history_visibility": "joined"},
tok=self.tok,
)
sticky = self._backlog_sticky(destination, "sticky")
self._set_partial_state(sticky)
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.extend([json_response(200, {})] * 2)
before = federation_gil_stats()
queue.attempt_new_transaction()
self._settle()
# Catch-up sent the event the destination missed. The sticky batch
# waits in Python for the full state.
self.assertEqual(self._sent_bodies(), [["latest"]])
self.assertIn("queue_sticky_backlog", self._queue_calls(before))
self.assertNotEqual(self._backlog_rows(destination), [])
self.get_success(
self.store.db_pool.simple_delete_one(
"partial_state_events", {"event_id": sticky.event_id}
)
)
self.get_success(
self.store.db_pool.simple_delete_one(
"partial_state_rooms", {"room_id": self.room_id}
)
)
self.store.is_partial_state_room.invalidate((self.room_id,))
self.store.is_partial_state_event.invalidate((sticky.event_id,))
self.hs.get_storage_controllers().state.notify_event_un_partial_stated(
sticky.event_id
)
self._settle()
# The destination has no members, so may not see the sticky event.
# Python's batch is still recorded as sent.
self.assertEqual(self._sent_bodies(), [["latest"]])
self.assertEqual(self._backlog_rows(destination), [])
self.assertIn("queue_complete", self._queue_calls(before))
def test_sticky_backlog(self) -> None:
"""Catching up past a sticky event backlogs it, and the loop then
sends it, all without taking the GIL."""
self.hs.get_module_api_callbacks().federation._on_event_delivered_over_federation_callbacks.clear()
destination = "sticky.example.com"
self._catch_up_destination(destination, self.event)
self.helper.send_sticky_event(
self.room_id,
"m.room.message",
duration=Duration(minutes=10),
content={"body": "sticky", "msgtype": "m.text"},
tok=self.tok,
)
latest = self._send_event("latest")
stream_ordering = latest.internal_metadata.stream_ordering
assert stream_ordering is not None
self.get_success(
self.store.store_destination_rooms_entries(
[destination], self.room_id, stream_ordering
)
)
queue = self.sender._get_per_destination_queue(destination)
assert isinstance(queue, RustPerDestinationQueue)
self.transport.responses.extend([json_response(200, {})] * 2)
before = federation_gil_stats()
queue.attempt_new_transaction()
self._settle()
bodies = [
[pdu["content"].get("body") for pdu in json.loads(request.body)["pdus"]]
for request in self.transport.requests
]
self.assertEqual(bodies, [["latest"], ["sticky"]])
# The loop looked again, found the backlog sent, and cleared it.
self.assertEqual(
self.get_success(
self.store.db_pool.simple_select_list(
"destination_rooms_sticky_events_backlog",
{"destination": destination},
["room_id"],
)
),
[],
)
self.assertFalse(queue._rust.sticky_backlogged)
calls = self._queue_calls(before)
self.assertNotIn("queue_catch_up", calls)
self.assertNotIn("queue_sticky_backlog", calls)
self.assertNotIn("queue_complete", calls)
def test_to_device(self) -> None:
"""A to-device message is sent, and deleted once sent. Reading the
stream position is the one GIL crossing."""
self._catch_up()
self.transport.responses.append(json_response(200, {}))
message = {
"sender": "@user:test",
"type": "m.test",
"message_id": "m1",
"messages": {"@u:remote": {"D": {"body": "hi"}}},
}
before = federation_gil_stats()
self.get_success(
self.store.add_remote_messages_from_client_to_device_inbox({DEST: message})
)
self.queue.attempt_new_transaction()
self._settle()
[request] = self.transport.requests
body = json.loads(request.body)
self.assertEqual(
body["edus"], [{"edu_type": "m.direct_to_device", "content": message}]
)
self.assertEqual(self._queue_calls(before), {"queue_stream_tokens": 1})
current = self.store.get_to_device_stream_token()
self.assertEqual(
self.get_success(
self.store.get_new_device_msgs_for_remote(DEST, 0, current, 10)
),
([], current),
)
def test_device_list_update(self) -> None:
"""A device list update is sent, and marked as sent."""
self._catch_up()
self.transport.responses.append(json_response(200, {}))
user = "@user:test"
self.get_success(self.store.store_device(user, "NEW", None))
self.get_success(self.store.add_device_change_to_streams(user, ["NEW"], []))
self.get_success(
self.store.add_device_list_outbound_pokes(
user_id=user, device_id="NEW", room_id="!r", hosts=[DEST], context={}
)
)
self.queue.attempt_new_transaction()
self._settle()
[request] = self.transport.requests
[edu] = json.loads(request.body)["edus"]
self.assertEqual(edu["edu_type"], "m.device_list_update")
self.assertEqual(edu["content"]["device_id"], "NEW")
pokes = self.get_success(
self.store.db_pool.simple_select_list(
"device_lists_outbound_pokes", {"destination": DEST}, ["stream_id"]
)
)
self.assertEqual(pokes, [])
def test_metrics(self) -> None:
"""The loops' counts reach the Python metrics in a scrape."""
self._catch_up()
def scrape(name: str, **labels: str) -> float | None:
# Collecting the registry runs the pre-collect hooks first.
return REGISTRY.get_sample_value(
name, {SERVER_NAME_LABEL: self.hs.hostname, **labels}
)
sent_before = scrape("synapse_federation_client_sent_transactions_total")
assert sent_before is not None
# A new event, so the gauge can only hold its time if this send set it.
self.reactor.advance(10)
event = self._send_event("again")
self.assertNotEqual(event.origin_server_ts, self.event.origin_server_ts)
self.transport.responses.append(json_response(200, {}))
self.queue.send_pdu(event)
self._settle()
self.assertEqual(
scrape("synapse_federation_client_sent_transactions_total"),
sent_before + 1,
)
self.assertEqual(
scrape(
"synapse_federation_last_sent_pdu_time", destination_server_name=DEST
),
event.origin_server_ts / 1000,
)
class RustQueuesLevelTestCase(unittest.HomeserverTestCase):
"""Which queue each level gets."""
def default_config(self) -> dict[str, Any]:
config = super().default_config()
config["federation_sender_instances"] = None
return config
def _queue(self) -> object:
sender = self.hs.get_federation_sender()
assert isinstance(sender, FederationSender)
return sender._get_per_destination_queue(DEST)
@unittest.override_config(
{"experimental_features": {"rust_federation_sending": "transactions"}}
)
def test_transactions_level(self) -> None:
self.assertIsInstance(self._queue(), PerDestinationQueue)
@unittest.override_config(
{"experimental_features": {"rust_federation_sending": "queues"}}
)
def test_queues_level(self) -> None:
self.assertIsInstance(self._queue(), RustPerDestinationQueue)
@unittest.override_config(
{
"experimental_features": {"rust_federation_sending": "queues"},
"instance_map": {
"main": {"host": "testserv", "port": 8765},
"federation_sender": {"host": "testserv", "port": 1001},
},
"outbound_federation_restricted_to": ["federation_sender"],
"worker_replication_secret": "secret",
}
)
def test_proxy_worker(self) -> None:
"""Without the Rust transaction sender, the queues are Python's."""
self.assertIsInstance(self._queue(), PerDestinationQueue)