Skip to main content

tuwunel_admin/query/feds/
state.rs

1use std::{
2	borrow::Cow,
3	collections::BTreeMap,
4	fmt::{Error as FmtError, Write as _},
5	iter::once,
6	num::NonZeroUsize,
7	time::{Duration, Instant},
8};
9
10use futures::StreamExt;
11use ruma::{
12	OwnedEventId, OwnedRoomOrAliasId, OwnedServerName, ServerName,
13	api::federation::event::get_room_state_ids::v1::Request,
14};
15use tuwunel_core::Result as CoreResult;
16use tuwunel_service::federation::feds::{Fault, Grid, Origins, OutcomeExt};
17
18use super::{
19	SweepArgs, fault_message, markdown_cell, prepare, render_totals, sorted_event_id_difference,
20};
21use crate::admin_command;
22
23pub(super) const WIDTH_DEFAULT: NonZeroUsize = NonZeroUsize::new(16).expect("16 is nonzero");
24
25type RenderResult<T = ()> = Result<T, FmtError>;
26type SetClass<'a> = (&'a [OwnedEventId], &'a Origins);
27type OriginClasses<'a> = BTreeMap<&'a ServerName, usize>;
28
29struct Render<'a> {
30	event_id: &'a OwnedEventId,
31	grid: &'a Grid<StateSet>,
32	local_state: &'a [OwnedEventId],
33	auth_chain: bool,
34	full: bool,
35	total: Duration,
36}
37
38#[derive(Eq, Ord, PartialEq, PartialOrd)]
39enum StateSet {
40	State(Vec<OwnedEventId>),
41	Auth(Vec<OwnedEventId>),
42}
43
44#[derive(Clone, Copy)]
45enum SetKind {
46	State,
47	Auth,
48}
49
50enum OriginRow<'a> {
51	Class(usize),
52	Empty,
53	Fault(&'a Fault),
54}
55
56#[admin_command]
57pub(super) async fn feds_state(
58	&self,
59	room: OwnedRoomOrAliasId,
60	at: Option<OwnedEventId>,
61	auth_chain: bool,
62	full: bool,
63	sweep: SweepArgs,
64) -> CoreResult {
65	let prepared = prepare(self, &room, sweep, WIDTH_DEFAULT).await?;
66	let event_id = match at {
67		| Some(event_id) => event_id,
68		| None =>
69			self.services
70				.timeline
71				.latest_pdu_in_room(&prepared.room_id)
72				.await?
73				.event_id,
74	};
75
76	let shortstatehash = self
77		.services
78		.state
79		.pdu_shortstatehash(&event_id)
80		.await?;
81
82	let local_state = self
83		.services
84		.state_accessor
85		.state_full_ids(shortstatehash)
86		.map(|(_shortstatekey, event_id)| event_id)
87		.collect::<Vec<_>>()
88		.await;
89
90	let local_state = normalize(local_state);
91	let request_room = prepared.room_id.clone();
92	let request_event = event_id.clone();
93	let started = Instant::now();
94	let grid = self
95		.services
96		.federation
97		.for_room(
98			&prepared.room_id,
99			move |_| Request {
100				room_id: request_room.clone(),
101				event_id: request_event.clone(),
102			},
103			prepared.opts,
104		)
105		.grid(move |response| {
106			let state = StateSet::State(normalize(response.pdu_ids));
107			let auth = auth_chain.then(|| StateSet::Auth(normalize(response.auth_chain_ids)));
108
109			once(state).chain(auth)
110		})
111		.await;
112
113	let total = started.elapsed();
114
115	let output = render(&event_id, &grid, &local_state, auth_chain, full, total);
116
117	self.write_str(&output).await
118}
119
120fn normalize(mut event_ids: Vec<OwnedEventId>) -> Vec<OwnedEventId> {
121	event_ids.sort_unstable();
122	event_ids.dedup();
123
124	event_ids
125}
126
127fn render(
128	event_id: &OwnedEventId,
129	grid: &Grid<StateSet>,
130	local_state: &[OwnedEventId],
131	auth_chain: bool,
132	full: bool,
133	total: Duration,
134) -> String {
135	let mut output = String::new();
136	let args = Render {
137		event_id,
138		grid,
139		local_state,
140		auth_chain,
141		full,
142		total,
143	};
144
145	render_into(&mut output, args).expect("writing to a String cannot fail");
146
147	output
148}
149
150fn render_into(
151	output: &mut String,
152	Render {
153		event_id,
154		grid,
155		local_state,
156		auth_chain,
157		full,
158		total,
159	}: Render<'_>,
160) -> RenderResult {
161	writeln!(output, "State identifiers before `{event_id}`.")?;
162
163	let state_origins = render_classes(
164		output,
165		"State",
166		classes_for(grid, SetKind::State),
167		Some(local_state),
168		full,
169	)?;
170
171	let auth_origins = auth_chain
172		.then(|| {
173			render_classes(output, "Auth chain", classes_for(grid, SetKind::Auth), None, full)
174		})
175		.transpose()?;
176
177	let rows: BTreeMap<OwnedServerName, OriginRow<'_>> = state_origins
178		.iter()
179		.map(|(origin, class)| ((*origin).to_owned(), OriginRow::Class(*class)))
180		.chain(
181			grid.empty
182				.iter()
183				.cloned()
184				.map(|origin| (origin, OriginRow::Empty)),
185		)
186		.chain(
187			grid.faults
188				.iter()
189				.map(|(origin, fault)| (origin.clone(), OriginRow::Fault(fault))),
190		)
191		.collect();
192
193	writeln!(output, "\n| origin | state class | auth class | fault |")?;
194	writeln!(output, "| :--- | ----: | ----: | :--- |")?;
195	for (origin, row) in rows {
196		match row {
197			| OriginRow::Empty => writeln!(output, "| {origin} | | | empty response |")?,
198			| OriginRow::Fault(fault) =>
199				writeln!(output, "| {origin} | | | {} |", markdown_cell(&fault_message(fault)),)?,
200			| OriginRow::Class(class) => {
201				let auth_class = auth_origins
202					.as_ref()
203					.and_then(|classes| classes.get::<ServerName>(origin.as_ref()));
204
205				match auth_class {
206					| None => writeln!(output, "| {origin} | {class} | | |")?,
207					| Some(auth_class) =>
208						writeln!(output, "| {origin} | {class} | {auth_class} | |")?,
209				}
210			},
211		}
212	}
213
214	let results = state_origins
215		.len()
216		.saturating_add(grid.empty.len());
217
218	render_totals(output, results, total)
219}
220
221fn classes_for(
222	grid: &Grid<StateSet>,
223	kind: SetKind,
224) -> impl Iterator<Item = SetClass<'_>> + Clone {
225	grid.data
226		.iter()
227		.filter_map(move |(set, origins)| match (set, kind) {
228			| (StateSet::State(ids), SetKind::State) | (StateSet::Auth(ids), SetKind::Auth) =>
229				Some((ids.as_slice(), origins)),
230			| _ => None,
231		})
232}
233
234fn render_classes<'a, I>(
235	output: &mut String,
236	title: &str,
237	classes: I,
238	local: Option<&[OwnedEventId]>,
239	full: bool,
240) -> RenderResult<OriginClasses<'a>>
241where
242	I: Iterator<Item = SetClass<'a>> + Clone,
243{
244	writeln!(output, "\n### {title} equivalence classes\n")?;
245	writeln!(output, "| class | servers | size | vs ours |")?;
246	writeln!(output, "| ----: | ------: | ---: | :--- |")?;
247	for (class, (ids, origins)) in classes.clone().enumerate() {
248		let comparison =
249			local.map_or(Cow::Borrowed("n/a"), |local| sorted_event_id_difference(ids, local));
250
251		writeln!(
252			output,
253			"| {} | {} | {} | {comparison} |",
254			class.saturating_add(1),
255			origins.len(),
256			ids.len(),
257		)?;
258	}
259
260	if full {
261		for (class, (ids, _origins)) in classes.clone().enumerate() {
262			writeln!(output, "\n#### {title} class {}\n\n```", class.saturating_add(1))?;
263			for event_id in ids {
264				writeln!(output, "{event_id}")?;
265			}
266
267			writeln!(output, "```")?;
268		}
269	}
270
271	let origin_classes: OriginClasses<'_> = classes
272		.enumerate()
273		.flat_map(|(class, (_ids, origins))| {
274			origins
275				.iter()
276				.map(move |origin| (origin.as_ref(), class.saturating_add(1)))
277		})
278		.collect();
279
280	Ok(origin_classes)
281}