1use std::time::Duration;
16
17use serde::Serialize;
18
19use crate::blocking::BlockingTaskSnapshot;
20use crate::task_group::TaskGroupId;
21use crate::task_group::TaskId;
22use crate::task_group::TaskKind;
23use crate::task_group::TaskState;
24
25#[derive(Debug, Serialize)]
27pub struct ShutdownReport {
28 pub name: String,
30 #[serde(with = "duration_millis")]
32 pub elapsed: Duration,
33 pub completed: usize,
35 pub cancelled: usize,
37 pub aborted: usize,
39 pub failed: usize,
41 pub panicked: usize,
43 pub timed_out: usize,
46 pub leaked: usize,
48 pub blocking_still_running: usize,
50 pub children: Vec<ShutdownReport>,
52 pub remaining_tasks: Vec<TaskSnapshot>,
55 pub remaining_tasks_omitted: usize,
58 pub blocking_tasks: Vec<BlockingTaskSnapshot>,
60 pub annotations: Vec<ShutdownAnnotation>,
62}
63
64impl Clone for ShutdownReport {
65 fn clone(&self) -> Self {
66 let mut copy = self.clone_local();
67 let mut pending = vec![(self, &mut copy)];
68 while let Some((source, target)) = pending.pop() {
69 target.children = source.children.iter().map(Self::clone_local).collect();
70 pending.extend(source.children.iter().zip(target.children.iter_mut()));
71 }
72 copy
73 }
74}
75
76impl ShutdownReport {
77 fn clone_local(&self) -> Self {
78 Self {
79 name: self.name.clone(),
80 elapsed: self.elapsed,
81 completed: self.completed,
82 cancelled: self.cancelled,
83 aborted: self.aborted,
84 failed: self.failed,
85 panicked: self.panicked,
86 timed_out: self.timed_out,
87 leaked: self.leaked,
88 blocking_still_running: self.blocking_still_running,
89 children: Vec::new(),
90 remaining_tasks: self.remaining_tasks.clone(),
91 remaining_tasks_omitted: self.remaining_tasks_omitted,
92 blocking_tasks: self.blocking_tasks.clone(),
93 annotations: self.annotations.clone(),
94 }
95 }
96
97 pub fn new(name: impl Into<String>, elapsed: Duration) -> Self {
99 Self {
100 name: name.into(),
101 elapsed,
102 completed: 0,
103 cancelled: 0,
104 aborted: 0,
105 failed: 0,
106 panicked: 0,
107 timed_out: 0,
108 leaked: 0,
109 blocking_still_running: 0,
110 children: Vec::new(),
111 remaining_tasks: Vec::new(),
112 remaining_tasks_omitted: 0,
113 blocking_tasks: Vec::new(),
114 annotations: Vec::new(),
115 }
116 }
117
118 pub const REMAINING_TASKS_LIMIT: usize = 64;
123
124 pub(crate) fn push_remaining_task(&mut self, task: TaskSnapshot) {
125 if self.remaining_tasks.len() < Self::REMAINING_TASKS_LIMIT {
126 self.remaining_tasks.push(task);
127 } else {
128 self.remaining_tasks_omitted += 1;
129 }
130 }
131
132 pub fn is_healthy(&self) -> bool {
134 let mut pending = vec![self];
135 while let Some(report) = pending.pop() {
136 if report.leaked != 0
137 || report.failed != 0
138 || report.panicked != 0
139 || report.timed_out != 0
140 || report.blocking_still_running != 0
141 {
142 return false;
143 }
144 pending.extend(&report.children);
145 }
146 true
147 }
148
149 pub fn assert_no_task_leak(&self) -> Result<(), String> {
151 if self.is_healthy() {
152 Ok(())
153 } else {
154 Err(format!("shutdown report is unhealthy: {}", self.to_json()))
155 }
156 }
157
158 pub fn log_if_unhealthy(&self) {
160 if !self.is_healthy() {
161 tracing::warn!(report = %self.to_json(), "runtime shutdown report is unhealthy");
162 }
163 }
164
165 pub fn to_json(&self) -> String {
167 serde_json::to_string_pretty(self).unwrap_or_else(|error| format!("{{\"serialization_error\":\"{}\"}}", error))
168 }
169
170 pub fn merge_blocking(&mut self, snapshot: crate::blocking::BlockingExecutorSnapshot) {
172 self.merge_blocking_tasks(snapshot.blocking_still_running, snapshot.tasks);
173 }
174
175 pub(crate) fn merge_blocking_tasks(&mut self, blocking_still_running: usize, tasks: Vec<BlockingTaskSnapshot>) {
180 const BLOCKING_STILL_RUNNING: &str =
181 "spawn_blocking tasks may continue after timeout; see blocking_still_running";
182
183 self.blocking_still_running += blocking_still_running;
184 self.blocking_tasks.extend(tasks);
185 if self.blocking_still_running > 0
186 && !self
187 .annotations
188 .iter()
189 .any(|annotation| annotation.message == BLOCKING_STILL_RUNNING)
190 {
191 self.annotations.push(ShutdownAnnotation::new(BLOCKING_STILL_RUNNING));
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize)]
198pub struct TaskSnapshot {
199 pub id: TaskId,
201 pub name: String,
203 pub group_id: TaskGroupId,
205 pub group_name: String,
207 pub kind: TaskKind,
209 pub state: TaskState,
211 #[serde(with = "duration_millis")]
213 pub elapsed: Duration,
214}
215
216#[derive(Debug, Clone, Serialize)]
218pub struct ShutdownAnnotation {
219 pub message: String,
221}
222
223impl ShutdownAnnotation {
224 pub fn new(message: impl Into<String>) -> Self {
226 Self {
227 message: message.into(),
228 }
229 }
230}
231
232mod duration_millis {
233 use std::time::Duration;
234
235 use serde::Serializer;
236
237 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
238 where
239 S: Serializer,
240 {
241 serializer.serialize_u64(duration.as_millis() as u64)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn explicit_component_failure_makes_the_report_unhealthy() {
251 let mut report = ShutdownReport::new("failed-component", Duration::ZERO);
252 report.failed = 1;
253
254 assert!(!report.is_healthy());
255 assert!(report.to_json().contains("\"failed\": 1"));
256 }
257
258 #[tokio::test]
259 async fn remaining_tasks_are_capped_and_the_omission_is_counted() {
260 let context = crate::RuntimeContext::from_current("remaining-task-limit");
261 let group_id = context.root_group().id();
262 let mut report = ShutdownReport::new("many-leaks", Duration::ZERO);
263 for id in 0..10_000 {
264 report.leaked += 1;
265 report.push_remaining_task(TaskSnapshot {
266 id: TaskId::new(group_id, id),
267 name: "leaked".to_owned(),
268 group_id,
269 group_name: "many-leaks".to_owned(),
270 kind: TaskKind::Worker,
271 state: TaskState::Leaked,
272 elapsed: Duration::ZERO,
273 });
274 }
275
276 assert_eq!(report.remaining_tasks.len(), ShutdownReport::REMAINING_TASKS_LIMIT);
277 assert_eq!(
278 report.remaining_tasks_omitted,
279 10_000 - ShutdownReport::REMAINING_TASKS_LIMIT
280 );
281 assert_eq!(report.leaked, 10_000);
282 let json = report.to_json();
283 assert!(json.contains("\"remaining_tasks_omitted\": 9936"), "{json}");
284 assert!(json.len() < 64 * 1024, "report JSON is {} bytes", json.len());
285 }
286}