Skip to main content

tuwunel_admin/debug/
prev_walk_rooms.rs

1use std::{
2	cmp::Ordering,
3	fmt::Write,
4	time::{Duration, Instant},
5};
6
7use futures::StreamExt;
8use ruma::{OwnedRoomId, RoomId, ServerName};
9use tuwunel_core::{
10	Result,
11	itertools::Itertools,
12	utils::{
13		BoolExt,
14		math::usize_from_u64_truncated,
15		string::{collect_stream, markdown_cell, plural},
16		time::{Elapsed, format as format_time},
17	},
18};
19use tuwunel_service::rooms::event_handler::{PrevWalkOutcome, PrevWalkPass, PrevWalkRoom};
20
21use crate::admin_command;
22
23const ROOMS_HEAD: &str = "| room | passes | appended | not appended | failed | cancelled | \
24                          fetch failed | fetch cancelled | capped | prevs | unprocessed | fetch \
25                          | upgrade |\n| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: \
26                          | ---: | ---: | ---: | ---: | ---: |";
27
28const PASSES_HEAD: &str = "| ended | event | origin | outcome | prevs | unprocessed | capped | \
29                           fetch | upgrade |\n| :--- | :--- | :--- | :--- | ---: | ---: | :--- \
30                           | ---: | ---: |";
31
32#[admin_command]
33pub(super) async fn prev_walk_rooms(&self, room_id: Option<OwnedRoomId>, limit: usize) -> Result {
34	let event_handler = &self.services.event_handler;
35	let output = match room_id {
36		| None => {
37			let started = Instant::now();
38			let rooms = event_handler.prev_walk_rooms().await;
39
40			render_rooms(rooms, limit, started.elapsed())?
41		},
42		| Some(room_id) => {
43			let passes: Vec<_> = event_handler
44				.prev_walk_passes(&room_id)
45				.take(limit)
46				.collect()
47				.await;
48
49			collect_stream(|out| write_passes(out, &room_id, &passes))?
50		},
51	};
52
53	self.write_str(&output).await
54}
55
56/// Ranks the rooms busiest first and renders the top `limit`.
57///
58/// The header counts every room swept and the footer every pass, whatever the
59/// limit leaves out.
60fn render_rooms(
61	rooms: impl ExactSizeIterator<Item = PrevWalkRoom>,
62	limit: usize,
63	sweep: Duration,
64) -> Result<String> {
65	let count = rooms.len();
66	let sorted = rooms.sorted_unstable_by(busiest_first);
67	let ranked = sorted.as_slice();
68	let passes = ranked
69		.iter()
70		.fold(0, |passes: u64, room| passes.saturating_add(room.passes));
71
72	let top = ranked.get(..limit).unwrap_or(ranked);
73
74	collect_stream(|out| write_rooms(out, count, passes, top, sweep))
75}
76
77fn busiest_first(a: &PrevWalkRoom, b: &PrevWalkRoom) -> Ordering {
78	b.passes
79		.cmp(&a.passes)
80		.then_with(|| a.room_id.cmp(&b.room_id))
81}
82
83fn write_rooms(
84	out: &mut dyn Write,
85	count: usize,
86	passes: u64,
87	top: &[PrevWalkRoom],
88	sweep: Duration,
89) -> Result {
90	let noun = plural(count, "room", "rooms");
91
92	writeln!(out, "{count} {noun} with recorded prev walks.")?;
93	if !top.is_empty() {
94		writeln!(out, "\n{ROOMS_HEAD}")?;
95		top.iter()
96			.try_for_each(|room| write_room(out, room))?;
97	}
98
99	let noun = plural(usize_from_u64_truncated(passes), "pass", "passes");
100
101	writeln!(out, "\n{passes} {noun} in {}.", Elapsed::from(sweep))?;
102
103	Ok(())
104}
105
106fn write_room(out: &mut dyn Write, room: &PrevWalkRoom) -> Result {
107	let PrevWalkRoom {
108		room_id,
109		passes,
110		appended,
111		not_appended,
112		failed,
113		cancelled,
114		fetch_failed,
115		fetch_cancelled,
116		capped,
117		prevs,
118		unprocessed,
119		fetch,
120		upgrade,
121	} = room;
122
123	writeln!(
124		out,
125		"| {} | {passes} | {appended} | {not_appended} | {failed} | {cancelled} | \
126		 {fetch_failed} | {fetch_cancelled} | {capped} | {prevs} | {unprocessed} | {} | {} |",
127		markdown_cell(room_id.as_str()),
128		Elapsed::from(*fetch),
129		Elapsed::from(*upgrade),
130	)?;
131
132	Ok(())
133}
134
135fn write_passes(out: &mut dyn Write, room_id: &RoomId, passes: &[PrevWalkPass]) -> Result {
136	let count = passes.len();
137	let noun = plural(count, "pass", "passes");
138
139	writeln!(out, "{count} latest {noun} in {room_id}.")?;
140	if count == 0 {
141		return Ok(());
142	}
143
144	writeln!(out, "\n{PASSES_HEAD}")?;
145	passes
146		.iter()
147		.try_for_each(|pass| write_pass(out, pass))
148}
149
150fn write_pass(out: &mut dyn Write, pass: &PrevWalkPass) -> Result {
151	let PrevWalkPass {
152		ended,
153		event_id,
154		origin,
155		outcome,
156		prevs,
157		unprocessed,
158		capped,
159		fetch,
160		upgrade,
161	} = pass;
162
163	let ended = format_time(*ended, "%+");
164	let origin = origin.as_deref().map_or("", ServerName::as_str);
165	let outcome = outcome.map_or("unknown", PrevWalkOutcome::name);
166	let capped = capped.copy_or("no", "yes");
167
168	writeln!(
169		out,
170		"| {ended} | {} | {} | {outcome} | {prevs} | {unprocessed} | {capped} | {} | {} |",
171		markdown_cell(event_id.as_str()),
172		markdown_cell(origin),
173		Elapsed::from(*fetch),
174		Elapsed::from(*upgrade),
175	)?;
176
177	Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182	use ruma::{owned_event_id, owned_server_name, room_id};
183	use tuwunel_core::utils::time::timepoint_from_epoch;
184
185	use super::*;
186
187	#[test]
188	fn render_rooms_lists_the_busiest_first() {
189		let rooms = [
190			PrevWalkRoom {
191				passes: 1,
192				appended: 1,
193				..PrevWalkRoom::empty(room_id!("!aaa:example.org"))
194			},
195			PrevWalkRoom {
196				passes: 3,
197				not_appended: 1,
198				failed: 1,
199				fetch_failed: 1,
200				prevs: 4,
201				unprocessed: 2,
202				fetch: Duration::from_millis(1_500),
203				upgrade: Duration::from_millis(250),
204				..PrevWalkRoom::empty(room_id!("!busy:example.org"))
205			},
206			PrevWalkRoom {
207				passes: 3,
208				cancelled: 2,
209				fetch_cancelled: 1,
210				capped: 1,
211				prevs: 60,
212				..PrevWalkRoom::empty(room_id!("!busier:example.org"))
213			},
214		];
215
216		let output = render_rooms(rooms.into_iter(), 2, Duration::from_micros(1_500))
217			.expect("the rooms render");
218
219		assert_eq!(output.lines().collect::<Vec<_>>(), [
220			"3 rooms with recorded prev walks.",
221			"",
222			"| room | passes | appended | not appended | failed | cancelled | fetch failed | \
223			 fetch cancelled | capped | prevs | unprocessed | fetch | upgrade |",
224			"| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | \
225			 ---: | ---: |",
226			"| !busier:example.org | 3 | 0 | 0 | 0 | 2 | 0 | 1 | 1 | 60 | 0 | 0ns | 0ns |",
227			"| !busy:example.org | 3 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 4 | 2 | 1.5s | 250ms |",
228			"",
229			"7 passes in 1.5ms.",
230		]);
231	}
232
233	#[test]
234	fn render_rooms_omits_an_empty_table() {
235		let output =
236			render_rooms([].into_iter(), 20, Duration::from_millis(3)).expect("the rooms render");
237
238		assert_eq!(output, "0 rooms with recorded prev walks.\n\n0 passes in 3ms.\n");
239	}
240
241	#[test]
242	fn render_rooms_omits_the_table_at_limit_zero() {
243		let rooms = [
244			PrevWalkRoom {
245				passes: 3,
246				..PrevWalkRoom::empty(room_id!("!busy:example.org"))
247			},
248			PrevWalkRoom {
249				passes: 1,
250				..PrevWalkRoom::empty(room_id!("!quiet:example.org"))
251			},
252		];
253
254		let output = render_rooms(rooms.into_iter(), 0, Duration::from_millis(1))
255			.expect("the rooms render");
256
257		assert_eq!(output, "2 rooms with recorded prev walks.\n\n4 passes in 1ms.\n");
258	}
259
260	#[test]
261	fn write_passes_formats_each_pass() {
262		let ended = |millis| {
263			timepoint_from_epoch(Duration::from_millis(millis))
264				.expect("the test time is in range")
265		};
266
267		let passes = [
268			PrevWalkPass {
269				ended: ended(1_700_000_000_250),
270				event_id: owned_event_id!("$later"),
271				origin: Some(owned_server_name!("remote.example")),
272				outcome: Some(PrevWalkOutcome::Cancelled),
273				prevs: 37,
274				unprocessed: 0,
275				capped: true,
276				fetch: Duration::from_millis(850),
277				upgrade: Duration::from_millis(14_030),
278			},
279			PrevWalkPass {
280				ended: ended(1_700_000_000_000),
281				event_id: owned_event_id!("$earlier"),
282				origin: None,
283				outcome: None,
284				prevs: 0,
285				unprocessed: 0,
286				capped: false,
287				fetch: Duration::from_millis(2),
288				upgrade: Duration::ZERO,
289			},
290		];
291
292		let room_id = room_id!("!room:example.org");
293		let output =
294			collect_stream(|out| write_passes(out, room_id, &passes)).expect("the passes render");
295
296		assert_eq!(output.lines().collect::<Vec<_>>(), [
297			"2 latest passes in !room:example.org.",
298			"",
299			"| ended | event | origin | outcome | prevs | unprocessed | capped | fetch | \
300			 upgrade |",
301			"| :--- | :--- | :--- | :--- | ---: | ---: | :--- | ---: | ---: |",
302			"| 2023-11-14T22:13:20.250+00:00 | $later | remote.example | cancelled | 37 | 0 | \
303			 yes | 850ms | 14.03s |",
304			"| 2023-11-14T22:13:20+00:00 | $earlier |  | unknown | 0 | 0 | no | 2ms | 0ns |",
305		]);
306	}
307
308	#[test]
309	fn write_passes_omits_an_empty_table() {
310		let room_id = room_id!("!room:example.org");
311		let output =
312			collect_stream(|out| write_passes(out, room_id, &[])).expect("the passes render");
313
314		assert_eq!(output, "0 latest passes in !room:example.org.\n");
315	}
316}