Skip to main content

tuwunel_api/client/admin/federation/
list_destinations.rs

1use std::cmp::Ordering;
2
3use axum::extract::State;
4use futures::StreamExt;
5use ruma::{ServerName, api::Direction};
6use synapse_admin_api::federation::{
7	Destination,
8	list_destinations::v1::{DestinationSortOrder, Request, Response},
9};
10use tuwunel_core::{
11	Err, Result,
12	utils::{
13		ReadyExt,
14		math::{ruma_from_usize, usize_from_ruma},
15	},
16};
17
18use super::destination_from_backoff;
19use crate::{Ruma, client::admin::require_admin};
20
21/// # `GET /_synapse/admin/v1/federation/destinations`
22pub(crate) async fn admin_list_destinations_route(
23	State(services): State<crate::State>,
24	body: Ruma<Request>,
25) -> Result<Response> {
26	require_admin(&services, body.sender_user()).await?;
27
28	let default_order = DestinationSortOrder::Destination;
29	let order_by = body.order_by.as_ref().unwrap_or(&default_order);
30
31	if !matches!(
32		order_by,
33		DestinationSortOrder::Destination
34			| DestinationSortOrder::RetryLastTs
35			| DestinationSortOrder::RetryInterval
36			| DestinationSortOrder::FailureTs
37			| DestinationSortOrder::LastSuccessfulStreamOrdering
38	) {
39		return Err!(Request(InvalidParam("Unknown order_by parameter")));
40	}
41
42	let dir = body.dir.unwrap_or(Direction::Forward);
43	let from = body.from.map_or(0, usize_from_ruma);
44	let limit = body.limit.map_or(100, usize_from_ruma);
45
46	let needle = body
47		.destination
48		.as_deref()
49		.filter(|needle| !needle.is_empty());
50
51	let mut backoffs = services.federation.peer_backoffs().await;
52
53	let own_server = services.globals.server_name();
54
55	// Self is never a federation destination; drop any stray self-row.
56	backoffs.remove(own_server);
57
58	let mut rows: Vec<Destination> = services
59		.state_cache
60		.servers()
61		.ready_filter(|server| *server != own_server && name_matches(server, needle))
62		.map(|server| destination_from_backoff(server.to_owned(), backoffs.remove(server)))
63		.collect()
64		.await;
65
66	rows.extend(
67		backoffs
68			.into_iter()
69			.filter(|(server, _)| name_matches(server, needle))
70			.map(|(server, backoff)| destination_from_backoff(server, Some(backoff))),
71	);
72
73	rows.sort_unstable_by(|a, b| {
74		let ordering = destination_ordering(order_by, a, b);
75
76		let ordering = match dir {
77			| Direction::Forward => ordering,
78			| Direction::Backward => ordering.reverse(),
79		};
80
81		ordering.then_with(|| a.destination.cmp(&b.destination))
82	});
83
84	let matched_count = rows.len();
85	let total = ruma_from_usize(matched_count);
86
87	let destinations: Vec<Destination> = rows.into_iter().skip(from).take(limit).collect();
88
89	let end = from.saturating_add(destinations.len());
90	let next_token = (end < matched_count).then(|| end.to_string());
91
92	Ok(Response { destinations, total, next_token })
93}
94
95/// Case-insensitive substring match of the server name, mirroring Synapse's
96/// `LIKE %needle%`. A `None` or empty needle matches every server.
97fn name_matches(server: &ServerName, needle: Option<&str>) -> bool {
98	let Some(needle) = needle.filter(|needle| !needle.is_empty()) else {
99		return true;
100	};
101
102	server
103		.as_bytes()
104		.windows(needle.len())
105		.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
106}
107
108/// The `Destination` and the doc-hidden `_Custom` variant both fall through to
109/// the server-name tiebreak.
110fn destination_ordering(
111	order_by: &DestinationSortOrder,
112	a: &Destination,
113	b: &Destination,
114) -> Ordering {
115	match order_by {
116		| DestinationSortOrder::RetryLastTs => a.retry_last_ts.cmp(&b.retry_last_ts),
117		| DestinationSortOrder::RetryInterval => a.retry_interval.cmp(&b.retry_interval),
118		| DestinationSortOrder::FailureTs => a.failure_ts.cmp(&b.failure_ts),
119		| DestinationSortOrder::LastSuccessfulStreamOrdering => a
120			.last_successful_stream_ordering
121			.cmp(&b.last_successful_stream_ordering),
122		| _ => a.destination.cmp(&b.destination),
123	}
124}
125
126#[cfg(test)]
127mod tests {
128	use std::cmp::Ordering;
129
130	use ruma::{UInt, server_name, uint};
131	use tuwunel_service::federation::PeerBackoff;
132
133	use super::{
134		Destination, DestinationSortOrder, destination_from_backoff, destination_ordering,
135		name_matches,
136	};
137
138	#[test]
139	fn healthy_destination_is_all_zeros() {
140		let row = destination_from_backoff(server_name!("matrix.org").to_owned(), None);
141
142		assert_eq!(row.retry_last_ts, uint!(0));
143		assert_eq!(row.retry_interval, uint!(0));
144		assert_eq!(row.failure_ts, None);
145		assert_eq!(row.last_successful_stream_ordering, None);
146	}
147
148	#[test]
149	fn failing_destination_maps_seconds_to_millis() {
150		let backoff = PeerBackoff {
151			anchor_secs: 10,
152			oldest_secs: 5,
153			delay_secs: 60,
154		};
155
156		let row = destination_from_backoff(server_name!("matrix.org").to_owned(), Some(backoff));
157
158		assert_eq!(row.retry_last_ts, uint!(10_000));
159		assert_eq!(row.retry_interval, uint!(60_000));
160		assert_eq!(row.failure_ts, Some(uint!(5_000)));
161		assert_eq!(row.last_successful_stream_ordering, None);
162	}
163
164	#[test]
165	fn millis_saturates() {
166		let backoff = PeerBackoff {
167			anchor_secs: u64::MAX,
168			oldest_secs: 0,
169			delay_secs: 0,
170		};
171
172		let row = destination_from_backoff(server_name!("matrix.org").to_owned(), Some(backoff));
173
174		assert_eq!(row.retry_last_ts, UInt::MAX);
175	}
176
177	#[test]
178	fn name_matches_is_ascii_case_insensitive() {
179		let server = server_name!("matrix.org");
180
181		assert!(name_matches(server, Some("MATRIX")));
182		assert!(name_matches(server, Some("matrix")));
183		assert!(!name_matches(server_name!("example.com"), Some("matrix")));
184		assert!(name_matches(server, None));
185		assert!(name_matches(server, Some("")));
186		assert!(!name_matches(server, Some("matrix.organization")));
187	}
188
189	#[test]
190	fn ordering_selects_the_field() {
191		let low = Destination {
192			retry_last_ts: uint!(1),
193			retry_interval: uint!(9),
194			failure_ts: None,
195			..Destination::new(server_name!("a.example").to_owned())
196		};
197
198		let high = Destination {
199			retry_last_ts: uint!(2),
200			retry_interval: uint!(8),
201			failure_ts: Some(uint!(1)),
202			last_successful_stream_ordering: Some(uint!(1)),
203			..Destination::new(server_name!("b.example").to_owned())
204		};
205
206		assert_eq!(
207			destination_ordering(&DestinationSortOrder::RetryLastTs, &low, &high),
208			Ordering::Less
209		);
210		assert_eq!(
211			destination_ordering(&DestinationSortOrder::RetryInterval, &low, &high),
212			Ordering::Greater
213		);
214		assert_eq!(
215			destination_ordering(&DestinationSortOrder::FailureTs, &low, &high),
216			Ordering::Less
217		);
218		assert_eq!(
219			destination_ordering(
220				&DestinationSortOrder::LastSuccessfulStreamOrdering,
221				&low,
222				&high,
223			),
224			Ordering::Less
225		);
226		assert_eq!(
227			destination_ordering(&DestinationSortOrder::Destination, &low, &high),
228			Ordering::Less
229		);
230	}
231}