Skip to main content

tuwunel_admin/query/feds/
head.rs

1use 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}