use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::time::{Duration, SystemTime};
use crate::identity::TaskId;
static EVENT_SEQ: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventKind {
SubscriberPanicked,
SubscriberOverflow,
ShutdownRequested,
AllStoppedWithinGrace,
GraceExceeded,
TaskStarting,
TaskStopped,
TaskCanceled,
TaskFailed,
TimeoutHit,
BackoffScheduled,
TaskAddRequested,
TaskAdded,
TaskAddFailed,
TaskRemoveRequested,
TaskRemoved,
ActorExhausted,
ActorDead,
#[cfg(feature = "controller")]
ControllerRejected,
#[cfg(feature = "controller")]
ControllerSubmitted,
#[cfg(feature = "controller")]
ControllerSlotTransition,
}
impl EventKind {
#[must_use]
pub fn as_label(&self) -> &'static str {
match self {
EventKind::SubscriberPanicked => "subscriber_panicked",
EventKind::SubscriberOverflow => "subscriber_overflow",
EventKind::ShutdownRequested => "shutdown_requested",
EventKind::AllStoppedWithinGrace => "all_stopped_within_grace",
EventKind::GraceExceeded => "grace_exceeded",
EventKind::TaskStarting => "task_starting",
EventKind::TaskStopped => "task_stopped",
EventKind::TaskCanceled => "task_canceled",
EventKind::TaskFailed => "task_failed",
EventKind::TimeoutHit => "timeout_hit",
EventKind::BackoffScheduled => "backoff_scheduled",
EventKind::TaskAddRequested => "task_add_requested",
EventKind::TaskAdded => "task_added",
EventKind::TaskAddFailed => "task_add_failed",
EventKind::TaskRemoveRequested => "task_remove_requested",
EventKind::TaskRemoved => "task_removed",
EventKind::ActorExhausted => "actor_exhausted",
EventKind::ActorDead => "actor_dead",
#[cfg(feature = "controller")]
EventKind::ControllerRejected => "controller_rejected",
#[cfg(feature = "controller")]
EventKind::ControllerSubmitted => "controller_submitted",
#[cfg(feature = "controller")]
EventKind::ControllerSlotTransition => "controller_slot_transition",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackoffSource {
Success,
Failure,
}
#[derive(Clone)]
#[non_exhaustive]
pub struct Event {
pub seq: u64,
pub at: SystemTime,
pub timeout_ms: Option<u32>,
pub delay_ms: Option<u32>,
pub duration_ms: Option<u32>,
pub reason: Option<Arc<str>>,
pub attempt: Option<u32>,
pub task: Option<Arc<str>>,
pub id: Option<TaskId>,
pub exit_code: Option<i32>,
pub kind: EventKind,
pub backoff_source: Option<BackoffSource>,
}
impl Event {
#[must_use]
pub fn new(kind: EventKind) -> Self {
Self {
seq: EVENT_SEQ.fetch_add(1, AtomicOrdering::Relaxed),
kind,
at: SystemTime::now(),
backoff_source: None,
timeout_ms: None,
delay_ms: None,
duration_ms: None,
attempt: None,
reason: None,
task: None,
id: None,
exit_code: None,
}
}
#[inline]
#[must_use]
pub fn with_reason(mut self, reason: impl Into<Arc<str>>) -> Self {
self.reason = Some(reason.into());
self
}
#[inline]
#[must_use]
pub fn with_task(mut self, task: impl Into<Arc<str>>) -> Self {
self.task = Some(task.into());
self
}
#[inline]
#[must_use]
pub fn with_id(mut self, id: TaskId) -> Self {
self.id = Some(id);
self
}
#[inline]
#[must_use]
pub fn with_timeout(mut self, d: Duration) -> Self {
let ms = d.as_millis().min(u128::from(u32::MAX)) as u32;
self.timeout_ms = Some(ms);
self
}
#[inline]
#[must_use]
pub fn with_delay(mut self, d: Duration) -> Self {
let ms = d.as_millis().min(u128::from(u32::MAX)) as u32;
self.delay_ms = Some(ms);
self
}
#[inline]
#[must_use]
pub fn with_duration(mut self, d: Duration) -> Self {
let ms = d.as_millis().min(u128::from(u32::MAX)) as u32;
self.duration_ms = Some(ms);
self
}
#[inline]
#[must_use]
pub fn with_attempt(mut self, n: u32) -> Self {
self.attempt = Some(n);
self
}
#[inline]
#[must_use]
pub fn with_exit_code(mut self, code: i32) -> Self {
self.exit_code = Some(code);
self
}
#[inline]
#[must_use]
pub fn with_backoff_success(mut self) -> Self {
self.backoff_source = Some(BackoffSource::Success);
self
}
#[inline]
#[must_use]
pub fn with_backoff_failure(mut self) -> Self {
self.backoff_source = Some(BackoffSource::Failure);
self
}
#[inline]
#[must_use]
pub fn subscriber_overflow(
subscriber: impl Into<Arc<str>>,
reason: impl Into<Arc<str>>,
) -> Self {
Event::new(EventKind::SubscriberOverflow)
.with_task(subscriber)
.with_reason(reason)
}
#[inline]
#[must_use]
pub fn subscriber_panicked(subscriber: impl Into<Arc<str>>, info: impl Into<Arc<str>>) -> Self {
Event::new(EventKind::SubscriberPanicked)
.with_task(subscriber)
.with_reason(info)
}
#[inline]
#[must_use]
pub fn is_internal_diagnostic(&self) -> bool {
matches!(
self.kind,
EventKind::SubscriberOverflow | EventKind::SubscriberPanicked
)
}
}
impl std::fmt::Debug for Event {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("Event");
d.field("seq", &self.seq);
d.field("kind", &self.kind);
if let Some(id) = self.id {
d.field("id", &id);
}
if let Some(ref task) = self.task {
d.field("task", task);
}
if let Some(attempt) = self.attempt {
d.field("attempt", &attempt);
}
if let Some(ref reason) = self.reason {
d.field("reason", reason);
}
if let Some(timeout_ms) = self.timeout_ms {
d.field("timeout_ms", &timeout_ms);
}
if let Some(delay_ms) = self.delay_ms {
d.field("delay_ms", &delay_ms);
}
if let Some(duration_ms) = self.duration_ms {
d.field("duration_ms", &duration_ms);
}
if let Some(exit_code) = self.exit_code {
d.field("exit_code", &exit_code);
}
if let Some(ref src) = self.backoff_source {
d.field("backoff_source", src);
}
d.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seq_increases_monotonically() {
let a = Event::new(EventKind::TaskStarting);
let b = Event::new(EventKind::TaskStopped);
assert!(b.seq > a.seq, "seq must grow: {} vs {}", a.seq, b.seq);
}
#[test]
fn new_event_leaves_all_optionals_empty() {
let ev = Event::new(EventKind::TaskStarting);
assert_eq!(ev.timeout_ms, None);
assert_eq!(ev.delay_ms, None);
assert_eq!(ev.duration_ms, None);
assert_eq!(ev.attempt, None);
assert_eq!(ev.exit_code, None);
assert_eq!(ev.reason, None);
assert_eq!(ev.task, None);
assert_eq!(ev.id, None);
assert_eq!(ev.backoff_source, None);
}
#[test]
fn ms_builders_set_then_clamp_to_u32_max() {
let normal = Duration::from_millis(42);
let huge = Duration::from_millis(u64::from(u32::MAX) + 1000);
assert_eq!(
Event::new(EventKind::TimeoutHit)
.with_timeout(normal)
.timeout_ms,
Some(42)
);
assert_eq!(
Event::new(EventKind::TimeoutHit)
.with_timeout(huge)
.timeout_ms,
Some(u32::MAX)
);
assert_eq!(
Event::new(EventKind::BackoffScheduled)
.with_delay(normal)
.delay_ms,
Some(42)
);
assert_eq!(
Event::new(EventKind::BackoffScheduled)
.with_delay(huge)
.delay_ms,
Some(u32::MAX)
);
assert_eq!(
Event::new(EventKind::TaskStopped)
.with_duration(normal)
.duration_ms,
Some(42)
);
assert_eq!(
Event::new(EventKind::TaskStopped)
.with_duration(huge)
.duration_ms,
Some(u32::MAX)
);
}
#[test]
fn is_internal_diagnostic_covers_both_variants() {
assert!(Event::new(EventKind::SubscriberOverflow).is_internal_diagnostic());
assert!(Event::new(EventKind::SubscriberPanicked).is_internal_diagnostic());
assert!(!Event::new(EventKind::TaskStarting).is_internal_diagnostic());
}
#[test]
fn subscriber_factories_set_kind_task_and_reason() {
let overflow = Event::subscriber_overflow("my-sub", "full");
assert_eq!(overflow.kind, EventKind::SubscriberOverflow);
assert_eq!(
overflow.task.as_deref(),
Some("my-sub"),
"subscriber name lives in `task`"
);
assert_eq!(
overflow.reason.as_deref(),
Some("full"),
"`reason` is the bare cause, not a re-encoding of the subscriber name"
);
let panicked = Event::subscriber_panicked("my-sub", "boom");
assert_eq!(panicked.kind, EventKind::SubscriberPanicked);
assert_eq!(panicked.task.as_deref(), Some("my-sub"));
assert_eq!(panicked.reason.as_deref(), Some("boom"));
}
#[test]
fn with_exit_code_keeps_sign() {
assert_eq!(
Event::new(EventKind::TaskFailed)
.with_exit_code(42)
.exit_code,
Some(42)
);
assert_eq!(
Event::new(EventKind::ActorDead)
.with_exit_code(-1)
.exit_code,
Some(-1)
);
}
#[test]
fn debug_renders_exit_code_only_when_set() {
let ev = Event::new(EventKind::ActorExhausted).with_exit_code(137);
assert!(
format!("{ev:?}").contains("exit_code: 137"),
"Debug must surface exit_code when present"
);
let none = Event::new(EventKind::TaskStopped);
assert!(
!format!("{none:?}").contains("exit_code"),
"Debug must omit exit_code when absent"
);
}
}