mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 06:57:20 +00:00
Send the sticky event backlog in Rust on the native driver
`NativeHooks::sticky_backlog` loads a backlogged batch, cleaning up rooms whose sticky events expired, reads the events through the shared event cache and filters them for the destination. Its completion records the batch natively. Where the filter would wait for full state, the batch is left to Python, as before. `FederationStore` also gains `get_catch_up_outstanding_destinations` for phase 6. Each query has a differential test against Python's. The native queue tests now run with MSC4354 on, so the loops look at the sticky event backlog, and pin that it takes no GIL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5b7Ruy4cgt27Mjq6Ptxbb
This commit is contained in:
1 parent
5c6f35b0e5
commit
2d76b50a9c
9 files changed
+550
-18
No files matched your search
@@ -0,0 +1 @@
|
||||
Run federation catch-up and the sticky event backlog in Rust when the native Postgres driver is in use, under the experimental `rust_federation_sending: queues` option.
|
||||
@@ -290,9 +290,13 @@ pub trait QueueHooks: Send + Sync + 'static {
|
||||
request: DeviceEdusRequest,
|
||||
) -> Result<DeviceEdus, LoopError>;
|
||||
|
||||
/// `StickyEventBacklogTracker.prepare_transaction`: `None` once the
|
||||
/// backlog is empty.
|
||||
async fn sticky_backlog(&self, observer: &Observer) -> Result<Option<StickyBatch>, LoopError>;
|
||||
/// `StickyEventBacklogTracker.prepare_transaction`, for `queue`: `None`
|
||||
/// once the backlog is empty.
|
||||
async fn sticky_backlog(
|
||||
&self,
|
||||
queue: &DestinationQueue,
|
||||
observer: &Observer,
|
||||
) -> Result<Option<StickyBatch>, LoopError>;
|
||||
|
||||
/// Record a finished transaction: the stream positions, the module
|
||||
/// callback and the metrics.
|
||||
@@ -908,7 +912,7 @@ impl DestinationQueue {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(batch) = self.hooks.sticky_backlog(observer).await? else {
|
||||
let Some(batch) = self.hooks.sticky_backlog(self, observer).await? else {
|
||||
log::info!(
|
||||
target: QUEUE_LOG,
|
||||
"Caught up on federation sticky event backlog for destination '{}'",
|
||||
@@ -1133,6 +1137,11 @@ impl DestinationQueue {
|
||||
self.state().sticky_backlogged = backlogged;
|
||||
}
|
||||
|
||||
/// The time now, by the sink's clock.
|
||||
pub fn now_ms(&self) -> u64 {
|
||||
self.sink.now_ms()
|
||||
}
|
||||
|
||||
/// The event IDs of the queued PDUs, oldest first. For tests.
|
||||
pub fn pending_pdu_ids(&self) -> Vec<String> {
|
||||
self.state()
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
//! the device EDUs and records finished transactions with the
|
||||
//! [`FederationStore`], on tokio. A transaction then needs no trip to the
|
||||
//! reactor, unless it finishes a sticky event batch or a module wants to
|
||||
//! hear about delivered events. Catch-up runs here too, reading events
|
||||
//! through the shared event cache. The sticky event backlog stays Python,
|
||||
//! through [`PyHooks`].
|
||||
//! hear about delivered events. Catch-up and the sticky event backlog run
|
||||
//! here too, reading events through the shared event cache.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -31,7 +30,7 @@ use pyo3::{exceptions::PyRuntimeError, intern, prelude::*};
|
||||
use super::{
|
||||
changes::StreamChanges, metrics::QueueMetrics, python::PyHooks, Completion,
|
||||
DestinationQueue, DeviceEdus, DeviceEdusRequest, LoopError, Observer, PduBody, QueueHooks,
|
||||
QueuedPdu, StickyBatch, QUEUE_LOG,
|
||||
QueuedPdu, StickyBatch, MAX_PDUS_PER_TRANSACTION, QUEUE_LOG,
|
||||
};
|
||||
use crate::{
|
||||
database::postgres::pool::{ConnectionPool, PyConnectionPool},
|
||||
@@ -129,6 +128,9 @@ fn db_error(err: anyhow::Error) -> LoopError {
|
||||
LoopError::Other(format!("{err:#}"))
|
||||
}
|
||||
|
||||
/// A position in the sticky events stream, from the native store.
|
||||
struct NativeStickyPosition(i64);
|
||||
|
||||
/// The stream ordering of an event read from the database, which has one.
|
||||
fn stream_ordering_of(event: &Event) -> Result<i64, LoopError> {
|
||||
event
|
||||
@@ -428,8 +430,71 @@ impl QueueHooks for NativeHooks {
|
||||
})
|
||||
}
|
||||
|
||||
async fn sticky_backlog(&self, observer: &Observer) -> Result<Option<StickyBatch>, LoopError> {
|
||||
self.python.sticky_backlog(observer).await
|
||||
/// `StickyEventBacklogTracker.load_backlog`, natively.
|
||||
async fn sticky_backlog(
|
||||
&self,
|
||||
queue: &DestinationQueue,
|
||||
observer: &Observer,
|
||||
) -> Result<Option<StickyBatch>, LoopError> {
|
||||
let destination = self.destination.as_str();
|
||||
let pool = self.native.pool.get()?;
|
||||
let Some((room_id, position, event_ids)) = self
|
||||
.native
|
||||
.store
|
||||
.get_backlogged_sticky_events_for_destination(
|
||||
&pool,
|
||||
destination,
|
||||
MAX_PDUS_PER_TRANSACTION,
|
||||
queue.now_ms() as i64,
|
||||
)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
log::debug!(
|
||||
target: QUEUE_LOG,
|
||||
"Selected {} backlogged sticky events to send to destination '{destination}' in room '{room_id}' (up to stream position {position})",
|
||||
event_ids.len()
|
||||
);
|
||||
|
||||
let reader = self.native.sync.reader(pool);
|
||||
let ids: Vec<Arc<str>> = event_ids.iter().map(|id| Arc::from(id.as_str())).collect();
|
||||
let events = reader
|
||||
.events(&ids, RedactBehaviour::Redact, false, false)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
// Leave out what the destination may not see, rather than send it
|
||||
// redacted, which would lose the stickiness. An erased user's
|
||||
// sticky events are not sent in catch-up. They are all local
|
||||
// events, so partial state does not apply.
|
||||
let filter = ServerFilter {
|
||||
target_server_name: destination,
|
||||
local_server_name: &self.native.server_name,
|
||||
redact: false,
|
||||
filter_out_erased_senders: true,
|
||||
filter_out_remote_partial_state_events: false,
|
||||
};
|
||||
let events = match filter_events_for_server(&reader, &filter, events)
|
||||
.await
|
||||
.map_err(db_error)?
|
||||
{
|
||||
ServerFiltered::Events(events) => events,
|
||||
// Python waits for the full state here. Let it.
|
||||
ServerFiltered::NeedsFullState => {
|
||||
return self.python.sticky_backlog(queue, observer).await;
|
||||
}
|
||||
};
|
||||
|
||||
let pdus = events
|
||||
.into_iter()
|
||||
.map(queued_pdu)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Some(StickyBatch {
|
||||
room_id,
|
||||
position: Arc::new(NativeStickyPosition(position)),
|
||||
pdus,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The database writes of `_rust_complete`, natively. Python is only
|
||||
@@ -494,9 +559,19 @@ impl QueueHooks for NativeHooks {
|
||||
}
|
||||
}
|
||||
|
||||
// The sticky batch finishes last, as in Python.
|
||||
if completion.sticky.is_none() {
|
||||
// The sticky batch finishes last, as in Python. One the native
|
||||
// store loaded finishes natively, and one Python loaded in Python.
|
||||
let Some((room_id, position)) = completion.sticky else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(NativeStickyPosition(position)) = position.downcast_ref() {
|
||||
let pool = self.native.pool.get()?;
|
||||
return self
|
||||
.native
|
||||
.store
|
||||
.mark_backlogged_sticky_events_sent(&pool, destination, &room_id, *position)
|
||||
.await
|
||||
.map_err(db_error);
|
||||
}
|
||||
self.python
|
||||
.call_complete(
|
||||
@@ -505,7 +580,7 @@ impl QueueHooks for NativeHooks {
|
||||
to_device_stream_id: None,
|
||||
device_list_stream_id: None,
|
||||
last_stream_ordering: None,
|
||||
sticky: completion.sticky,
|
||||
sticky: Some((room_id, position)),
|
||||
sent: None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -550,7 +550,11 @@ impl QueueHooks for PyHooks {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sticky_backlog(&self, observer: &Observer) -> Result<Option<StickyBatch>, LoopError> {
|
||||
async fn sticky_backlog(
|
||||
&self,
|
||||
_queue: &DestinationQueue,
|
||||
observer: &Observer,
|
||||
) -> Result<Option<StickyBatch>, LoopError> {
|
||||
self.call(
|
||||
observer,
|
||||
"queue_sticky_backlog",
|
||||
|
||||
@@ -165,7 +165,11 @@ impl QueueHooks for FakeHooks {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn sticky_backlog(&self, _observer: &Observer) -> Result<Option<StickyBatch>, LoopError> {
|
||||
async fn sticky_backlog(
|
||||
&self,
|
||||
_queue: &DestinationQueue,
|
||||
_observer: &Observer,
|
||||
) -> Result<Option<StickyBatch>, LoopError> {
|
||||
Ok(self.sticky.lock().unwrap().pop_front().flatten())
|
||||
}
|
||||
|
||||
|
||||
@@ -506,6 +506,199 @@ impl FederationStore {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `get_backlogged_sticky_events_for_destination`: up to `limit` of
|
||||
/// the local sticky events, live at `now_ms`, from one room whose
|
||||
/// backlog for `destination` is not empty, oldest first, and the sticky
|
||||
/// events stream position of the last. `None` if there is no backlog.
|
||||
///
|
||||
/// A room in the backlog whose sticky events have all expired is
|
||||
/// removed from it, and another room tried.
|
||||
pub async fn get_backlogged_sticky_events_for_destination(
|
||||
&self,
|
||||
pool: &ConnectionPool,
|
||||
destination: &str,
|
||||
limit: usize,
|
||||
now_ms: i64,
|
||||
) -> anyhow::Result<Option<(String, i64, Vec<String>)>> {
|
||||
let pattern = self.config.local_users_pattern();
|
||||
let mut conn = Conn::borrow(pool).await?;
|
||||
conn.begin().await?;
|
||||
|
||||
let mut backlog = self
|
||||
.try_backlogged_sticky_events(&conn, destination, limit, now_ms, &pattern)
|
||||
.await?;
|
||||
if matches!(&backlog, Some((_, _, events)) if events.is_empty()) {
|
||||
// The room is backlogged but has no sticky events to send, as
|
||||
// they expired, say. Clean up and try again.
|
||||
conn.query(
|
||||
"WITH to_clean_up AS (
|
||||
SELECT backlog.room_id
|
||||
FROM destination_rooms_sticky_events_backlog AS backlog
|
||||
LEFT JOIN sticky_events AS se
|
||||
ON se.room_id = backlog.room_id
|
||||
AND se.sender LIKE $1
|
||||
AND $2 < se.expires_at
|
||||
AND backlog.sticky_events_stream_position <= se.stream_id
|
||||
WHERE se.event_id IS NULL
|
||||
AND backlog.destination = $3
|
||||
)
|
||||
DELETE FROM destination_rooms_sticky_events_backlog
|
||||
WHERE destination = $3 AND room_id IN (SELECT room_id FROM to_clean_up)",
|
||||
&[&pattern, &now_ms, &destination],
|
||||
)
|
||||
.await?;
|
||||
backlog = self
|
||||
.try_backlogged_sticky_events(&conn, destination, limit, now_ms, &pattern)
|
||||
.await?;
|
||||
if matches!(&backlog, Some((_, _, events)) if events.is_empty()) {
|
||||
anyhow::bail!("Sticky event backlog for {destination} not cleaned up");
|
||||
}
|
||||
}
|
||||
conn.commit().await?;
|
||||
|
||||
Ok(backlog.map(|(room_id, position, events)| {
|
||||
(room_id, position.unwrap_or_default(), events)
|
||||
}))
|
||||
}
|
||||
|
||||
/// One try of [`FederationStore::get_backlogged_sticky_events_for_destination`]:
|
||||
/// a backlogged room, the position of its last event and its events,
|
||||
/// which may be none.
|
||||
async fn try_backlogged_sticky_events(
|
||||
&self,
|
||||
conn: &Conn,
|
||||
destination: &str,
|
||||
limit: usize,
|
||||
now_ms: i64,
|
||||
pattern: &str,
|
||||
) -> anyhow::Result<Option<(String, Option<i64>, Vec<String>)>> {
|
||||
let rows = conn
|
||||
.query(
|
||||
"SELECT room_id, sticky_events_stream_position::bigint
|
||||
FROM destination_rooms_sticky_events_backlog
|
||||
WHERE destination = $1
|
||||
LIMIT 1",
|
||||
&[&destination],
|
||||
)
|
||||
.await?;
|
||||
let Some(row) = rows.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let room_id: String = row.try_get(0)?;
|
||||
let from_position: i64 = row.try_get(1)?;
|
||||
|
||||
let rows = conn
|
||||
.query(
|
||||
"SELECT event_id, stream_id::bigint
|
||||
FROM sticky_events
|
||||
WHERE room_id = $1
|
||||
AND $2::bigint <= stream_id
|
||||
AND $3 < expires_at
|
||||
AND sender LIKE $4
|
||||
ORDER BY stream_id ASC
|
||||
LIMIT $5",
|
||||
&[&room_id, &from_position, &now_ms, &pattern, &(limit as i64)],
|
||||
)
|
||||
.await?;
|
||||
let mut position = None;
|
||||
let mut events = Vec::with_capacity(rows.len());
|
||||
for row in &rows {
|
||||
events.push(row.try_get(0)?);
|
||||
let stream_id: i64 = row.try_get(1)?;
|
||||
position = Some(position.map_or(stream_id, |p: i64| p.max(stream_id)));
|
||||
}
|
||||
Ok(Some((room_id, position, events)))
|
||||
}
|
||||
|
||||
/// `mark_backlogged_sticky_events_sent`: the sticky events of the room
|
||||
/// up to `position` reached `destination`.
|
||||
pub async fn mark_backlogged_sticky_events_sent(
|
||||
&self,
|
||||
pool: &ConnectionPool,
|
||||
destination: &str,
|
||||
room_id: &str,
|
||||
position: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
Conn::borrow(pool)
|
||||
.await?
|
||||
.query(
|
||||
"INSERT INTO destination_rooms_sticky_events_backlog
|
||||
(destination, room_id, sticky_events_stream_position)
|
||||
VALUES ($1, $2, $3::bigint)
|
||||
ON CONFLICT (destination, room_id) DO UPDATE
|
||||
SET sticky_events_stream_position = EXCLUDED.sticky_events_stream_position",
|
||||
// The position is an inclusive lower bound on what is left.
|
||||
&[&destination, &room_id, &(position + 1)],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `get_catch_up_outstanding_destinations`: up to 25 destinations after
|
||||
/// `after_destination`, in order, that we are not backing off from at
|
||||
/// `now_ms` and that have PDUs, to-device messages or sticky events
|
||||
/// waiting.
|
||||
pub async fn get_catch_up_outstanding_destinations(
|
||||
&self,
|
||||
pool: &ConnectionPool,
|
||||
after_destination: Option<&str>,
|
||||
now_ms: i64,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
// Everything sorts after "".
|
||||
let after = after_destination.unwrap_or("");
|
||||
let rows = Conn::borrow(pool)
|
||||
.await?
|
||||
.query(
|
||||
"WITH pdu_destinations AS (
|
||||
SELECT DISTINCT destination FROM destination_rooms
|
||||
LEFT JOIN destinations USING (destination)
|
||||
WHERE
|
||||
destination > $1
|
||||
AND destination_rooms.stream_ordering
|
||||
> COALESCE(destinations.last_successful_stream_ordering, 0)
|
||||
AND (
|
||||
destinations.retry_last_ts IS NULL OR
|
||||
destinations.retry_last_ts + destinations.retry_interval < $2
|
||||
)
|
||||
ORDER BY destination
|
||||
LIMIT 25
|
||||
), to_device_destinations AS (
|
||||
SELECT DISTINCT destination FROM device_federation_outbox
|
||||
LEFT JOIN destinations USING (destination)
|
||||
WHERE
|
||||
destination > $1
|
||||
AND (
|
||||
destinations.retry_last_ts IS NULL OR
|
||||
destinations.retry_last_ts + destinations.retry_interval < $2
|
||||
)
|
||||
ORDER BY destination
|
||||
LIMIT 25
|
||||
), sticky_event_destinations AS (
|
||||
SELECT DISTINCT destination
|
||||
FROM destination_rooms_sticky_events_backlog
|
||||
LEFT JOIN destinations USING (destination)
|
||||
WHERE
|
||||
destination > $1
|
||||
AND (
|
||||
destinations.retry_last_ts IS NULL OR
|
||||
destinations.retry_last_ts + destinations.retry_interval < $2
|
||||
)
|
||||
ORDER BY destination
|
||||
LIMIT 25
|
||||
)
|
||||
SELECT destination FROM pdu_destinations
|
||||
UNION SELECT destination FROM to_device_destinations
|
||||
UNION SELECT destination FROM sticky_event_destinations
|
||||
ORDER BY destination
|
||||
LIMIT 25",
|
||||
&[&after, &now_ms],
|
||||
)
|
||||
.await?;
|
||||
rows.iter()
|
||||
.map(|row| Ok(row.try_get(0)?))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `mark_backlogged_sticky_events_after_catchup_transaction`: a
|
||||
/// catch-up transaction moved `destination` on from `old_position` to
|
||||
/// `new_position`, skipping events. Record each room with local sticky
|
||||
@@ -1055,6 +1248,53 @@ impl PyFederationStoreForTest {
|
||||
self.run(py, self.store.get_prev_events_for_room(&self.pool, room_id))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn get_backlogged_sticky_events_for_destination(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
destination: &str,
|
||||
limit: usize,
|
||||
now_ms: i64,
|
||||
) -> PyResult<Option<(String, i64, Vec<String>)>> {
|
||||
self.run(
|
||||
py,
|
||||
self.store.get_backlogged_sticky_events_for_destination(
|
||||
&self.pool,
|
||||
destination,
|
||||
limit,
|
||||
now_ms,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn mark_backlogged_sticky_events_sent(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
destination: &str,
|
||||
room_id: &str,
|
||||
position: i64,
|
||||
) -> PyResult<()> {
|
||||
self.run(
|
||||
py,
|
||||
self.store
|
||||
.mark_backlogged_sticky_events_sent(&self.pool, destination, room_id, position),
|
||||
)
|
||||
}
|
||||
|
||||
#[pyo3(signature = (after_destination, now_ms))]
|
||||
fn get_catch_up_outstanding_destinations(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
after_destination: Option<&str>,
|
||||
now_ms: i64,
|
||||
) -> PyResult<Vec<String>> {
|
||||
self.run(
|
||||
py,
|
||||
self.store
|
||||
.get_catch_up_outstanding_destinations(&self.pool, after_destination, now_ms),
|
||||
)
|
||||
}
|
||||
|
||||
fn mark_backlogged_sticky_events_after_catchup_transaction(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
|
||||
@@ -323,6 +323,15 @@ class FederationStoreForTest:
|
||||
self, destination: str, last_successful_stream_ordering: int
|
||||
) -> list[str]: ...
|
||||
def get_prev_events_for_room(self, room_id: str) -> list[str]: ...
|
||||
def get_backlogged_sticky_events_for_destination(
|
||||
self, destination: str, limit: int, now_ms: int
|
||||
) -> tuple[str, int, list[str]] | None: ...
|
||||
def mark_backlogged_sticky_events_sent(
|
||||
self, destination: str, room_id: str, position: int
|
||||
) -> None: ...
|
||||
def get_catch_up_outstanding_destinations(
|
||||
self, after_destination: str | None, now_ms: int
|
||||
) -> list[str]: ...
|
||||
def mark_backlogged_sticky_events_after_catchup_transaction(
|
||||
self,
|
||||
destination: str,
|
||||
|
||||
@@ -51,6 +51,7 @@ from synapse.synapse_rust.federation import (
|
||||
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
|
||||
@@ -118,7 +119,9 @@ class RustQueuesTestCase(unittest.HomeserverTestCase):
|
||||
def _make_queues(
|
||||
self, transactions: RustTransactionManager
|
||||
) -> RustDestinationQueues:
|
||||
return RustDestinationQueues(transactions, False, {DEST})
|
||||
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"]
|
||||
@@ -264,7 +267,13 @@ class RustQueuesTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
|
||||
class RustQueuesNativeTestCase(RustQueuesTestCase):
|
||||
"""The same, with the database work done through the native driver."""
|
||||
"""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()
|
||||
@@ -394,6 +403,57 @@ class RustQueuesNativeTestCase(RustQueuesTestCase):
|
||||
[self.event.content["body"]],
|
||||
)
|
||||
|
||||
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."""
|
||||
|
||||
@@ -31,12 +31,13 @@ from signedjson.key import (
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
from synapse.handlers.e2e_keys import SignatureListItem
|
||||
from synapse.replication.tcp.streams._base import StickyEventStreamPosition
|
||||
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 FederationStoreForTest
|
||||
from synapse.types import JsonDict
|
||||
from synapse.types import JsonDict, RoomID
|
||||
from synapse.util.clock import Clock
|
||||
from synapse.util.duration import Duration
|
||||
|
||||
@@ -540,3 +541,132 @@ class CatchUpStoreTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(self._backlog(), python, (old, new, sent, existing))
|
||||
# The cases did backlog something.
|
||||
self.assertNotEqual(python, [])
|
||||
|
||||
def _seed_backlog(self) -> list[int]:
|
||||
"""Sticky and plain events in each room, with every room backlogged
|
||||
for `DEST` from its start."""
|
||||
orderings = [self._send(self.rooms[i % 3], sticky=i % 2 == 0) for i in range(9)]
|
||||
self.get_success(
|
||||
self.store.mark_backlogged_sticky_events_after_catchup_transaction(
|
||||
DEST,
|
||||
old_last_successfully_sent_stream_ordering=0,
|
||||
new_last_successfully_sent_stream_ordering=orderings[-1] + 1,
|
||||
event_stream_orderings_sent_in_transaction=[],
|
||||
)
|
||||
)
|
||||
return orderings
|
||||
|
||||
def test_backlogged_sticky_events_for_destination(self) -> None:
|
||||
self._seed_backlog()
|
||||
now = self.clock.time_msec()
|
||||
for limit in (1, 2, 50):
|
||||
python = self.get_success(
|
||||
self.store.get_backlogged_sticky_events_for_destination(
|
||||
DEST, limit=limit
|
||||
)
|
||||
)
|
||||
assert python is not None
|
||||
room_id, position, event_ids = python
|
||||
self.assertEqual(
|
||||
self.rust.get_backlogged_sticky_events_for_destination(
|
||||
DEST, limit, now
|
||||
),
|
||||
(room_id.to_string(), position, event_ids),
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.rust.get_backlogged_sticky_events_for_destination(OTHER, 50, now)
|
||||
)
|
||||
|
||||
def test_backlogged_sticky_events_expire(self) -> None:
|
||||
"""Once the sticky events expire, the backlog is cleaned up."""
|
||||
self._seed_backlog()
|
||||
self.assertNotEqual(self._backlog(), [])
|
||||
later = self.clock.time_msec() + 60 * 60 * 1000
|
||||
self.assertIsNone(
|
||||
self.rust.get_backlogged_sticky_events_for_destination(DEST, 50, later)
|
||||
)
|
||||
self.assertEqual(self._backlog(), [])
|
||||
|
||||
def test_mark_backlogged_sticky_events_sent(self) -> None:
|
||||
self._seed_backlog()
|
||||
backlog = self.rust.get_backlogged_sticky_events_for_destination(
|
||||
DEST, 50, self.clock.time_msec()
|
||||
)
|
||||
assert backlog is not None
|
||||
room_id, position, _ = backlog
|
||||
|
||||
self.rust.mark_backlogged_sticky_events_sent(DEST, room_id, position)
|
||||
rust = self._backlog()
|
||||
self._clear_backlog()
|
||||
self._seed_backlog_rows(rust)
|
||||
self.get_success(
|
||||
self.store.mark_backlogged_sticky_events_sent(
|
||||
DEST, RoomID.from_string(room_id), StickyEventStreamPosition(position)
|
||||
)
|
||||
)
|
||||
self.assertEqual(self._backlog(), rust)
|
||||
self.assertIn((DEST, room_id, position + 1), rust)
|
||||
|
||||
def _seed_backlog_rows(self, rows: list[tuple[str, str, int]]) -> None:
|
||||
for destination, room_id, position in rows:
|
||||
self.get_success(
|
||||
self.store.db_pool.simple_upsert(
|
||||
"destination_rooms_sticky_events_backlog",
|
||||
{"destination": destination, "room_id": room_id},
|
||||
{"sticky_events_stream_position": position},
|
||||
)
|
||||
)
|
||||
|
||||
def test_catch_up_outstanding_destinations(self) -> None:
|
||||
# PDUs for `DEST` and `OTHER`, a to-device message for a third, a
|
||||
# sticky backlog for a fourth, and a fifth that is backed off.
|
||||
self._send(self.rooms[0], sticky=False)
|
||||
self.get_success(
|
||||
self.store.add_remote_messages_from_client_to_device_inbox(
|
||||
{
|
||||
"todevice.example.com": {
|
||||
"sender": "@alice:test",
|
||||
"type": "m.test",
|
||||
"message_id": "1",
|
||||
"messages": {"@u:todevice.example.com": {"D": {}}},
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
self._seed_backlog_rows([("sticky.example.com", self.rooms[0], 1)])
|
||||
self.get_success(
|
||||
self.store.store_destination_rooms_entries(
|
||||
["backoff.example.com"], self.rooms[0], 1_000_000
|
||||
)
|
||||
)
|
||||
now = self.clock.time_msec()
|
||||
self.get_success(
|
||||
self.store.set_destination_retry_timings(
|
||||
"backoff.example.com", now, now, 60_000
|
||||
)
|
||||
)
|
||||
|
||||
for after in (None, "", DEST, "zzz"):
|
||||
for at in (now, now + 120_000):
|
||||
# Python's reads the clock, so call its transaction directly.
|
||||
python = self.get_success(
|
||||
self.store.db_pool.runInteraction(
|
||||
"outstanding",
|
||||
self.store._get_catch_up_outstanding_destinations_txn,
|
||||
at,
|
||||
after,
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.rust.get_catch_up_outstanding_destinations(after, at),
|
||||
python,
|
||||
(after, at),
|
||||
)
|
||||
self.assertIn(
|
||||
"backoff.example.com",
|
||||
self.rust.get_catch_up_outstanding_destinations(None, now + 120_000),
|
||||
)
|
||||
self.assertNotIn(
|
||||
"backoff.example.com",
|
||||
self.rust.get_catch_up_outstanding_destinations(None, now),
|
||||
)
|
||||
Reference in new issue
Block a user