Skip to main content

soma_fleet/
event.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use soma_ops::Timestamp;
4
5use crate::{FleetResult, HostId, TopologyRevision};
6
7/// Stable fleet lifecycle event category.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum FleetEventKind {
11    /// A topology snapshot was loaded.
12    TopologyLoaded,
13    /// A connection was opened for one exact revision.
14    ConnectionOpened,
15    /// A cached connection was invalidated.
16    ConnectionInvalidated,
17    /// Command execution began.
18    CommandStarted,
19    /// Command execution completed.
20    CommandCompleted,
21    /// Transfer began.
22    TransferStarted,
23    /// Transfer completed.
24    TransferCompleted,
25    /// One target was cancelled.
26    Cancelled,
27    /// One target exceeded its deadline.
28    TimedOut,
29}
30
31/// Product-neutral fleet lifecycle event.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct FleetEvent {
34    kind: FleetEventKind,
35    host: Option<HostId>,
36    revision: Option<TopologyRevision>,
37    occurred_at: Timestamp,
38    detail: Option<String>,
39}
40
41impl FleetEvent {
42    /// Creates a fleet event.
43    #[must_use]
44    pub const fn new(kind: FleetEventKind, occurred_at: Timestamp) -> Self {
45        Self {
46            kind,
47            host: None,
48            revision: None,
49            occurred_at,
50            detail: None,
51        }
52    }
53
54    /// Binds the event to one host and topology revision.
55    #[must_use]
56    pub fn with_host(mut self, host: &crate::HostRecord) -> Self {
57        self.host = Some(host.id().clone());
58        self.revision = Some(host.revision().clone());
59        self
60    }
61
62    /// Adds bounded human-readable detail.
63    pub fn with_detail(mut self, detail: impl Into<String>) -> FleetResult<Self> {
64        let detail = detail.into();
65        let count = detail.chars().count();
66        if count == 0 || count > 1024 || detail.chars().any(char::is_control) {
67            return Err(crate::FleetError::EventSink(
68                "invalid fleet event detail".into(),
69            ));
70        }
71        self.detail = Some(detail);
72        Ok(self)
73    }
74
75    /// Returns the event kind.
76    #[must_use]
77    pub const fn kind(&self) -> FleetEventKind {
78        self.kind
79    }
80
81    /// Returns the optional host identity.
82    #[must_use]
83    pub fn host(&self) -> Option<&HostId> {
84        self.host.as_ref()
85    }
86
87    /// Returns the optional topology revision.
88    #[must_use]
89    pub fn revision(&self) -> Option<&TopologyRevision> {
90        self.revision.as_ref()
91    }
92
93    /// Returns event time.
94    #[must_use]
95    pub const fn occurred_at(&self) -> Timestamp {
96        self.occurred_at
97    }
98
99    /// Returns optional event detail.
100    #[must_use]
101    pub fn detail(&self) -> Option<&str> {
102        self.detail.as_deref()
103    }
104}
105
106/// Sink for durable or observable fleet lifecycle events.
107#[async_trait]
108pub trait FleetEventSink: Send + Sync {
109    /// Emits one event.
110    async fn emit(&self, event: FleetEvent) -> FleetResult<()>;
111}
112
113/// Event sink that discards all events.
114#[derive(Debug, Clone, Copy, Default)]
115pub struct NoopFleetEventSink;
116
117#[async_trait]
118impl FleetEventSink for NoopFleetEventSink {
119    async fn emit(&self, _event: FleetEvent) -> FleetResult<()> {
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125#[path = "event_tests.rs"]
126mod tests;