tuwunel_admin/query/feds/
state.rs1use 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}