Skip to main content

core_lib/exec/
pipeline.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    sync::Arc,
4};
5
6use anyhow::Result;
7use tokio::sync::Mutex;
8
9use crate::{
10    config::parser::check_dependency_graph,
11    core::watcher::WatchContext,
12    exec::{
13        PipeRegistry,
14        metrics::ExecMetrics,
15        runner::{JobNode, build_dependency_graph, run_step},
16    },
17    notifications::sender::{discord_send_failure, discord_send_succes},
18};
19
20pub async fn run_pipeline(ctx: Arc<WatchContext>) -> Result<()> {
21    let metrics = Arc::new(tokio::sync::Mutex::new(ExecMetrics::new(
22        &ctx.id,
23        &ctx.repo.name,
24    )));
25
26    let pipe_registry = Arc::new(Mutex::new(PipeRegistry {
27        pipes_register: HashMap::new(),
28    }));
29
30    check_dependency_graph(&ctx.config)?;
31    let graph_map = build_dependency_graph(&ctx.config)?;
32    let graph: Arc<Mutex<HashMap<String, JobNode>>> = Arc::new(Mutex::new(graph_map));
33
34    let ready_queue = Arc::new(Mutex::new(VecDeque::new()));
35    initialize_ready_queue(&graph, &ready_queue).await;
36
37    loop {
38        let ready_jobs = drain_ready_queue(&ready_queue).await;
39        if ready_jobs.is_empty() {
40            break;
41        }
42
43        // Parallel execution of ready jobs
44        let handles: Vec<_> = ready_jobs
45            .into_iter()
46            .map(|job_name| {
47                let graph_clone = Arc::clone(&graph);
48                let ready_clone = Arc::clone(&ready_queue);
49                let ctx_clone = Arc::clone(&ctx);
50                let metrics_clone = Arc::clone(&metrics);
51                let pipe_registry_clone = Arc::clone(&pipe_registry);
52
53                tokio::spawn(run_job(
54                    job_name,
55                    graph_clone,
56                    ready_clone,
57                    ctx_clone,
58                    metrics_clone,
59                    pipe_registry_clone,
60                ))
61            })
62            .collect();
63
64        wait_jobs(handles, &metrics, &ctx).await?;
65    }
66
67    finalize_pipeline(&metrics, &ctx).await?;
68    Ok(())
69}
70
71/// Init job queue with job ready to execute based on graph
72async fn initialize_ready_queue(
73    graph: &Arc<Mutex<HashMap<String, JobNode>>>,
74    ready_queue: &Arc<Mutex<VecDeque<String>>>,
75) {
76    let graph_guard = graph.lock().await;
77    let mut ready_guard = ready_queue.lock().await;
78    for (name, node) in graph_guard.iter() {
79        if node.remaining_dependencies == 0 {
80            ready_guard.push_back(name.clone());
81        }
82    }
83}
84
85/// Empty and retrieve ready jobs from the queue
86async fn drain_ready_queue(ready_queue: &Arc<Mutex<VecDeque<String>>>) -> Vec<String> {
87    let mut queue = ready_queue.lock().await;
88    queue.drain(..).collect()
89}
90
91/// Runs a single job with step and dependency management
92async fn run_job(
93    job_name: String,
94    graph: Arc<Mutex<HashMap<String, JobNode>>>,
95    ready_queue: Arc<Mutex<VecDeque<String>>>,
96    ctx: Arc<WatchContext>,
97    metrics: Arc<Mutex<ExecMetrics>>,
98    pipe_registry: Arc<Mutex<PipeRegistry>>,
99) -> Result<bool> {
100    let (job_arc, dependents) = {
101        let g = graph.lock().await;
102        let node = g.get(&job_name).unwrap();
103        (Arc::clone(&node.job), node.dependents.clone())
104    };
105
106    // init job metrics
107    {
108        let mut m = metrics.lock().await;
109        m.job_started(&job_name);
110    }
111    tracing::event!(
112        target: "fleet",
113        tracing::Level::INFO,
114        log_path = %ctx.log_path().display(),
115        kind = "JOB START",
116        message = %job_name,
117    );
118
119    let output_strategy = ctx.config.drop_strategy(&job_name, &ctx)?;
120    for step in &job_arc.steps {
121        if let Err(e) = run_step(
122            &ctx,
123            step,
124            &job_arc.env,
125            &output_strategy,
126            Arc::clone(&pipe_registry),
127        )
128        .await
129        {
130            handle_job_failure(&ctx, &metrics, &job_name, e).await?;
131            return Err(anyhow::anyhow!("Job failed: {job_name}"));
132        }
133    }
134
135    // set job  as finished in metrics
136    {
137        let mut m = metrics.lock().await;
138        m.job_finished(&job_name, true);
139    }
140
141    tracing::info!(
142        target: "fleet",
143        log_path = %ctx.log_path().display(),
144        message = %format!("Job {job_name} succeeded"),
145    );
146    update_dependents(&graph, &ready_queue, &dependents).await;
147    tracing::event!(
148        target: "fleet",
149        tracing::Level::INFO,
150        log_path = %ctx.log_path().display(),
151        kind = "JOB END",
152        message = %job_name,
153    );
154    Ok(true)
155}
156
157async fn update_dependents(
158    graph: &Arc<Mutex<HashMap<String, JobNode>>>,
159    ready_queue: &Arc<Mutex<VecDeque<String>>>,
160    dependents: &[String],
161) {
162    let mut g = graph.lock().await;
163    for dep_name in dependents {
164        let dep_node = g.get_mut(dep_name).unwrap();
165        if dep_node.remaining_dependencies != usize::MAX {
166            dep_node.remaining_dependencies -= 1;
167            if dep_node.remaining_dependencies == 0 {
168                ready_queue.lock().await.push_back(dep_name.clone());
169            }
170        }
171    }
172}
173
174/// Manage job failure (log, metrics, notifications)
175async fn handle_job_failure(
176    ctx: &Arc<WatchContext>,
177    metrics: &Arc<Mutex<ExecMetrics>>,
178    job_name: &str,
179    error: anyhow::Error,
180) -> Result<()> {
181    {
182        let mut m = metrics.lock().await;
183        m.job_finished(job_name, false);
184    }
185
186    let need_notif_on_failure = ctx
187        .config
188        .pipeline
189        .notifications
190        .as_ref()
191        .map(|notif| notif.on.contains(&"failure".to_string()))
192        .unwrap_or(false);
193
194    if need_notif_on_failure {
195        // Snapshot metrics, drop the lock, then send the notification: HTTP
196        // must never happen while holding the metrics mutex.
197        let m = metrics.lock().await.clone();
198        let err = error.to_string();
199        let lines: Vec<&str> = err.lines().collect();
200        let first_line = lines.first().unwrap_or(&"");
201        let second_line = lines.get(1).unwrap_or(&"");
202        discord_send_failure(
203            ctx,
204            &format!(
205                "**Job** `{job_name}` **failed**
206                {first_line}
207                **Error:** `{second_line}`",
208            ),
209            &m,
210        )
211        .await?;
212    }
213
214    tracing::error!(
215        target: "fleet",
216        log_path = %ctx.log_path().display(),
217        message = %format!("Job {job_name} failed"),
218    );
219    Ok(())
220}
221
222async fn wait_jobs(
223    handles: Vec<tokio::task::JoinHandle<Result<bool, anyhow::Error>>>,
224    metrics: &Arc<Mutex<ExecMetrics>>,
225    ctx: &Arc<WatchContext>,
226) -> Result<()> {
227    for h in handles {
228        match h.await {
229            Ok(inner) => match inner {
230                Ok(_) => {}
231                Err(e) => {
232                    let mut m = metrics.lock().await;
233                    m.finalize();
234                    m.save().await.ok();
235                    tracing::error!(
236                        target: "fleet",
237                        log_path = %ctx.log_path().display(),
238                        message = %format!("Pipeline failed: {e}"),
239                    );
240                    return Err(anyhow::anyhow!("Pipeline failed: {e}"));
241                }
242            },
243            Err(e) => {
244                let mut m = metrics.lock().await;
245                m.finalize();
246                m.save().await.ok();
247                tracing::error!(
248                    target: "fleet",
249                    log_path = %ctx.log_path().display(),
250                    message = %format!("Pipeline failed: {e}"),
251                );
252                return Err(anyhow::anyhow!("Pipeline failed: {e}"));
253            }
254        }
255    }
256    Ok(())
257}
258
259async fn finalize_pipeline(
260    metrics: &Arc<Mutex<ExecMetrics>>,
261    ctx: &Arc<WatchContext>,
262) -> Result<()> {
263    let m = {
264        let mut m = metrics.lock().await;
265        m.finalize();
266        m.save().await?;
267        m.clone()
268    };
269
270    let need_notif_on_success = ctx
271        .config
272        .pipeline
273        .notifications
274        .as_ref()
275        .map(|notif| notif.on.contains(&"success".to_string()))
276        .unwrap_or(false);
277
278    if need_notif_on_success {
279        // Snapshot released the lock: HTTP happens outside the metrics mutex.
280        discord_send_succes(ctx, &m).await?;
281    }
282
283    Ok(())
284}