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")]
#[cfg_attr(docsrs, doc(cfg(feature = "controller")))]
ControllerRejected,
#[cfg(feature = "controller")]
#[cfg_attr(docsrs, doc(cfg(feature = "controller")))]
ControllerSubmitted,
#[cfg(feature = "controller")]
#[cfg_attr(docsrs, doc(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,
}
impl BackoffSource {
#[must_use]
pub fn as_label(&self) -> &'static str {
match self {
BackoffSource::Success => "success",
BackoffSource::Failure => "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_source(mut self, source: BackoffSource) -> Self {
self.backoff_source = Some(source);
self
}
#[inline]
#[must_use]
pub fn with_backoff_success(self) -> Self {
self.with_backoff_source(BackoffSource::Success)
}
#[inline]
#[must_use]
pub fn with_backoff_failure(self) -> Self {
self.with_backoff_source(BackoffSource::Failure)
}
#[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 event_kind_labels_are_stable() {
let cases = [
(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",
),
];
for (kind, expected) in cases {
assert_eq!(kind.as_label(), expected, "{kind:?}");
}
}
#[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);
type Builder = fn(Event, Duration) -> Event;
type ReadMs = fn(&Event) -> Option<u32>;
let cases: [(&str, EventKind, Builder, ReadMs); 3] = [
("timeout", EventKind::TimeoutHit, Event::with_timeout, |e| {
e.timeout_ms
}),
(
"delay",
EventKind::BackoffScheduled,
Event::with_delay,
|e| e.delay_ms,
),
(
"duration",
EventKind::TaskStopped,
Event::with_duration,
|e| e.duration_ms,
),
];
for (label, kind, build, read) in cases {
assert_eq!(read(&build(Event::new(kind), normal)), Some(42), "{label}");
assert_eq!(
read(&build(Event::new(kind), huge)),
Some(u32::MAX),
"{label} must saturate"
);
}
}
#[test]
fn is_internal_diagnostic_covers_both_variants() {
for kind in [EventKind::SubscriberOverflow, EventKind::SubscriberPanicked] {
assert!(Event::new(kind).is_internal_diagnostic(), "{kind:?}");
}
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() {
for (kind, code) in [(EventKind::TaskFailed, 42), (EventKind::ActorDead, -1)] {
assert_eq!(Event::new(kind).with_exit_code(code).exit_code, Some(code));
}
}
#[test]
fn backoff_source_labels_and_builders_are_stable() {
for (source, label) in [
(BackoffSource::Success, "success"),
(BackoffSource::Failure, "failure"),
] {
assert_eq!(source.as_label(), label);
}
let generic =
Event::new(EventKind::BackoffScheduled).with_backoff_source(BackoffSource::Failure);
assert_eq!(generic.backoff_source, Some(BackoffSource::Failure));
assert_eq!(
Event::new(EventKind::BackoffScheduled)
.with_backoff_success()
.backoff_source,
Some(BackoffSource::Success)
);
assert_eq!(
Event::new(EventKind::BackoffScheduled)
.with_backoff_failure()
.backoff_source,
Some(BackoffSource::Failure)
);
}
#[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"
);
}
}