use std::time::Duration;
use serde::Serialize;
use crate::blocking::BlockingTaskSnapshot;
use crate::task_group::TaskGroupId;
use crate::task_group::TaskId;
use crate::task_group::TaskKind;
use crate::task_group::TaskState;
#[derive(Debug, Serialize)]
pub struct ShutdownReport {
pub name: String,
#[serde(with = "duration_millis")]
pub elapsed: Duration,
pub completed: usize,
pub cancelled: usize,
pub aborted: usize,
pub failed: usize,
pub panicked: usize,
pub timed_out: usize,
pub leaked: usize,
pub blocking_still_running: usize,
pub children: Vec<ShutdownReport>,
pub remaining_tasks: Vec<TaskSnapshot>,
pub remaining_tasks_omitted: usize,
pub blocking_tasks: Vec<BlockingTaskSnapshot>,
pub annotations: Vec<ShutdownAnnotation>,
}
impl Clone for ShutdownReport {
fn clone(&self) -> Self {
let mut copy = self.clone_local();
let mut pending = vec![(self, &mut copy)];
while let Some((source, target)) = pending.pop() {
target.children = source.children.iter().map(Self::clone_local).collect();
pending.extend(source.children.iter().zip(target.children.iter_mut()));
}
copy
}
}
impl ShutdownReport {
fn clone_local(&self) -> Self {
Self {
name: self.name.clone(),
elapsed: self.elapsed,
completed: self.completed,
cancelled: self.cancelled,
aborted: self.aborted,
failed: self.failed,
panicked: self.panicked,
timed_out: self.timed_out,
leaked: self.leaked,
blocking_still_running: self.blocking_still_running,
children: Vec::new(),
remaining_tasks: self.remaining_tasks.clone(),
remaining_tasks_omitted: self.remaining_tasks_omitted,
blocking_tasks: self.blocking_tasks.clone(),
annotations: self.annotations.clone(),
}
}
pub fn new(name: impl Into<String>, elapsed: Duration) -> Self {
Self {
name: name.into(),
elapsed,
completed: 0,
cancelled: 0,
aborted: 0,
failed: 0,
panicked: 0,
timed_out: 0,
leaked: 0,
blocking_still_running: 0,
children: Vec::new(),
remaining_tasks: Vec::new(),
remaining_tasks_omitted: 0,
blocking_tasks: Vec::new(),
annotations: Vec::new(),
}
}
pub const REMAINING_TASKS_LIMIT: usize = 64;
pub(crate) fn push_remaining_task(&mut self, task: TaskSnapshot) {
if self.remaining_tasks.len() < Self::REMAINING_TASKS_LIMIT {
self.remaining_tasks.push(task);
} else {
self.remaining_tasks_omitted += 1;
}
}
pub fn is_healthy(&self) -> bool {
let mut pending = vec![self];
while let Some(report) = pending.pop() {
if report.leaked != 0
|| report.failed != 0
|| report.panicked != 0
|| report.timed_out != 0
|| report.blocking_still_running != 0
{
return false;
}
pending.extend(&report.children);
}
true
}
pub fn assert_no_task_leak(&self) -> Result<(), String> {
if self.is_healthy() {
Ok(())
} else {
Err(format!("shutdown report is unhealthy: {}", self.to_json()))
}
}
pub fn log_if_unhealthy(&self) {
if !self.is_healthy() {
tracing::warn!(report = %self.to_json(), "runtime shutdown report is unhealthy");
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|error| format!("{{\"serialization_error\":\"{}\"}}", error))
}
pub fn merge_blocking(&mut self, snapshot: crate::blocking::BlockingExecutorSnapshot) {
self.merge_blocking_tasks(snapshot.blocking_still_running, snapshot.tasks);
}
pub(crate) fn merge_blocking_tasks(&mut self, blocking_still_running: usize, tasks: Vec<BlockingTaskSnapshot>) {
const BLOCKING_STILL_RUNNING: &str =
"spawn_blocking tasks may continue after timeout; see blocking_still_running";
self.blocking_still_running += blocking_still_running;
self.blocking_tasks.extend(tasks);
if self.blocking_still_running > 0
&& !self
.annotations
.iter()
.any(|annotation| annotation.message == BLOCKING_STILL_RUNNING)
{
self.annotations.push(ShutdownAnnotation::new(BLOCKING_STILL_RUNNING));
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskSnapshot {
pub id: TaskId,
pub name: String,
pub group_id: TaskGroupId,
pub group_name: String,
pub kind: TaskKind,
pub state: TaskState,
#[serde(with = "duration_millis")]
pub elapsed: Duration,
}
#[derive(Debug, Clone, Serialize)]
pub struct ShutdownAnnotation {
pub message: String,
}
impl ShutdownAnnotation {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
mod duration_millis {
use std::time::Duration;
use serde::Serializer;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_millis() as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_component_failure_makes_the_report_unhealthy() {
let mut report = ShutdownReport::new("failed-component", Duration::ZERO);
report.failed = 1;
assert!(!report.is_healthy());
assert!(report.to_json().contains("\"failed\": 1"));
}
#[tokio::test]
async fn remaining_tasks_are_capped_and_the_omission_is_counted() {
let context = crate::RuntimeContext::from_current("remaining-task-limit");
let group_id = context.root_group().id();
let mut report = ShutdownReport::new("many-leaks", Duration::ZERO);
for id in 0..10_000 {
report.leaked += 1;
report.push_remaining_task(TaskSnapshot {
id: TaskId::new(group_id, id),
name: "leaked".to_owned(),
group_id,
group_name: "many-leaks".to_owned(),
kind: TaskKind::Worker,
state: TaskState::Leaked,
elapsed: Duration::ZERO,
});
}
assert_eq!(report.remaining_tasks.len(), ShutdownReport::REMAINING_TASKS_LIMIT);
assert_eq!(
report.remaining_tasks_omitted,
10_000 - ShutdownReport::REMAINING_TASKS_LIMIT
);
assert_eq!(report.leaked, 10_000);
let json = report.to_json();
assert!(json.contains("\"remaining_tasks_omitted\": 9936"), "{json}");
assert!(json.len() < 64 * 1024, "report JSON is {} bytes", json.len());
}
}