tuwunel_api/client/admin/federation/
list_destinations.rs1use 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
21pub(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 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
95fn 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
108fn 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}