use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::time::{Duration, SystemTime};
use crate::{TaskOutcomeKind, identity::TaskId};
static EVENT_SEQ: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventKind {
SubscriberPanicked,
RuntimeFailure,
SubscriberOverflow,
ShutdownRequested,
AllStoppedWithinGrace,
GraceExceeded,
AttemptStarting,
AttemptSucceeded,
AttemptCanceled,
AttemptFailed,
AttemptTimedOut,
BackoffScheduled,
TaskAddRequested,
TaskAdded,
TaskAddFailed,
TaskRemoveRequested,
TaskRemoved,
TaskFinished,
#[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::RuntimeFailure => "runtime_failure",
EventKind::SubscriberOverflow => "subscriber_overflow",
EventKind::ShutdownRequested => "shutdown_requested",
EventKind::AllStoppedWithinGrace => "all_stopped_within_grace",
EventKind::GraceExceeded => "grace_exceeded",
EventKind::AttemptStarting => "attempt_starting",
EventKind::AttemptSucceeded => "attempt_succeeded",
EventKind::AttemptCanceled => "attempt_canceled",
EventKind::AttemptFailed => "attempt_failed",
EventKind::AttemptTimedOut => "attempt_timed_out",
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::TaskFinished => "task_finished",
#[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(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RejectionKind {
AlreadyExists,
BatchRejected,
SlotBusy,
QueueFull,
SupersededByReplace,
RemovedFromQueue,
ControllerShuttingDown,
AdmissionFailed,
}
impl RejectionKind {
#[must_use]
pub fn as_label(&self) -> &'static str {
match self {
Self::AlreadyExists => "already_exists",
Self::BatchRejected => "batch_rejected",
Self::SlotBusy => "slot_busy",
Self::QueueFull => "queue_full",
Self::SupersededByReplace => "superseded_by_replace",
Self::RemovedFromQueue => "removed_from_queue",
Self::ControllerShuttingDown => "controller_shutting_down",
Self::AdmissionFailed => "admission_failed",
}
}
}
#[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 outcome_kind: Option<TaskOutcomeKind>,
pub rejection_kind: Option<RejectionKind>,
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,
outcome_kind: None,
rejection_kind: 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_outcome_kind(mut self, kind: TaskOutcomeKind) -> Self {
self.outcome_kind = Some(kind);
self
}
#[inline]
#[must_use]
pub fn with_rejection_kind(mut self, kind: RejectionKind) -> Self {
self.rejection_kind = Some(kind);
self.outcome_kind = Some(TaskOutcomeKind::Rejected);
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 runtime_failure(component: impl Into<Arc<str>>, reason: impl Into<Arc<str>>) -> Self {
Event::new(EventKind::RuntimeFailure)
.with_task(component)
.with_reason(reason)
}
#[inline]
#[must_use]
pub fn is_internal_diagnostic(&self) -> bool {
matches!(
self.kind,
EventKind::SubscriberOverflow
| EventKind::SubscriberPanicked
| EventKind::RuntimeFailure
)
}
}
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(outcome_kind) = self.outcome_kind {
d.field("outcome_kind", &outcome_kind);
}
if let Some(rejection_kind) = self.rejection_kind {
d.field("rejection_kind", &rejection_kind);
}
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::AttemptStarting);
let b = Event::new(EventKind::AttemptSucceeded);
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::RuntimeFailure, "runtime_failure"),
(EventKind::SubscriberOverflow, "subscriber_overflow"),
(EventKind::ShutdownRequested, "shutdown_requested"),
(EventKind::AllStoppedWithinGrace, "all_stopped_within_grace"),
(EventKind::GraceExceeded, "grace_exceeded"),
(EventKind::AttemptStarting, "attempt_starting"),
(EventKind::AttemptSucceeded, "attempt_succeeded"),
(EventKind::AttemptCanceled, "attempt_canceled"),
(EventKind::AttemptFailed, "attempt_failed"),
(EventKind::AttemptTimedOut, "attempt_timed_out"),
(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::TaskFinished, "task_finished"),
#[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::AttemptStarting);
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.outcome_kind, None);
assert_eq!(ev.rejection_kind, None);
assert_eq!(ev.task, None);
assert_eq!(ev.id, None);
assert_eq!(ev.backoff_source, None);
}
#[test]
fn rejection_kind_labels_are_stable() {
let cases = [
(RejectionKind::AlreadyExists, "already_exists"),
(RejectionKind::BatchRejected, "batch_rejected"),
(RejectionKind::SlotBusy, "slot_busy"),
(RejectionKind::QueueFull, "queue_full"),
(RejectionKind::SupersededByReplace, "superseded_by_replace"),
(RejectionKind::RemovedFromQueue, "removed_from_queue"),
(
RejectionKind::ControllerShuttingDown,
"controller_shutting_down",
),
(RejectionKind::AdmissionFailed, "admission_failed"),
];
for (kind, expected) in cases {
assert_eq!(kind.as_label(), expected, "{kind:?}");
}
let rejected =
Event::new(EventKind::TaskAddFailed).with_rejection_kind(RejectionKind::AlreadyExists);
assert_eq!(rejected.rejection_kind, Some(RejectionKind::AlreadyExists));
assert_eq!(rejected.outcome_kind, Some(TaskOutcomeKind::Rejected));
}
#[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::AttemptTimedOut,
Event::with_timeout,
|e| e.timeout_ms,
),
(
"delay",
EventKind::BackoffScheduled,
Event::with_delay,
|e| e.delay_ms,
),
(
"duration",
EventKind::AttemptSucceeded,
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_all_variants() {
for kind in [
EventKind::SubscriberOverflow,
EventKind::SubscriberPanicked,
EventKind::RuntimeFailure,
] {
assert!(Event::new(kind).is_internal_diagnostic(), "{kind:?}");
}
assert!(!Event::new(EventKind::AttemptStarting).is_internal_diagnostic());
}
#[test]
fn diagnostic_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"));
let runtime_failure = Event::runtime_failure("registry", "listener join failed");
assert_eq!(runtime_failure.kind, EventKind::RuntimeFailure);
assert_eq!(runtime_failure.task.as_deref(), Some("registry"));
assert_eq!(
runtime_failure.reason.as_deref(),
Some("listener join failed")
);
}
#[test]
fn with_exit_code_keeps_sign() {
for (kind, code) in [
(EventKind::AttemptFailed, 42),
(EventKind::TaskFinished, -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::TaskFinished)
.with_outcome_kind(TaskOutcomeKind::ForceAborted)
.with_exit_code(137);
assert!(
format!("{ev:?}").contains("exit_code: 137"),
"Debug must surface exit_code when present"
);
assert!(
format!("{ev:?}").contains("outcome_kind: ForceAborted"),
"Debug must surface outcome_kind when present"
);
let none = Event::new(EventKind::AttemptSucceeded);
assert!(
!format!("{none:?}").contains("exit_code"),
"Debug must omit exit_code when absent"
);
}
}