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 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
71async 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
85async 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
91async 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 {
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 {
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
174async 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 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 discord_send_succes(ctx, &m).await?;
281 }
282
283 Ok(())
284}