use super::{Budget, RegionId, TaskId, Time};
use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CancelAttributionConfig {
pub max_chain_depth: usize,
pub max_chain_memory: usize,
}
impl CancelAttributionConfig {
pub const DEFAULT_MAX_DEPTH: usize = 16;
pub const DEFAULT_MAX_MEMORY: usize = 4096;
#[inline]
#[must_use]
pub const fn new(max_chain_depth: usize, max_chain_memory: usize) -> Self {
Self {
max_chain_depth,
max_chain_memory,
}
}
#[inline]
#[must_use]
pub const fn unlimited() -> Self {
Self {
max_chain_depth: usize::MAX,
max_chain_memory: usize::MAX,
}
}
#[inline]
#[must_use]
pub const fn single_reason_cost() -> usize {
80
}
#[inline]
#[must_use]
pub const fn estimated_chain_cost(depth: usize) -> usize {
if depth == 0 {
return 0;
}
Self::single_reason_cost() * depth + 8 * depth.saturating_sub(1)
}
}
impl Default for CancelAttributionConfig {
fn default() -> Self {
Self {
max_chain_depth: Self::DEFAULT_MAX_DEPTH,
max_chain_memory: Self::DEFAULT_MAX_MEMORY,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum CancelKind {
User,
Timeout,
Deadline,
PollQuota,
CostBudget,
FailFast,
RaceLost,
ParentCancelled,
ResourceUnavailable,
Shutdown,
LinkedExit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum CancelPhase {
Requested,
Cancelling,
Finalizing,
Completed,
}
impl CancelPhase {
#[inline]
fn rank(self) -> u8 {
match self {
Self::Requested => 0,
Self::Cancelling => 1,
Self::Finalizing => 2,
Self::Completed => 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CancelWitness {
pub task_id: TaskId,
pub region_id: RegionId,
pub epoch: u64,
pub phase: CancelPhase,
pub reason: CancelReason,
}
impl CancelWitness {
#[inline]
#[must_use]
pub fn new(
task_id: TaskId,
region_id: RegionId,
epoch: u64,
phase: CancelPhase,
reason: CancelReason,
) -> Self {
Self {
task_id,
region_id,
epoch,
phase,
reason,
}
}
pub fn validate_transition(prev: Option<&Self>, next: &Self) -> Result<(), CancelWitnessError> {
let Some(prev) = prev else {
return Ok(());
};
if prev.task_id != next.task_id {
return Err(CancelWitnessError::TaskMismatch);
}
if prev.region_id != next.region_id {
return Err(CancelWitnessError::RegionMismatch);
}
if prev.epoch != next.epoch {
return Err(CancelWitnessError::EpochMismatch);
}
if next.phase.rank() < prev.phase.rank() {
return Err(CancelWitnessError::PhaseRegression {
from: prev.phase,
to: next.phase,
});
}
if next.reason.severity() < prev.reason.severity() {
return Err(CancelWitnessError::ReasonWeakened {
from: prev.reason.kind(),
to: next.reason.kind(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CancelWitnessError {
TaskMismatch,
RegionMismatch,
EpochMismatch,
PhaseRegression {
from: CancelPhase,
to: CancelPhase,
},
ReasonWeakened {
from: CancelKind,
to: CancelKind,
},
}
impl CancelKind {
#[inline]
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::User => "User",
Self::Timeout => "Timeout",
Self::Deadline => "Deadline",
Self::PollQuota => "PollQuota",
Self::CostBudget => "CostBudget",
Self::FailFast => "FailFast",
Self::RaceLost => "RaceLost",
Self::ParentCancelled => "ParentCancelled",
Self::ResourceUnavailable => "ResourceUnavailable",
Self::Shutdown => "Shutdown",
Self::LinkedExit => "LinkedExit",
}
}
#[inline]
#[must_use]
pub const fn severity(self) -> u8 {
match self {
Self::User => 0,
Self::Timeout | Self::Deadline => 1,
Self::PollQuota | Self::CostBudget => 2,
Self::FailFast | Self::RaceLost | Self::LinkedExit => 3,
Self::ParentCancelled | Self::ResourceUnavailable => 4,
Self::Shutdown => 5,
}
}
}
impl fmt::Display for CancelKind {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::User => write!(f, "user"),
Self::Timeout => write!(f, "timeout"),
Self::Deadline => write!(f, "deadline"),
Self::PollQuota => write!(f, "poll quota"),
Self::CostBudget => write!(f, "cost budget"),
Self::FailFast => write!(f, "fail-fast"),
Self::RaceLost => write!(f, "race lost"),
Self::ParentCancelled => write!(f, "parent cancelled"),
Self::ResourceUnavailable => write!(f, "resource unavailable"),
Self::Shutdown => write!(f, "shutdown"),
Self::LinkedExit => write!(f, "linked exit"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CancelReason {
pub kind: CancelKind,
pub origin_region: RegionId,
pub origin_task: Option<TaskId>,
pub timestamp: Time,
pub message: Option<String>,
pub cause: Option<Box<Self>>,
pub truncated: bool,
pub truncated_at_depth: Option<usize>,
}
impl CancelReason {
#[inline]
#[must_use]
pub const fn with_origin(kind: CancelKind, origin_region: RegionId, timestamp: Time) -> Self {
Self {
kind,
origin_region,
origin_task: None,
timestamp,
message: None,
cause: None,
truncated: false,
truncated_at_depth: None,
}
}
#[inline]
#[must_use]
pub const fn new(kind: CancelKind) -> Self {
Self {
kind,
origin_region: RegionId::testing_default(),
origin_task: None,
timestamp: Time::ZERO,
message: None,
cause: None,
truncated: false,
truncated_at_depth: None,
}
}
#[inline]
#[must_use]
pub fn user(message: &'static str) -> Self {
Self {
kind: CancelKind::User,
origin_region: RegionId::testing_default(),
origin_task: None,
timestamp: Time::ZERO,
message: Some(message.to_string()),
cause: None,
truncated: false,
truncated_at_depth: None,
}
}
#[inline]
#[must_use]
pub const fn timeout() -> Self {
Self::new(CancelKind::Timeout)
}
#[inline]
#[must_use]
pub const fn deadline() -> Self {
Self::new(CancelKind::Deadline)
}
#[inline]
#[must_use]
pub const fn poll_quota() -> Self {
Self::new(CancelKind::PollQuota)
}
#[inline]
#[must_use]
pub const fn cost_budget() -> Self {
Self::new(CancelKind::CostBudget)
}
#[inline]
#[must_use]
pub const fn sibling_failed() -> Self {
Self::new(CancelKind::FailFast)
}
#[inline]
#[must_use]
pub const fn fail_fast() -> Self {
Self::new(CancelKind::FailFast)
}
#[inline]
#[must_use]
pub const fn race_loser() -> Self {
Self::new(CancelKind::RaceLost)
}
#[inline]
#[must_use]
pub const fn race_lost() -> Self {
Self::new(CancelKind::RaceLost)
}
#[inline]
#[must_use]
pub const fn parent_cancelled() -> Self {
Self::new(CancelKind::ParentCancelled)
}
#[inline]
#[must_use]
pub const fn resource_unavailable() -> Self {
Self::new(CancelKind::ResourceUnavailable)
}
#[inline]
#[must_use]
pub const fn shutdown() -> Self {
Self::new(CancelKind::Shutdown)
}
#[inline]
#[must_use]
pub const fn linked_exit() -> Self {
Self::new(CancelKind::LinkedExit)
}
#[inline]
#[must_use]
pub const fn with_task(mut self, task: TaskId) -> Self {
self.origin_task = Some(task);
self
}
#[inline]
#[must_use]
pub fn with_message(mut self, message: &'static str) -> Self {
self.message = Some(message.to_string());
self
}
#[inline]
#[must_use]
pub fn with_cause(mut self, cause: Self) -> Self {
self.cause = Some(Box::new(cause));
self
}
#[must_use]
pub fn with_cause_limited(mut self, cause: Self, config: &CancelAttributionConfig) -> Self {
let current_depth = 1_usize;
let cause_depth = cause.chain_depth();
let total_depth = current_depth + cause_depth;
if total_depth > config.max_chain_depth {
let allowed_cause_depth = config.max_chain_depth.saturating_sub(current_depth);
if allowed_cause_depth == 0 {
self.truncated = true;
self.truncated_at_depth = Some(current_depth);
return self;
}
let truncated_cause = Self::truncate_chain(cause, allowed_cause_depth);
self.cause = Some(Box::new(truncated_cause));
self.truncated = true;
self.truncated_at_depth = Some(current_depth + allowed_cause_depth);
return self;
}
let estimated_memory = CancelAttributionConfig::estimated_chain_cost(total_depth);
if estimated_memory > config.max_chain_memory {
let mut allowed_depth = 0;
while CancelAttributionConfig::estimated_chain_cost(current_depth + allowed_depth + 1)
<= config.max_chain_memory
&& allowed_depth < cause_depth
{
allowed_depth += 1;
}
if allowed_depth == 0 {
self.truncated = true;
self.truncated_at_depth = Some(current_depth);
return self;
}
let truncated_cause = Self::truncate_chain(cause, allowed_depth);
self.cause = Some(Box::new(truncated_cause));
self.truncated = true;
self.truncated_at_depth = Some(current_depth + allowed_depth);
return self;
}
self.cause = Some(Box::new(cause));
self
}
fn truncate_chain(reason: Self, max_depth: usize) -> Self {
if max_depth == 0 {
return Self {
cause: None,
truncated: true,
truncated_at_depth: Some(0),
..reason
};
}
if max_depth == 1 || reason.cause.is_none() {
return Self {
cause: None,
truncated: reason.cause.is_some(), truncated_at_depth: if reason.cause.is_some() {
Some(1)
} else {
reason.truncated_at_depth
},
..reason
};
}
let truncated_cause = reason
.cause
.map(|boxed_cause| Box::new(Self::truncate_chain(*boxed_cause, max_depth - 1)));
Self {
cause: truncated_cause,
truncated: reason.truncated,
truncated_at_depth: reason.truncated_at_depth,
..reason
}
}
#[inline]
#[must_use]
pub const fn with_timestamp(mut self, timestamp: Time) -> Self {
self.timestamp = timestamp;
self
}
#[inline]
#[must_use]
pub const fn with_region(mut self, region: RegionId) -> Self {
self.origin_region = region;
self
}
#[inline]
#[must_use]
pub fn chain(&self) -> CancelReasonChain<'_> {
CancelReasonChain {
current: Some(self),
}
}
#[must_use]
pub fn root_cause(&self) -> &Self {
let mut current = self;
while let Some(ref cause) = current.cause {
current = cause;
}
current
}
#[inline]
#[must_use]
pub fn chain_depth(&self) -> usize {
self.chain().count()
}
#[must_use]
pub fn any_cause_is(&self, kind: CancelKind) -> bool {
self.chain().any(|r| r.kind == kind)
}
#[must_use]
pub fn caused_by(&self, cause: &Self) -> bool {
self.chain().skip(1).any(|r| r == cause)
}
#[inline]
#[must_use]
pub const fn severity(&self) -> u8 {
self.kind.severity()
}
#[inline]
#[must_use]
pub const fn is_kind(&self, kind: CancelKind) -> bool {
matches!(
(self.kind, kind),
(CancelKind::User, CancelKind::User)
| (CancelKind::Timeout, CancelKind::Timeout)
| (CancelKind::Deadline, CancelKind::Deadline)
| (CancelKind::PollQuota, CancelKind::PollQuota)
| (CancelKind::CostBudget, CancelKind::CostBudget)
| (CancelKind::FailFast, CancelKind::FailFast)
| (CancelKind::RaceLost, CancelKind::RaceLost)
| (CancelKind::ParentCancelled, CancelKind::ParentCancelled)
| (
CancelKind::ResourceUnavailable,
CancelKind::ResourceUnavailable
)
| (CancelKind::LinkedExit, CancelKind::LinkedExit)
| (CancelKind::Shutdown, CancelKind::Shutdown)
)
}
#[inline]
#[must_use]
pub const fn is_shutdown(&self) -> bool {
matches!(self.kind, CancelKind::Shutdown)
}
#[inline]
#[must_use]
pub const fn is_budget_exceeded(&self) -> bool {
matches!(
self.kind,
CancelKind::Deadline | CancelKind::PollQuota | CancelKind::CostBudget
)
}
#[inline]
#[must_use]
pub const fn is_time_exceeded(&self) -> bool {
matches!(self.kind, CancelKind::Timeout | CancelKind::Deadline)
}
pub fn strengthen(&mut self, other: &Self) -> bool {
if other.kind.severity() > self.kind.severity() {
self.kind = other.kind;
self.origin_region = other.origin_region;
self.origin_task = other.origin_task;
self.timestamp = other.timestamp;
self.message.clone_from(&other.message);
self.cause.clone_from(&other.cause);
self.truncated = other.truncated;
self.truncated_at_depth = other.truncated_at_depth;
return true;
}
if other.kind.severity() < self.kind.severity() {
return false;
}
if other.timestamp < self.timestamp {
self.kind = other.kind;
self.origin_region = other.origin_region;
self.origin_task = other.origin_task;
self.timestamp = other.timestamp;
self.message.clone_from(&other.message);
self.cause.clone_from(&other.cause);
self.truncated = other.truncated;
self.truncated_at_depth = other.truncated_at_depth;
return true;
}
if other.timestamp > self.timestamp {
return false;
}
let should_replace = match (&self.message, &other.message) {
(None, Some(_)) => true,
(Some(current), Some(candidate)) if candidate < current => true,
_ => false,
};
if should_replace {
self.kind = other.kind;
self.origin_region = other.origin_region;
self.origin_task = other.origin_task;
self.timestamp = other.timestamp;
self.message.clone_from(&other.message);
self.cause.clone_from(&other.cause);
self.truncated = other.truncated;
self.truncated_at_depth = other.truncated_at_depth;
}
should_replace
}
#[must_use]
pub fn cleanup_budget(&self) -> Budget {
match self.kind {
CancelKind::User => Budget::new().with_poll_quota(1000).with_priority(200),
CancelKind::Timeout | CancelKind::Deadline => {
Budget::new().with_poll_quota(500).with_priority(210)
}
CancelKind::PollQuota | CancelKind::CostBudget => {
Budget::new().with_poll_quota(300).with_priority(215)
}
CancelKind::FailFast
| CancelKind::RaceLost
| CancelKind::ParentCancelled
| CancelKind::ResourceUnavailable
| CancelKind::LinkedExit => Budget::new().with_poll_quota(200).with_priority(220),
CancelKind::Shutdown => Budget::new().with_poll_quota(50).with_priority(255),
}
}
#[inline]
#[must_use]
pub const fn kind(&self) -> CancelKind {
self.kind
}
#[inline]
#[must_use]
pub const fn origin_region(&self) -> RegionId {
self.origin_region
}
#[inline]
#[must_use]
pub const fn origin_task(&self) -> Option<TaskId> {
self.origin_task
}
#[inline]
#[must_use]
pub const fn timestamp(&self) -> Time {
self.timestamp
}
#[inline]
#[must_use]
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
#[inline]
#[must_use]
pub fn cause(&self) -> Option<&Self> {
self.cause.as_deref()
}
#[inline]
#[must_use]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
#[inline]
#[must_use]
pub const fn truncated_at_depth(&self) -> Option<usize> {
self.truncated_at_depth
}
#[must_use]
pub fn any_truncated(&self) -> bool {
self.chain().any(|r| r.truncated)
}
#[must_use]
pub fn estimated_memory_cost(&self) -> usize {
CancelAttributionConfig::estimated_chain_cost(self.chain_depth())
}
}
pub struct CancelReasonChain<'a> {
current: Option<&'a CancelReason>,
}
impl<'a> Iterator for CancelReasonChain<'a> {
type Item = &'a CancelReason;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current?;
self.current = current.cause.as_deref();
Some(current)
}
}
impl Default for CancelReason {
fn default() -> Self {
Self::new(CancelKind::User)
}
}
impl fmt::Display for CancelReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(msg) = &self.message {
write!(f, ": {msg}")?;
}
if f.alternate() {
write!(f, " (from {} at {})", self.origin_region, self.timestamp)?;
if let Some(ref task) = self.origin_task {
write!(f, " task {task}")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::init_test_logging;
use serde_json::{Value, json};
fn init_test(test_name: &str) {
init_test_logging();
crate::test_phase!(test_name);
}
fn combine(mut a: CancelReason, b: &CancelReason) -> CancelReason {
a.strengthen(b);
a
}
fn scrub_cancel_snapshot(mut value: Value) -> Value {
fn scrub_in_place(value: &mut Value) {
match value {
Value::Object(map) => {
for (key, entry) in map.iter_mut() {
match key.as_str() {
"task_id" | "origin_task" if !entry.is_null() => {
*entry = Value::String("[TASK_ID]".into());
}
"region_id" | "origin_region" => {
*entry = Value::String("[REGION_ID]".into());
}
"timestamp" => {
*entry = Value::String("[TIME]".into());
}
_ => scrub_in_place(entry),
}
}
}
Value::Array(items) => {
for item in items {
scrub_in_place(item);
}
}
_ => {}
}
}
scrub_in_place(&mut value);
value
}
#[test]
fn severity_ordering() {
init_test("severity_ordering");
crate::assert_with_log!(
CancelKind::User.severity() < CancelKind::Timeout.severity(),
"User should be below Timeout",
true,
CancelKind::User.severity() < CancelKind::Timeout.severity()
);
crate::assert_with_log!(
CancelKind::Timeout.severity() == CancelKind::Deadline.severity(),
"Timeout and Deadline should have same severity",
true,
CancelKind::Timeout.severity() == CancelKind::Deadline.severity()
);
crate::assert_with_log!(
CancelKind::Deadline.severity() < CancelKind::PollQuota.severity(),
"Deadline should be below PollQuota",
true,
CancelKind::Deadline.severity() < CancelKind::PollQuota.severity()
);
crate::assert_with_log!(
CancelKind::PollQuota.severity() == CancelKind::CostBudget.severity(),
"PollQuota and CostBudget should have same severity",
true,
CancelKind::PollQuota.severity() == CancelKind::CostBudget.severity()
);
crate::assert_with_log!(
CancelKind::CostBudget.severity() < CancelKind::FailFast.severity(),
"CostBudget should be below FailFast",
true,
CancelKind::CostBudget.severity() < CancelKind::FailFast.severity()
);
crate::assert_with_log!(
CancelKind::FailFast.severity() == CancelKind::RaceLost.severity(),
"FailFast and RaceLost should have same severity",
true,
CancelKind::FailFast.severity() == CancelKind::RaceLost.severity()
);
crate::assert_with_log!(
CancelKind::RaceLost.severity() < CancelKind::ParentCancelled.severity(),
"RaceLost should be below ParentCancelled",
true,
CancelKind::RaceLost.severity() < CancelKind::ParentCancelled.severity()
);
crate::assert_with_log!(
CancelKind::ParentCancelled.severity() == CancelKind::ResourceUnavailable.severity(),
"ParentCancelled and ResourceUnavailable should have same severity",
true,
CancelKind::ParentCancelled.severity() == CancelKind::ResourceUnavailable.severity()
);
crate::assert_with_log!(
CancelKind::ParentCancelled.severity() < CancelKind::Shutdown.severity(),
"ParentCancelled should be below Shutdown",
true,
CancelKind::ParentCancelled.severity() < CancelKind::Shutdown.severity()
);
crate::test_complete!("severity_ordering");
}
#[test]
fn strengthen_takes_more_severe() {
init_test("strengthen_takes_more_severe");
let mut reason = CancelReason::new(CancelKind::User);
let strengthened = reason.strengthen(&CancelReason::timeout());
crate::assert_with_log!(
strengthened,
"should strengthen to Timeout",
true,
strengthened
);
crate::assert_with_log!(
reason.kind == CancelKind::Timeout,
"kind should be Timeout",
CancelKind::Timeout,
reason.kind
);
let strengthened_shutdown = reason.strengthen(&CancelReason::shutdown());
crate::assert_with_log!(
strengthened_shutdown,
"should strengthen to Shutdown",
true,
strengthened_shutdown
);
crate::assert_with_log!(
reason.kind == CancelKind::Shutdown,
"kind should be Shutdown",
CancelKind::Shutdown,
reason.kind
);
let unchanged = !reason.strengthen(&CancelReason::timeout());
crate::assert_with_log!(unchanged, "less severe should not change", true, unchanged);
crate::assert_with_log!(
reason.kind == CancelKind::Shutdown,
"kind should remain Shutdown",
CancelKind::Shutdown,
reason.kind
);
crate::test_complete!("strengthen_takes_more_severe");
}
#[test]
fn strengthen_adopts_truncation_metadata_from_winner() {
init_test("strengthen_adopts_truncation_metadata_from_winner");
let config = CancelAttributionConfig::new(2, usize::MAX);
let deep_cause = CancelReason::timeout().with_cause(CancelReason::user("root"));
let mut truncated_reason =
CancelReason::user("weak").with_cause_limited(deep_cause, &config);
crate::assert_with_log!(
truncated_reason.truncated || truncated_reason.any_truncated(),
"pre-strengthen reason should be truncated",
true,
truncated_reason.truncated || truncated_reason.any_truncated()
);
let non_truncated = CancelReason::shutdown();
crate::assert_with_log!(
!non_truncated.truncated,
"stronger reason should not be truncated",
false,
non_truncated.truncated
);
let changed = truncated_reason.strengthen(&non_truncated);
crate::assert_with_log!(changed, "should strengthen to Shutdown", true, changed);
crate::assert_with_log!(
!truncated_reason.truncated,
"truncated flag should adopt winner's value (false)",
false,
truncated_reason.truncated
);
crate::assert_with_log!(
truncated_reason.truncated_at_depth.is_none(),
"truncated_at_depth should adopt winner's value (None)",
true,
truncated_reason.truncated_at_depth.is_none()
);
crate::test_complete!("strengthen_adopts_truncation_metadata_from_winner");
}
#[test]
fn strengthen_is_idempotent() {
init_test("strengthen_is_idempotent");
let mut reason = CancelReason::timeout();
let unchanged = !reason.strengthen(&CancelReason::timeout());
crate::assert_with_log!(
unchanged,
"strengthen should be idempotent",
true,
unchanged
);
crate::assert_with_log!(
reason.kind == CancelKind::Timeout,
"kind should remain Timeout",
CancelKind::Timeout,
reason.kind
);
crate::test_complete!("strengthen_is_idempotent");
}
#[test]
fn strengthen_is_associative() {
init_test("strengthen_is_associative");
let a = CancelReason::user("a");
let b = CancelReason::timeout();
let c = CancelReason::shutdown();
let left = combine(combine(a.clone(), &b), &c);
let right = {
let bc = combine(b, &c);
combine(a, &bc)
};
crate::assert_with_log!(
left == right,
"strengthen should be associative",
left,
right
);
crate::test_complete!("strengthen_is_associative");
}
#[test]
fn strengthen_same_kind_picks_deterministic_message() {
init_test("strengthen_same_kind_picks_deterministic_message");
let mut reason = CancelReason::user("b");
let changed = reason.strengthen(&CancelReason::user("a"));
crate::assert_with_log!(
changed,
"same-kind strengthen should change message",
true,
changed
);
crate::assert_with_log!(
reason.kind == CancelKind::User,
"kind should remain User",
CancelKind::User,
reason.kind
);
crate::assert_with_log!(
reason.message == Some("a".to_string()),
"message should be deterministic",
Some("a"),
reason.message
);
crate::test_complete!("strengthen_same_kind_picks_deterministic_message");
}
#[test]
fn strengthen_resets_message_when_kind_increases() {
init_test("strengthen_resets_message_when_kind_increases");
let mut reason = CancelReason::user("please stop");
let changed = reason.strengthen(&CancelReason::shutdown());
crate::assert_with_log!(changed, "kind increase should change reason", true, changed);
crate::assert_with_log!(
reason.kind == CancelKind::Shutdown,
"kind should be Shutdown",
CancelKind::Shutdown,
reason.kind
);
crate::assert_with_log!(
reason.message.is_none(),
"message should reset on kind increase",
true,
reason.message.is_none()
);
crate::test_complete!("strengthen_resets_message_when_kind_increases");
}
#[test]
fn cleanup_budget_scales_with_severity() {
init_test("cleanup_budget_scales_with_severity");
let user_budget = CancelReason::user("stop").cleanup_budget();
crate::assert_with_log!(
user_budget.poll_quota == 1000,
"user budget poll_quota should be 1000",
1000,
user_budget.poll_quota
);
let timeout_budget = CancelReason::timeout().cleanup_budget();
crate::assert_with_log!(
timeout_budget.poll_quota == 500,
"timeout budget poll_quota should be 500",
500,
timeout_budget.poll_quota
);
let poll_quota_budget = CancelReason::poll_quota().cleanup_budget();
crate::assert_with_log!(
poll_quota_budget.poll_quota == 300,
"poll_quota budget poll_quota should be 300",
300,
poll_quota_budget.poll_quota
);
let fail_fast_budget = CancelReason::sibling_failed().cleanup_budget();
crate::assert_with_log!(
fail_fast_budget.poll_quota == 200,
"fail_fast budget poll_quota should be 200",
200,
fail_fast_budget.poll_quota
);
let shutdown_budget = CancelReason::shutdown().cleanup_budget();
crate::assert_with_log!(
shutdown_budget.poll_quota == 50,
"shutdown budget poll_quota should be 50",
50,
shutdown_budget.poll_quota
);
crate::assert_with_log!(
shutdown_budget.priority == 255,
"shutdown budget priority should be 255",
255,
shutdown_budget.priority
);
crate::assert_with_log!(
user_budget.priority < timeout_budget.priority,
"user priority should be below timeout",
true,
user_budget.priority < timeout_budget.priority
);
crate::assert_with_log!(
timeout_budget.priority < poll_quota_budget.priority,
"timeout priority should be below poll_quota",
true,
timeout_budget.priority < poll_quota_budget.priority
);
crate::assert_with_log!(
poll_quota_budget.priority < fail_fast_budget.priority,
"poll_quota priority should be below fail_fast",
true,
poll_quota_budget.priority < fail_fast_budget.priority
);
crate::assert_with_log!(
fail_fast_budget.priority < shutdown_budget.priority,
"fail_fast priority should be below shutdown",
true,
fail_fast_budget.priority < shutdown_budget.priority
);
crate::test_complete!("cleanup_budget_scales_with_severity");
}
#[test]
fn cleanup_budget_always_finite_and_positive() {
init_test("cleanup_budget_always_finite_and_positive");
let kinds = [
CancelKind::User,
CancelKind::Timeout,
CancelKind::Deadline,
CancelKind::PollQuota,
CancelKind::CostBudget,
CancelKind::FailFast,
CancelKind::RaceLost,
CancelKind::ParentCancelled,
CancelKind::ResourceUnavailable,
CancelKind::Shutdown,
];
for kind in kinds {
let reason =
CancelReason::with_origin(kind, RegionId::new_for_test(1, 0), Time::from_secs(0));
let budget = reason.cleanup_budget();
crate::assert_with_log!(
budget.poll_quota > 0 && budget.poll_quota < u32::MAX,
"cleanup budget must be finite and positive",
true,
budget.poll_quota
);
}
crate::test_complete!("cleanup_budget_always_finite_and_positive");
}
#[test]
fn cleanup_budget_terminates_after_quota_polls() {
init_test("cleanup_budget_terminates_after_quota_polls");
let reason = CancelReason::timeout();
let mut budget = reason.cleanup_budget();
let quota = budget.poll_quota;
for i in 0..quota {
let result = budget.consume_poll();
crate::assert_with_log!(
result.is_some(),
"poll should succeed within budget",
true,
i
);
}
crate::assert_with_log!(
budget.is_exhausted(),
"budget exhausted after quota polls",
true,
budget.poll_quota
);
let result = budget.consume_poll();
crate::assert_with_log!(
result.is_none(),
"poll fails after exhaustion",
true,
result.is_none()
);
crate::test_complete!("cleanup_budget_terminates_after_quota_polls");
}
#[test]
fn cleanup_budget_combine_never_widens() {
init_test("cleanup_budget_combine_never_widens");
let user_budget = CancelReason::user("stop").cleanup_budget();
let timeout_budget = CancelReason::timeout().cleanup_budget();
let shutdown_budget = CancelReason::shutdown().cleanup_budget();
let combined = user_budget.combine(timeout_budget);
crate::assert_with_log!(
combined.poll_quota <= user_budget.poll_quota,
"combined ≤ user",
user_budget.poll_quota,
combined.poll_quota
);
crate::assert_with_log!(
combined.poll_quota <= timeout_budget.poll_quota,
"combined ≤ timeout",
timeout_budget.poll_quota,
combined.poll_quota
);
crate::assert_with_log!(
combined.priority >= user_budget.priority,
"combined priority >= user",
user_budget.priority,
combined.priority
);
let with_shutdown = combined.combine(shutdown_budget);
crate::assert_with_log!(
with_shutdown.poll_quota <= combined.poll_quota,
"shutdown tightens further",
combined.poll_quota,
with_shutdown.poll_quota
);
crate::assert_with_log!(
with_shutdown.priority >= combined.priority,
"shutdown priority max",
combined.priority,
with_shutdown.priority
);
crate::test_complete!("cleanup_budget_combine_never_widens");
}
#[test]
fn cleanup_budget_severity_monotone() {
init_test("cleanup_budget_severity_monotone");
let user = CancelReason::user("stop");
let timeout = CancelReason::timeout();
let quota = CancelReason::poll_quota();
let fail_fast = CancelReason::sibling_failed();
let shutdown = CancelReason::shutdown();
let budgets = [
user.cleanup_budget(),
timeout.cleanup_budget(),
quota.cleanup_budget(),
fail_fast.cleanup_budget(),
shutdown.cleanup_budget(),
];
for i in 1..budgets.len() {
crate::assert_with_log!(
budgets[i].poll_quota <= budgets[i - 1].poll_quota,
"poll quota non-increasing with severity",
budgets[i - 1].poll_quota,
budgets[i].poll_quota
);
}
for i in 1..budgets.len() {
crate::assert_with_log!(
budgets[i].priority >= budgets[i - 1].priority,
"priority non-decreasing with severity",
budgets[i - 1].priority,
budgets[i].priority
);
}
crate::test_complete!("cleanup_budget_severity_monotone");
}
#[test]
fn cleanup_budget_has_no_deadline() {
init_test("cleanup_budget_has_no_deadline");
let kinds = [CancelKind::User, CancelKind::Timeout, CancelKind::Shutdown];
for kind in kinds {
let reason =
CancelReason::with_origin(kind, RegionId::new_for_test(1, 0), Time::from_secs(0));
let budget = reason.cleanup_budget();
crate::assert_with_log!(
budget.deadline.is_none(),
"cleanup budget should have no deadline",
true,
budget.deadline.is_none()
);
}
crate::test_complete!("cleanup_budget_has_no_deadline");
}
#[test]
fn cancel_reason_with_full_attribution() {
init_test("cancel_reason_with_full_attribution");
let region = RegionId::new_for_test(1, 0);
let task = TaskId::new_for_test(2, 0);
let timestamp = Time::from_millis(1000);
let reason = CancelReason::with_origin(CancelKind::Timeout, region, timestamp)
.with_task(task)
.with_message("test timeout");
crate::assert_with_log!(
reason.kind == CancelKind::Timeout,
"kind should be Timeout",
CancelKind::Timeout,
reason.kind
);
crate::assert_with_log!(
reason.origin_region == region,
"origin_region should match",
region,
reason.origin_region
);
crate::assert_with_log!(
reason.origin_task == Some(task),
"origin_task should match",
Some(task),
reason.origin_task
);
crate::assert_with_log!(
reason.timestamp == timestamp,
"timestamp should match",
timestamp,
reason.timestamp
);
crate::assert_with_log!(
reason.message == Some("test timeout".to_string()),
"message should match",
Some("test timeout"),
reason.message
);
crate::test_complete!("cancel_reason_with_full_attribution");
}
#[test]
fn cancel_witness_json_snapshot_scrubbed() {
init_test("cancel_witness_json_snapshot_scrubbed");
let task = TaskId::new_for_test(8, 2);
let region = RegionId::new_for_test(7, 1);
let cause = CancelReason::with_origin(
CancelKind::Timeout,
RegionId::new_for_test(3, 0),
Time::from_millis(220),
)
.with_task(TaskId::new_for_test(4, 0))
.with_message("deadline budget expired");
let reason = CancelReason::with_origin(
CancelKind::ParentCancelled,
RegionId::new_for_test(9, 1),
Time::from_millis(550),
)
.with_task(TaskId::new_for_test(10, 0))
.with_message("closing subtree")
.with_cause(cause);
let witness = CancelWitness::new(task, region, 3, CancelPhase::Finalizing, reason);
insta::assert_json_snapshot!(
"cancel_witness_json_scrubbed",
scrub_cancel_snapshot(json!({
"phase_label": format!("{:?}", witness.phase),
"witness": witness,
}))
);
}
#[test]
fn cause_chain_single() {
init_test("cause_chain_single");
let reason = CancelReason::timeout();
crate::assert_with_log!(
reason.chain_depth() == 1,
"single reason should have depth 1",
1,
reason.chain_depth()
);
let root = reason.root_cause();
crate::assert_with_log!(
root == &reason,
"root_cause of single reason should be itself",
true,
root == &reason
);
crate::test_complete!("cause_chain_single");
}
#[test]
fn cause_chain_multiple() {
init_test("cause_chain_multiple");
let root = CancelReason::timeout().with_message("original timeout");
let middle = CancelReason::parent_cancelled()
.with_message("parent cancelled")
.with_cause(root);
let leaf = CancelReason::shutdown()
.with_message("shutdown")
.with_cause(middle);
crate::assert_with_log!(
leaf.chain_depth() == 3,
"three-level chain should have depth 3",
3,
leaf.chain_depth()
);
let found_root = leaf.root_cause();
crate::assert_with_log!(
found_root.kind == CancelKind::Timeout,
"root_cause should be Timeout",
CancelKind::Timeout,
found_root.kind
);
crate::assert_with_log!(
found_root.message == Some("original timeout".to_string()),
"root_cause message should match",
Some("original timeout"),
found_root.message
);
crate::test_complete!("cause_chain_multiple");
}
#[test]
fn any_cause_is_works() {
init_test("any_cause_is_works");
let root = CancelReason::timeout();
let leaf = CancelReason::shutdown().with_cause(root);
crate::assert_with_log!(
leaf.any_cause_is(CancelKind::Shutdown),
"should find Shutdown in chain",
true,
leaf.any_cause_is(CancelKind::Shutdown)
);
crate::assert_with_log!(
leaf.any_cause_is(CancelKind::Timeout),
"should find Timeout in chain",
true,
leaf.any_cause_is(CancelKind::Timeout)
);
crate::assert_with_log!(
!leaf.any_cause_is(CancelKind::User),
"should not find User in chain",
false,
leaf.any_cause_is(CancelKind::User)
);
crate::test_complete!("any_cause_is_works");
}
#[test]
fn caused_by_works() {
init_test("caused_by_works");
let root = CancelReason::timeout().with_message("root");
let leaf = CancelReason::shutdown().with_cause(root.clone());
crate::assert_with_log!(
leaf.caused_by(&root),
"leaf should be caused_by root",
true,
leaf.caused_by(&root)
);
crate::assert_with_log!(
!root.caused_by(&leaf),
"root should not be caused_by leaf",
false,
root.caused_by(&leaf)
);
crate::assert_with_log!(
!leaf.caused_by(&leaf),
"leaf should not be caused_by itself",
false,
leaf.caused_by(&leaf)
);
crate::test_complete!("caused_by_works");
}
#[test]
fn is_kind_works() {
init_test("is_kind_works");
let reason = CancelReason::poll_quota();
crate::assert_with_log!(
reason.is_kind(CancelKind::PollQuota),
"is_kind should return true for matching kind",
true,
reason.is_kind(CancelKind::PollQuota)
);
crate::assert_with_log!(
!reason.is_kind(CancelKind::Timeout),
"is_kind should return false for non-matching kind",
false,
reason.is_kind(CancelKind::Timeout)
);
crate::test_complete!("is_kind_works");
}
#[test]
fn is_budget_exceeded_works() {
init_test("is_budget_exceeded_works");
crate::assert_with_log!(
CancelReason::deadline().is_budget_exceeded(),
"Deadline should be budget_exceeded",
true,
CancelReason::deadline().is_budget_exceeded()
);
crate::assert_with_log!(
CancelReason::poll_quota().is_budget_exceeded(),
"PollQuota should be budget_exceeded",
true,
CancelReason::poll_quota().is_budget_exceeded()
);
crate::assert_with_log!(
CancelReason::cost_budget().is_budget_exceeded(),
"CostBudget should be budget_exceeded",
true,
CancelReason::cost_budget().is_budget_exceeded()
);
crate::assert_with_log!(
!CancelReason::timeout().is_budget_exceeded(),
"Timeout should not be budget_exceeded",
false,
CancelReason::timeout().is_budget_exceeded()
);
crate::test_complete!("is_budget_exceeded_works");
}
#[test]
fn is_time_exceeded_works() {
init_test("is_time_exceeded_works");
crate::assert_with_log!(
CancelReason::timeout().is_time_exceeded(),
"Timeout should be time_exceeded",
true,
CancelReason::timeout().is_time_exceeded()
);
crate::assert_with_log!(
CancelReason::deadline().is_time_exceeded(),
"Deadline should be time_exceeded",
true,
CancelReason::deadline().is_time_exceeded()
);
crate::assert_with_log!(
!CancelReason::poll_quota().is_time_exceeded(),
"PollQuota should not be time_exceeded",
false,
CancelReason::poll_quota().is_time_exceeded()
);
crate::test_complete!("is_time_exceeded_works");
}
#[test]
fn new_variants_constructors() {
init_test("new_variants_constructors");
let deadline = CancelReason::deadline();
crate::assert_with_log!(
deadline.kind == CancelKind::Deadline,
"deadline() should create Deadline kind",
CancelKind::Deadline,
deadline.kind
);
let poll_quota = CancelReason::poll_quota();
crate::assert_with_log!(
poll_quota.kind == CancelKind::PollQuota,
"poll_quota() should create PollQuota kind",
CancelKind::PollQuota,
poll_quota.kind
);
let cost_budget = CancelReason::cost_budget();
crate::assert_with_log!(
cost_budget.kind == CancelKind::CostBudget,
"cost_budget() should create CostBudget kind",
CancelKind::CostBudget,
cost_budget.kind
);
let resource = CancelReason::resource_unavailable();
crate::assert_with_log!(
resource.kind == CancelKind::ResourceUnavailable,
"resource_unavailable() should create ResourceUnavailable kind",
CancelKind::ResourceUnavailable,
resource.kind
);
crate::test_complete!("new_variants_constructors");
}
#[test]
fn new_variants_display() {
init_test("new_variants_display");
crate::assert_with_log!(
format!("{}", CancelKind::Deadline) == "deadline",
"Deadline display should be 'deadline'",
"deadline",
format!("{}", CancelKind::Deadline)
);
crate::assert_with_log!(
format!("{}", CancelKind::PollQuota) == "poll quota",
"PollQuota display should be 'poll quota'",
"poll quota",
format!("{}", CancelKind::PollQuota)
);
crate::assert_with_log!(
format!("{}", CancelKind::CostBudget) == "cost budget",
"CostBudget display should be 'cost budget'",
"cost budget",
format!("{}", CancelKind::CostBudget)
);
crate::assert_with_log!(
format!("{}", CancelKind::ResourceUnavailable) == "resource unavailable",
"ResourceUnavailable display should be 'resource unavailable'",
"resource unavailable",
format!("{}", CancelKind::ResourceUnavailable)
);
crate::test_complete!("new_variants_display");
}
#[test]
fn cancel_attribution_config_defaults() {
init_test("cancel_attribution_config_defaults");
let config = CancelAttributionConfig::default();
crate::assert_with_log!(
config.max_chain_depth == 16,
"default max_chain_depth should be 16",
16,
config.max_chain_depth
);
crate::assert_with_log!(
config.max_chain_memory == 4096,
"default max_chain_memory should be 4096",
4096,
config.max_chain_memory
);
crate::test_complete!("cancel_attribution_config_defaults");
}
#[test]
fn cancel_attribution_config_custom() {
init_test("cancel_attribution_config_custom");
let config = CancelAttributionConfig::new(8, 2048);
crate::assert_with_log!(
config.max_chain_depth == 8,
"custom max_chain_depth should be 8",
8,
config.max_chain_depth
);
crate::assert_with_log!(
config.max_chain_memory == 2048,
"custom max_chain_memory should be 2048",
2048,
config.max_chain_memory
);
crate::test_complete!("cancel_attribution_config_custom");
}
#[test]
fn cancel_attribution_config_unlimited() {
init_test("cancel_attribution_config_unlimited");
let config = CancelAttributionConfig::unlimited();
crate::assert_with_log!(
config.max_chain_depth == usize::MAX,
"unlimited max_chain_depth should be usize::MAX",
usize::MAX,
config.max_chain_depth
);
crate::test_complete!("cancel_attribution_config_unlimited");
}
#[test]
fn chain_at_exact_limit() {
init_test("chain_at_exact_limit");
let config = CancelAttributionConfig::new(3, usize::MAX);
let level1 = CancelReason::timeout();
let level2 = CancelReason::parent_cancelled().with_cause(level1);
let level3 = CancelReason::shutdown().with_cause_limited(level2, &config);
crate::assert_with_log!(
level3.chain_depth() == 3,
"chain at limit should have depth 3",
3,
level3.chain_depth()
);
crate::assert_with_log!(
!level3.truncated,
"chain at limit should not be truncated",
false,
level3.truncated
);
crate::test_complete!("chain_at_exact_limit");
}
#[test]
fn chain_beyond_limit_truncates() {
init_test("chain_beyond_limit_truncates");
let config = CancelAttributionConfig::new(2, usize::MAX);
let level1 = CancelReason::timeout();
let level2 = CancelReason::parent_cancelled().with_cause(level1);
let level3 = CancelReason::shutdown().with_cause_limited(level2, &config);
crate::assert_with_log!(
level3.chain_depth() <= 2,
"chain beyond limit should be truncated to 2",
2,
level3.chain_depth()
);
crate::assert_with_log!(
level3.truncated || level3.any_truncated(),
"truncated chain should have truncated flag",
true,
level3.truncated || level3.any_truncated()
);
crate::test_complete!("chain_beyond_limit_truncates");
}
#[test]
fn truncated_reason_new_fields() {
init_test("truncated_reason_new_fields");
let reason = CancelReason::timeout();
crate::assert_with_log!(
!reason.truncated,
"new reason should not be truncated",
false,
reason.truncated
);
crate::assert_with_log!(
reason.truncated_at_depth.is_none(),
"new reason should have no truncated_at_depth",
true,
reason.truncated_at_depth.is_none()
);
crate::assert_with_log!(
!reason.is_truncated(),
"is_truncated() should be false",
false,
reason.is_truncated()
);
crate::test_complete!("truncated_reason_new_fields");
}
#[test]
fn estimated_memory_cost() {
init_test("estimated_memory_cost");
let single = CancelReason::timeout();
let cost1 = single.estimated_memory_cost();
crate::assert_with_log!(
cost1 > 0,
"single reason should have positive memory cost",
true,
cost1 > 0
);
let chain2 = CancelReason::shutdown().with_cause(CancelReason::timeout());
let cost2 = chain2.estimated_memory_cost();
crate::assert_with_log!(
cost2 > cost1,
"chain of 2 should cost more than single",
true,
cost2 > cost1
);
crate::test_complete!("estimated_memory_cost");
}
#[test]
fn memory_limit_triggers_truncation() {
init_test("memory_limit_triggers_truncation");
let config = CancelAttributionConfig::new(usize::MAX, 100);
let level1 = CancelReason::timeout();
let level2 = CancelReason::parent_cancelled().with_cause(level1);
let level3 = CancelReason::shutdown().with_cause_limited(level2, &config);
let truncated = level3.truncated || level3.any_truncated();
crate::assert_with_log!(
truncated,
"tight memory limit should trigger truncation",
true,
truncated
);
crate::test_complete!("memory_limit_triggers_truncation");
}
#[test]
fn any_truncated_finds_nested_truncation() {
init_test("any_truncated_finds_nested_truncation");
let inner = CancelReason {
truncated: true,
truncated_at_depth: Some(1),
..CancelReason::timeout()
};
let outer = CancelReason::shutdown().with_cause(inner);
crate::assert_with_log!(
!outer.truncated,
"outer itself is not truncated",
false,
outer.truncated
);
crate::assert_with_log!(
outer.any_truncated(),
"any_truncated should find inner truncation",
true,
outer.any_truncated()
);
crate::test_complete!("any_truncated_finds_nested_truncation");
}
#[test]
fn stress_deep_chain_bounded_by_config() {
init_test("stress_deep_chain_bounded_by_config");
let config = CancelAttributionConfig::new(16, 4096);
let mut current = CancelReason::timeout();
for _ in 1..100 {
current = CancelReason::parent_cancelled().with_cause_limited(current, &config);
}
crate::assert_with_log!(
current.chain_depth() <= 16,
"deep chain must be bounded by max_chain_depth",
true,
current.chain_depth() <= 16
);
crate::assert_with_log!(
current.any_truncated(),
"deep chain must report truncation",
true,
current.any_truncated()
);
crate::test_complete!("stress_deep_chain_bounded_by_config");
}
#[test]
fn stress_wide_fanout_bounded() {
init_test("stress_wide_fanout_bounded");
let config = CancelAttributionConfig::new(4, 4096);
let root = CancelReason::shutdown();
for _i in 0..200 {
let child = CancelReason::parent_cancelled().with_cause_limited(root.clone(), &config);
crate::assert_with_log!(
child.chain_depth() <= 4,
"fanout child must respect depth limit",
true,
child.chain_depth() <= 4
);
}
crate::test_complete!("stress_wide_fanout_bounded");
}
#[test]
fn zero_depth_config_drops_all_causes() {
init_test("zero_depth_config_drops_all_causes");
let config = CancelAttributionConfig::new(0, usize::MAX);
let cause = CancelReason::timeout();
let result = CancelReason::shutdown().with_cause_limited(cause, &config);
crate::assert_with_log!(
result.cause.is_none(),
"zero depth config should prevent cause attachment",
true,
result.cause.is_none()
);
crate::assert_with_log!(
result.truncated,
"should be marked truncated when cause is dropped",
true,
result.truncated
);
crate::test_complete!("zero_depth_config_drops_all_causes");
}
#[test]
fn depth_one_config_keeps_only_self() {
init_test("depth_one_config_keeps_only_self");
let config = CancelAttributionConfig::new(1, usize::MAX);
let deep = CancelReason::timeout().with_cause(CancelReason::parent_cancelled());
let result = CancelReason::shutdown().with_cause_limited(deep, &config);
crate::assert_with_log!(
result.chain_depth() <= 1,
"depth-1 config should keep only the outermost level",
true,
result.chain_depth() <= 1
);
crate::test_complete!("depth_one_config_keeps_only_self");
}
#[test]
fn zero_memory_config_drops_all_causes() {
init_test("zero_memory_config_drops_all_causes");
let config = CancelAttributionConfig::new(usize::MAX, 0);
let cause = CancelReason::timeout();
let result = CancelReason::shutdown().with_cause_limited(cause, &config);
crate::assert_with_log!(
result.truncated || result.any_truncated(),
"zero memory should trigger truncation",
true,
result.truncated || result.any_truncated()
);
crate::test_complete!("zero_memory_config_drops_all_causes");
}
#[test]
fn stress_incremental_chain_growth() {
init_test("stress_incremental_chain_growth");
let config = CancelAttributionConfig::default();
let root_reason = CancelReason::shutdown();
let mut parent_reason = root_reason;
for _i in 0..50 {
let child_reason =
CancelReason::parent_cancelled().with_cause_limited(parent_reason.clone(), &config);
crate::assert_with_log!(
child_reason.chain_depth() <= config.max_chain_depth,
"incremental chain must stay within configured depth",
true,
child_reason.chain_depth() <= config.max_chain_depth
);
parent_reason = child_reason;
}
crate::assert_with_log!(
parent_reason.any_truncated(),
"deeply nested region chain must report truncation",
true,
parent_reason.any_truncated()
);
crate::test_complete!("stress_incremental_chain_growth");
}
#[test]
fn cancel_kind_debug_clone_copy() {
let k = CancelKind::Timeout;
let k2 = k; let k3 = k; assert_eq!(k2, k3);
let dbg = format!("{k:?}");
assert!(dbg.contains("Timeout"));
}
#[test]
fn cancel_kind_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(CancelKind::User);
set.insert(CancelKind::Shutdown);
set.insert(CancelKind::User); assert_eq!(set.len(), 2);
}
#[test]
fn cancel_kind_as_str_all_variants() {
assert_eq!(CancelKind::User.as_str(), "User");
assert_eq!(CancelKind::Timeout.as_str(), "Timeout");
assert_eq!(CancelKind::Deadline.as_str(), "Deadline");
assert_eq!(CancelKind::PollQuota.as_str(), "PollQuota");
assert_eq!(CancelKind::CostBudget.as_str(), "CostBudget");
assert_eq!(CancelKind::FailFast.as_str(), "FailFast");
assert_eq!(CancelKind::RaceLost.as_str(), "RaceLost");
assert_eq!(CancelKind::ParentCancelled.as_str(), "ParentCancelled");
assert_eq!(
CancelKind::ResourceUnavailable.as_str(),
"ResourceUnavailable"
);
assert_eq!(CancelKind::Shutdown.as_str(), "Shutdown");
assert_eq!(CancelKind::LinkedExit.as_str(), "LinkedExit");
}
#[test]
fn cancel_kind_display_all_variants() {
assert_eq!(format!("{}", CancelKind::User), "user");
assert_eq!(format!("{}", CancelKind::Timeout), "timeout");
assert_eq!(format!("{}", CancelKind::Deadline), "deadline");
assert_eq!(format!("{}", CancelKind::PollQuota), "poll quota");
assert_eq!(format!("{}", CancelKind::CostBudget), "cost budget");
assert_eq!(format!("{}", CancelKind::FailFast), "fail-fast");
assert_eq!(format!("{}", CancelKind::RaceLost), "race lost");
assert_eq!(
format!("{}", CancelKind::ParentCancelled),
"parent cancelled"
);
assert_eq!(
format!("{}", CancelKind::ResourceUnavailable),
"resource unavailable"
);
assert_eq!(format!("{}", CancelKind::Shutdown), "shutdown");
assert_eq!(format!("{}", CancelKind::LinkedExit), "linked exit");
}
#[test]
fn cancel_kind_ord() {
assert!(CancelKind::User < CancelKind::Timeout);
assert!(CancelKind::Shutdown > CancelKind::User);
}
#[test]
fn cancel_phase_debug_clone_copy_eq() {
let p = CancelPhase::Requested;
let p2 = p; assert_eq!(p, p2);
assert!(format!("{p:?}").contains("Requested"));
}
#[test]
fn cancel_phase_ord() {
assert!(CancelPhase::Requested < CancelPhase::Cancelling);
assert!(CancelPhase::Cancelling < CancelPhase::Finalizing);
assert!(CancelPhase::Finalizing < CancelPhase::Completed);
}
#[test]
fn cancel_witness_error_debug_clone_copy_eq() {
let e = CancelWitnessError::TaskMismatch;
let e2 = e; assert_eq!(e, e2);
assert!(format!("{e:?}").contains("TaskMismatch"));
let e3 = CancelWitnessError::RegionMismatch;
assert_ne!(e, e3);
let e4 = CancelWitnessError::EpochMismatch;
assert!(format!("{e4:?}").contains("EpochMismatch"));
let e5 = CancelWitnessError::PhaseRegression {
from: CancelPhase::Cancelling,
to: CancelPhase::Requested,
};
assert!(format!("{e5:?}").contains("PhaseRegression"));
let e6 = CancelWitnessError::ReasonWeakened {
from: CancelKind::Shutdown,
to: CancelKind::User,
};
assert!(format!("{e6:?}").contains("ReasonWeakened"));
}
#[test]
fn cancel_reason_debug_clone_eq() {
let r = CancelReason::timeout();
let dbg = format!("{r:?}");
assert!(dbg.contains("CancelReason"));
let r2 = r.clone();
assert_eq!(r, r2);
}
#[test]
fn cancel_reason_default() {
let r = CancelReason::default();
assert_eq!(r.kind, CancelKind::User);
assert!(r.cause.is_none());
assert!(!r.truncated);
}
#[test]
fn cancel_reason_display_normal() {
let r = CancelReason::timeout();
assert_eq!(format!("{r}"), "timeout");
let r2 = CancelReason::user("custom msg");
assert_eq!(format!("{r2}"), "user: custom msg");
}
#[test]
fn cancel_reason_display_alternate() {
let r = CancelReason::shutdown();
let alt = format!("{r:#}");
assert!(alt.contains("shutdown"));
assert!(alt.contains("from"));
}
#[test]
fn cancel_reason_root_cause_no_chain() {
let r = CancelReason::timeout();
assert_eq!(r.root_cause().kind, CancelKind::Timeout);
}
#[test]
fn cancel_reason_root_cause_with_chain() {
let root = CancelReason::shutdown();
let child = CancelReason::parent_cancelled().with_cause(root);
assert_eq!(child.root_cause().kind, CancelKind::Shutdown);
}
#[test]
fn cancel_reason_chain_depth() {
let r1 = CancelReason::user("a");
assert_eq!(r1.chain_depth(), 1);
let r2 = CancelReason::timeout().with_cause(r1);
assert_eq!(r2.chain_depth(), 2);
let r3 = CancelReason::shutdown().with_cause(r2);
assert_eq!(r3.chain_depth(), 3);
}
#[test]
fn cancel_reason_estimated_memory_cost() {
let r = CancelReason::user("x");
let cost = r.estimated_memory_cost();
assert_eq!(cost, CancelAttributionConfig::estimated_chain_cost(1));
}
#[test]
fn cancel_attribution_config_estimated_chain_cost() {
assert_eq!(CancelAttributionConfig::estimated_chain_cost(0), 0);
assert_eq!(
CancelAttributionConfig::estimated_chain_cost(1),
CancelAttributionConfig::single_reason_cost()
);
assert_eq!(CancelAttributionConfig::estimated_chain_cost(2), 168);
}
#[test]
fn cancel_witness_validate_transition_ok() {
let w1 = CancelWitness::new(
TaskId::testing_default(),
RegionId::testing_default(),
1,
CancelPhase::Requested,
CancelReason::timeout(),
);
let w2 = CancelWitness::new(
TaskId::testing_default(),
RegionId::testing_default(),
1,
CancelPhase::Cancelling,
CancelReason::timeout(),
);
assert!(CancelWitness::validate_transition(Some(&w1), &w2).is_ok());
}
#[test]
fn cancel_witness_validate_transition_none_prev() {
let w = CancelWitness::new(
TaskId::testing_default(),
RegionId::testing_default(),
1,
CancelPhase::Requested,
CancelReason::timeout(),
);
assert!(CancelWitness::validate_transition(None, &w).is_ok());
}
#[test]
fn cancel_witness_validate_phase_regression() {
let w1 = CancelWitness::new(
TaskId::testing_default(),
RegionId::testing_default(),
1,
CancelPhase::Cancelling,
CancelReason::timeout(),
);
let w2 = CancelWitness::new(
TaskId::testing_default(),
RegionId::testing_default(),
1,
CancelPhase::Requested,
CancelReason::timeout(),
);
let err = CancelWitness::validate_transition(Some(&w1), &w2).unwrap_err();
assert!(matches!(err, CancelWitnessError::PhaseRegression { .. }));
}
#[test]
fn canonical_5_mapping_and_extension_policy() {
init_test("canonical_5_mapping_and_extension_policy");
let all_kinds = [
CancelKind::User,
CancelKind::Timeout,
CancelKind::Deadline,
CancelKind::PollQuota,
CancelKind::CostBudget,
CancelKind::FailFast,
CancelKind::RaceLost,
CancelKind::ParentCancelled,
CancelKind::ResourceUnavailable,
CancelKind::Shutdown,
CancelKind::LinkedExit,
];
for kind in &all_kinds {
let sev = kind.severity();
assert!(
sev <= 5,
"CancelKind::{kind:?} has severity {sev} > 5, violating extension policy"
);
}
assert_eq!(CancelKind::User.severity(), 0, "User must be severity 0");
assert_eq!(
CancelKind::Shutdown.severity(),
5,
"Shutdown must be severity 5"
);
let mut covered = [false; 6];
for kind in &all_kinds {
covered[kind.severity() as usize] = true;
}
for (level, &has_kind) in covered.iter().enumerate() {
assert!(has_kind, "Severity level {level} has no CancelKind mapping");
}
for &a in &all_kinds {
for &b in &all_kinds {
let mut reason_a = CancelReason::new(a);
let reason_b = CancelReason::new(b);
let original_sev = reason_a.kind.severity();
reason_a.strengthen(&reason_b);
assert!(
reason_a.kind.severity() >= original_sev,
"strengthen({a:?}, {b:?}) decreased severity from {original_sev} to {}",
reason_a.kind.severity()
);
}
}
}
}