Skip to main content

ironflow_runtime/trigger/
mod.rs

1//! Pluggable trigger sources for starting workflow runs.
2//!
3//! A [`Trigger`] listens for external or internal signals and emits
4//! [`TriggerEvent`]s through a [`TriggerSink`]. The runtime starts all
5//! registered triggers alongside the HTTP server and cron scheduler, and
6//! forwards their events to the configured handler.
7//!
8//! Built-in triggers:
9//! - `EventTrigger` -- reacts to internal domain events (workflow chaining).
10//! - `NatsTrigger` -- consumes messages from a NATS JetStream subject
11//!   (behind the `trigger-nats` feature flag).
12//! - [`PollingTrigger`](polling::PollingTrigger) -- periodically polls an
13//!   external source via a [`PollingProbe`](polling::PollingProbe) and
14//!   triggers when new data is detected. Built-in probes: `HttpProbe`
15//!   (`trigger-polling-http`) and `SqlProbe` (`trigger-polling-sql`).
16//!
17//! # Examples
18//!
19//! ```no_run
20//! use ironflow_runtime::trigger::{Trigger, TriggerSink, TriggerEvent};
21//! use ironflow_runtime::trigger::event::{EventTrigger, EventTriggerRule};
22//! use ironflow_store::entities::EventKind;
23//!
24//! let trigger = EventTrigger::new(vec![
25//!     EventTriggerRule {
26//!         on_event: EventKind::RunFailed,
27//!         source_workflow: "deploy".to_string(),
28//!         target_workflow: "rollback".to_string(),
29//!         max_chain_depth: 3,
30//!         conditions: vec![],
31//!     },
32//! ]);
33//! ```
34
35pub mod event;
36#[cfg(feature = "trigger-nats")]
37pub mod nats;
38pub mod polling;
39
40use std::future::Future;
41use std::pin::Pin;
42
43use serde_json::Value;
44use tokio::sync::mpsc;
45use tokio_util::sync::CancellationToken;
46
47use ironflow_store::entities::TriggerKind;
48
49/// Future returned by [`Trigger::start`].
50pub type TriggerFuture<'a> = Pin<Box<dyn Future<Output = Result<(), TriggerError>> + Send + 'a>>;
51
52/// A source of workflow run triggers.
53///
54/// Implementations listen for some signal (domain event, message queue,
55/// external webhook, etc.) and send [`TriggerEvent`]s through the
56/// [`TriggerSink`] to request run creation.
57///
58/// # Lifecycle
59///
60/// 1. The runtime calls [`start`](Trigger::start) with a sink and a
61///    cancellation token.
62/// 2. The trigger runs until the token is cancelled (graceful shutdown)
63///    or an unrecoverable error occurs.
64/// 3. Dropping the trigger releases all resources.
65///
66/// # Examples
67///
68/// ```no_run
69/// use ironflow_runtime::trigger::{Trigger, TriggerFuture, TriggerSink, TriggerError};
70/// use tokio_util::sync::CancellationToken;
71///
72/// struct MyTrigger;
73///
74/// impl Trigger for MyTrigger {
75///     fn name(&self) -> &str { "my-trigger" }
76///
77///     fn start<'a>(
78///         &'a self,
79///         _sink: TriggerSink,
80///         token: &'a CancellationToken,
81///     ) -> TriggerFuture<'a> {
82///         Box::pin(async move {
83///             token.cancelled().await;
84///             Ok(())
85///         })
86///     }
87/// }
88/// ```
89pub trait Trigger: Send + Sync {
90    /// Human-readable name for logging.
91    fn name(&self) -> &str;
92
93    /// Start the trigger.
94    ///
95    /// The implementation should send [`TriggerEvent`]s through `sink`
96    /// whenever a run should be created, and return when `token` is
97    /// cancelled.
98    fn start<'a>(&'a self, sink: TriggerSink, token: &'a CancellationToken) -> TriggerFuture<'a>;
99}
100
101/// Error type for trigger operations.
102///
103/// # Examples
104///
105/// ```
106/// use ironflow_runtime::trigger::TriggerError;
107///
108/// let err = TriggerError::Failed("connection lost".to_string());
109/// assert!(err.to_string().contains("connection lost"));
110/// ```
111#[derive(Debug, thiserror::Error)]
112pub enum TriggerError {
113    /// A trigger encountered an unrecoverable error.
114    #[error("trigger failed: {0}")]
115    Failed(String),
116}
117
118/// A request to create a workflow run, emitted by a [`Trigger`].
119///
120/// # Examples
121///
122/// ```
123/// use ironflow_runtime::trigger::TriggerEvent;
124/// use ironflow_store::entities::TriggerKind;
125/// use serde_json::json;
126///
127/// let event = TriggerEvent {
128///     workflow_name: "rollback".to_string(),
129///     payload: json!({"source": "deploy"}),
130///     trigger_kind: TriggerKind::Manual,
131/// };
132/// assert_eq!(event.workflow_name, "rollback");
133/// ```
134#[derive(Debug, Clone)]
135pub struct TriggerEvent {
136    /// The workflow to run.
137    pub workflow_name: String,
138    /// Payload passed to the new run.
139    pub payload: Value,
140    /// How the run was triggered (stored on the run record).
141    pub trigger_kind: TriggerKind,
142}
143
144/// Channel endpoint for triggers to emit [`TriggerEvent`]s.
145///
146/// Obtained from [`TriggerSink::channel`] and passed to
147/// [`Trigger::start`].
148///
149/// # Examples
150///
151/// ```
152/// use ironflow_runtime::trigger::{TriggerSink, TriggerEvent};
153/// use ironflow_store::entities::TriggerKind;
154/// use serde_json::json;
155///
156/// let (sink, mut rx) = TriggerSink::channel(16);
157///
158/// # tokio_test::block_on(async {
159/// sink.send(TriggerEvent {
160///     workflow_name: "deploy".to_string(),
161///     payload: json!({}),
162///     trigger_kind: TriggerKind::Manual,
163/// }).await.unwrap();
164///
165/// let event = rx.recv().await.unwrap();
166/// assert_eq!(event.workflow_name, "deploy");
167/// # });
168/// ```
169#[derive(Clone)]
170pub struct TriggerSink {
171    tx: mpsc::Sender<TriggerEvent>,
172}
173
174impl TriggerSink {
175    /// Create a sink/receiver pair with the given buffer capacity.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use ironflow_runtime::trigger::TriggerSink;
181    ///
182    /// let (sink, rx) = TriggerSink::channel(32);
183    /// drop(rx);
184    /// ```
185    pub fn channel(buffer: usize) -> (Self, mpsc::Receiver<TriggerEvent>) {
186        let (tx, rx) = mpsc::channel(buffer);
187        (Self { tx }, rx)
188    }
189
190    /// Send a trigger event.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the receiver has been dropped.
195    pub async fn send(&self, event: TriggerEvent) -> Result<(), TriggerError> {
196        self.tx
197            .send(event)
198            .await
199            .map_err(|e| TriggerError::Failed(format!("sink closed: {e}")))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use serde_json::json;
207
208    #[test]
209    fn trigger_error_display() {
210        let err = TriggerError::Failed("boom".to_string());
211        assert_eq!(err.to_string(), "trigger failed: boom");
212    }
213
214    #[tokio::test]
215    async fn sink_sends_and_receives() {
216        let (sink, mut rx) = TriggerSink::channel(4);
217        sink.send(TriggerEvent {
218            workflow_name: "deploy".to_string(),
219            payload: json!({"key": "val"}),
220            trigger_kind: TriggerKind::Manual,
221        })
222        .await
223        .unwrap();
224
225        let event = rx.recv().await.unwrap();
226        assert_eq!(event.workflow_name, "deploy");
227        assert_eq!(event.payload, json!({"key": "val"}));
228    }
229
230    #[tokio::test]
231    async fn sink_errors_when_receiver_dropped() {
232        let (sink, rx) = TriggerSink::channel(1);
233        drop(rx);
234
235        let result = sink
236            .send(TriggerEvent {
237                workflow_name: "x".to_string(),
238                payload: json!(null),
239                trigger_kind: TriggerKind::Manual,
240            })
241            .await;
242        assert!(result.is_err());
243    }
244
245    #[test]
246    fn trigger_event_clone() {
247        let event = TriggerEvent {
248            workflow_name: "w".to_string(),
249            payload: json!(42),
250            trigger_kind: TriggerKind::Api,
251        };
252        let cloned = event.clone();
253        assert_eq!(cloned.workflow_name, "w");
254    }
255}