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
56fn 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}