faucet_cli/serve/triggers/
enqueue.rs1use super::compiled::CompiledTrigger;
6use super::context::{self, TriggerEvent};
7use super::{metrics, spec::PipelineRef};
8use crate::serve::runner::{self, ConfigFormatWire, SubmitRequest};
9use crate::serve::state::ServerState;
10
11#[derive(Debug)]
12pub enum FireOutcome {
13 Enqueued(String),
15 Coalesced,
18 Dropped(&'static str),
21 Error(String),
23}
24
25impl FireOutcome {
26 pub fn committed(&self) -> bool {
28 matches!(self, FireOutcome::Enqueued(_) | FireOutcome::Coalesced)
29 }
30}
31
32pub async fn resolve_config_text(
34 config: &PipelineRef,
35 event: &TriggerEvent,
36 name: &str,
37 fired_at: &str,
38) -> Result<String, String> {
39 let raw = match config {
40 PipelineRef::Path(p) => tokio::fs::read_to_string(p)
41 .await
42 .map_err(|e| format!("reading pipeline config '{p}': {e}"))?,
43 PipelineRef::Inline(v) => {
44 serde_yaml::to_string(v).map_err(|e| format!("serializing inline pipeline: {e}"))?
45 }
46 };
47 context::substitute(&raw, event, name, fired_at)
48}
49
50pub fn build_submit_request(
52 compiled: &CompiledTrigger,
53 event: &TriggerEvent,
54 config_text: String,
55 fired_at: &str,
56) -> SubmitRequest {
57 let name = compiled.name();
58 let mut labels = context::labels(name, event);
59 labels.extend(compiled.spec.run.labels.clone());
60 let run_name = compiled
61 .spec
62 .run
63 .name
64 .as_deref()
65 .map(|tpl| context::render_name(tpl, event, name, fired_at))
66 .unwrap_or_else(|| name.to_string());
67 SubmitRequest {
68 config: config_text,
69 config_format: ConfigFormatWire::Yaml,
70 name: Some(run_name),
71 labels,
72 timeout_secs: compiled.spec.run.timeout_secs,
73 doctor_first: false,
74 idempotency_key: Some(context::idempotency_key(name, event)),
75 clock: None,
76 callback: None,
82 }
83}
84
85pub async fn fire(
87 state: &ServerState,
88 compiled: &CompiledTrigger,
89 event: TriggerEvent,
90 fired_at: &str,
91) -> FireOutcome {
92 let kind = compiled.kind_label();
93 metrics::fired(compiled.name(), kind);
94
95 let text =
96 match resolve_config_text(&compiled.spec.config, &event, compiled.name(), fired_at).await {
97 Ok(t) => t,
98 Err(e) => {
99 metrics::error(compiled.name(), kind);
100 return FireOutcome::Error(e);
101 }
102 };
103 let req = build_submit_request(compiled, &event, text, fired_at);
104 let actor = crate::serve::rbac::AuthContext::trigger(compiled.name());
112 match runner::submit(state.clone(), req, actor).await {
113 Ok(resp) => {
114 metrics::enqueued(compiled.name());
115 FireOutcome::Enqueued(resp.run_id)
116 }
117 Err(crate::serve::error::ServeError::QueueFull { .. }) => {
118 metrics::dropped(compiled.name(), "queue_full");
119 FireOutcome::Dropped("queue_full")
120 }
121 Err(crate::serve::error::ServeError::Conflict(_)) => {
122 metrics::coalesced(compiled.name());
125 FireOutcome::Coalesced
126 }
127 Err(e) => {
128 metrics::error(compiled.name(), kind);
129 FireOutcome::Error(e.api_error().error.message)
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::serve::triggers::spec::{RunTemplate, TriggerKind, TriggerSpec};
138
139 fn compiled_webhook() -> CompiledTrigger {
140 CompiledTrigger {
141 spec: TriggerSpec {
142 name: "hook".into(),
143 enabled: true,
144 config: PipelineRef::Path("/tmp/x.yaml".into()),
145 run: RunTemplate {
146 name: Some("{name}:{object_key}".into()),
147 labels: Default::default(),
148 timeout_secs: Some(60),
149 },
150 kind: TriggerKind::Webhook {
151 methods: vec!["POST".into()],
152 dedupe_header: None,
153 debounce_secs: 0,
154 },
155 },
156 webhook_path: Some("/v1/triggers/hook".into()),
157 }
158 }
159
160 #[test]
161 fn builds_request_with_labels_idem_and_timeout() {
162 let event = TriggerEvent::Object {
163 bucket: "b".into(),
164 key: "k".into(),
165 size: 1,
166 last_modified: "2026-06-12T00:00:00Z".into(),
167 };
168 let req = build_submit_request(&compiled_webhook(), &event, "version: 1".into(), "now");
169 assert_eq!(req.name.as_deref(), Some("hook:k"));
170 assert_eq!(req.timeout_secs, Some(60));
171 assert_eq!(
172 req.idempotency_key.as_deref(),
173 Some("trig:hook:b:k:2026-06-12T00:00:00Z")
174 );
175 assert_eq!(
176 req.labels.get("faucet.trigger.name").map(String::as_str),
177 Some("hook")
178 );
179 assert!(!req.doctor_first);
180 }
181}