tuwunel_admin/query/feds/
head.rs1use std::{
2 cmp::Ordering,
3 collections::BTreeMap,
4 fmt::{Result as FmtResult, Write as _},
5 num::NonZeroUsize,
6 time::{Duration, Instant},
7};
8
9use futures::StreamExt;
10use ruma::{
11 OwnedEventId, OwnedRoomOrAliasId, OwnedUserId, UInt, UserId,
12 api::{
13 error::ErrorKind,
14 federation::membership::prepare_join_event::v1::{Request, Response},
15 },
16};
17use serde::Deserialize;
18use smallvec::SmallVec;
19use tuwunel_core::{Err, Error, Result, utils::time::Elapsed};
20use tuwunel_service::federation::feds::{Fault, Outcome};
21
22use super::{
23 SweepArgs, count_results, fault_message, markdown_cell, prepare, render_totals,
24 sorted_event_id_difference,
25};
26use crate::admin_command;
27
28pub(super) const WIDTH_DEFAULT: NonZeroUsize = NonZeroUsize::new(192).expect("192 is nonzero");
29
30type Extremities = SmallVec<[OwnedEventId; 1]>;
31
32#[derive(Deserialize)]
33struct Head {
34 depth: UInt,
35 prev_events: Extremities,
36}
37
38type Classes<'a> = BTreeMap<&'a [OwnedEventId], usize>;
39type ClassNumbers<'a> = BTreeMap<&'a [OwnedEventId], usize>;
40
41#[admin_command]
42pub(super) async fn feds_head(
43 &self,
44 room: OwnedRoomOrAliasId,
45 probe_user: Option<OwnedUserId>,
46 sweep: SweepArgs,
47) -> Result {
48 let prepared = prepare(self, &room, sweep, WIDTH_DEFAULT).await?;
49 let probe_user = match probe_user {
50 | Some(probe_user) => probe_user,
51 | None =>
52 UserId::parse_with_server_name("_feds_probe", self.services.globals.server_name())?,
53 };
54
55 if !self.services.globals.user_is_local(&probe_user) {
56 return Err!("The feds head probe user must be local.");
57 }
58
59 let latest = self
60 .services
61 .timeline
62 .latest_pdu_in_room(&prepared.room_id)
63 .await?;
64
65 let local_depth = latest.depth;
66 let local_extremities = self
67 .services
68 .state
69 .get_forward_extremities(&prepared.room_id)
70 .map(ToOwned::to_owned)
71 .collect::<Extremities>()
72 .await;
73
74 let local_extremities = normalize(local_extremities);
75 let versions: Vec<_> = self
76 .services
77 .config
78 .supported_room_versions()
79 .map(|(version, _stability)| version)
80 .collect();
81
82 let request_room = prepared.room_id.clone();
83 let started = Instant::now();
84 let outcomes = self
85 .services
86 .federation
87 .for_room(
88 &prepared.room_id,
89 move |_| Request {
90 room_id: request_room.clone(),
91 user_id: probe_user.clone(),
92 ver: versions.clone(),
93 },
94 prepared.opts,
95 )
96 .map(parse_outcome)
97 .collect::<Vec<_>>()
98 .await;
99
100 let total = started.elapsed();
101
102 let output = render(outcomes, local_depth, &local_extremities, total);
103
104 self.write_str(&output).await
105}
106
107fn normalize(mut event_ids: Extremities) -> Extremities {
108 event_ids.sort_unstable();
109 event_ids.dedup();
110
111 event_ids
112}
113
114fn parse_outcome(outcome: Outcome<Response>) -> Outcome<Head> {
115 let result = outcome.result.and_then(|response| {
116 serde_json::from_str(response.event.get())
117 .map_err(Error::from)
118 .map_err(Fault::Error)
119 .map(normalize_head)
120 });
121
122 Outcome {
123 origin: outcome.origin,
124 elapsed: outcome.elapsed,
125 result,
126 }
127}
128
129fn normalize_head(mut head: Head) -> Head {
130 head.prev_events = normalize(head.prev_events);
131
132 head
133}
134
135fn render(
136 mut outcomes: Vec<Outcome<Head>>,
137 local_depth: UInt,
138 local_extremities: &[OwnedEventId],
139 total: Duration,
140) -> String {
141 outcomes.sort_by(|left, right| match (&left.result, &right.result) {
142 | (Err(_), Err(_)) => left.origin.cmp(&right.origin),
143 | (Ok(_), Err(_)) => Ordering::Less,
144 | (Err(_), Ok(_)) => Ordering::Greater,
145 | (Ok(left_head), Ok(right_head)) => right_head
146 .depth
147 .cmp(&left_head.depth)
148 .then_with(|| left.origin.cmp(&right.origin)),
149 });
150
151 let mut output = String::new();
152
153 render_into(&mut output, &outcomes, local_depth, local_extremities, total)
154 .expect("writing to a String cannot fail");
155
156 output
157}
158
159fn render_into(
160 output: &mut String,
161 outcomes: &[Outcome<Head>],
162 local_depth: UInt,
163 local_extremities: &[OwnedEventId],
164 total: Duration,
165) -> FmtResult {
166 let classes: Classes<'_> = outcomes
167 .iter()
168 .filter_map(|outcome| {
169 outcome
170 .result
171 .as_ref()
172 .ok()
173 .map(|head| head.prev_events.as_slice())
174 })
175 .fold(BTreeMap::new(), |mut classes, extremities| {
176 classes
177 .entry(extremities)
178 .and_modify(|count| *count = count.saturating_add(1))
179 .or_insert(1);
180
181 classes
182 });
183
184 let class_numbers: ClassNumbers<'_> = classes
185 .keys()
186 .copied()
187 .enumerate()
188 .map(|(class, extremities)| (extremities, class.saturating_add(1)))
189 .collect();
190
191 let incompatible = outcomes
192 .iter()
193 .filter(|outcome| {
194 matches!(
195 &outcome.result,
196 Err(Fault::Error(error))
197 if matches!(
198 error.kind(),
199 ErrorKind::IncompatibleRoomVersion { .. }
200 | ErrorKind::UnsupportedRoomVersion
201 )
202 )
203 })
204 .count();
205
206 writeln!(
207 output,
208 "Remote template depth is one greater than the selected room head. {incompatible} \
209 servers reported an incompatible room version.\n"
210 )?;
211
212 writeln!(output, "| origin | depth | extremities | class | elapsed | fault |")?;
213 writeln!(output, "| :--- | ---: | ---: | ----: | ---: | :--- |")?;
214 writeln!(output, "| local | {local_depth} | {} | local | | |", local_extremities.len(),)?;
215 for outcome in outcomes {
216 match &outcome.result {
217 | Ok(head) => writeln!(
218 output,
219 "| {} | {} | {} | {} | {} | |",
220 outcome.origin,
221 head.depth,
222 head.prev_events.len(),
223 class_numbers
224 .get(head.prev_events.as_slice())
225 .copied()
226 .unwrap_or_default(),
227 Elapsed::from(outcome.elapsed),
228 )?,
229 | Err(fault @ Fault::NotAttempted) => writeln!(
230 output,
231 "| {} | | | | | {} |",
232 outcome.origin,
233 markdown_cell(&fault_message(fault)),
234 )?,
235 | Err(fault) => writeln!(
236 output,
237 "| {} | | | | {} | {} |",
238 outcome.origin,
239 Elapsed::from(outcome.elapsed),
240 markdown_cell(&fault_message(fault)),
241 )?,
242 }
243 }
244
245 writeln!(output, "\n### Extremity equivalence classes\n")?;
246 writeln!(output, "| class | servers | size | vs local |")?;
247 writeln!(output, "| ----: | ------: | ---: | :--- |")?;
248 for (class, (extremities, count)) in classes.iter().enumerate() {
249 writeln!(
250 output,
251 "| {} | {} | {} | {} |",
252 class.saturating_add(1),
253 count,
254 extremities.len(),
255 sorted_event_id_difference(extremities, local_extremities),
256 )?;
257 }
258
259 for (class, extremities) in classes.keys().enumerate() {
260 writeln!(output, "\n#### Extremity class {}\n\n```", class.saturating_add(1))?;
261 for event_id in *extremities {
262 writeln!(output, "{event_id}")?;
263 }
264
265 writeln!(output, "```")?;
266 }
267
268 render_totals(output, count_results(outcomes), total)
269}
270
271#[cfg(test)]
272mod tests {
273 use ruma::server_name;
274
275 use super::*;
276
277 #[test]
278 fn undispatched_destination_has_no_elapsed_time() {
279 let outcomes: Vec<Outcome<Head>> = vec![Outcome {
280 origin: server_name!("skipped.example").to_owned(),
281 elapsed: Duration::ZERO,
282 result: Err(Fault::NotAttempted),
283 }];
284
285 let output = render(outcomes, UInt::from(1_u8), &[], Duration::ZERO);
286
287 assert!(
288 output
289 .contains("| skipped.example | | | | | sweep budget exhausted before dispatch |")
290 );
291
292 assert!(output.ends_with("\n0 results in 0ns.\n"));
293 }
294}