1use std::time::{Duration, Instant};
2
3use super::{
4 batch::{indexed_failures, indexed_outcomes},
5 transaction::{
6 ArtifactBuildTransaction, ArtifactCacheError, ArtifactCacheOutcome,
7 ArtifactCachePreparation, ArtifactCacheSpec, ArtifactCacheTimings, prepare_artifact_cache,
8 },
9};
10
11#[derive(Debug)]
13pub struct ArtifactCacheBatchReport<E> {
14 results: Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>>,
15 entry_elapsed: Vec<Duration>,
16 total: Duration,
17}
18
19#[derive(Debug)]
21pub struct ArtifactCacheBatchFailedEntry<'a, E> {
22 index: usize,
23 failure: &'a ArtifactCacheBatchFailure<E>,
24 entry_elapsed: Duration,
25}
26
27#[derive(Debug)]
29pub enum ArtifactCacheBatchFailure<E> {
30 Cache {
32 source: Box<ArtifactCacheError>,
34 },
35 Build {
37 source: Box<E>,
39 cleanup_error: Option<Box<ArtifactCacheError>>,
41 },
42}
43
44#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub struct ArtifactCacheBatchMetrics {
47 entries: usize,
48 succeeded: usize,
49 failed: usize,
50 built: usize,
51 reused: usize,
52 successful_timings: ArtifactCacheTimings,
53 total: Duration,
54}
55
56impl<E> ArtifactCacheBatchReport<E> {
57 pub fn results(&self) -> &[Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>] {
59 &self.results
60 }
61
62 #[must_use]
67 pub fn entry_elapsed(&self) -> &[Duration] {
68 &self.entry_elapsed
69 }
70
71 #[must_use]
73 pub fn into_results(self) -> Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>> {
74 self.results
75 }
76
77 pub fn outcomes(&self) -> impl Iterator<Item = (usize, &ArtifactCacheOutcome)> {
79 indexed_outcomes(&self.results)
80 }
81
82 pub fn failures(&self) -> impl Iterator<Item = ArtifactCacheBatchFailedEntry<'_, E>> {
84 indexed_failures(&self.results).map(|(index, failure)| ArtifactCacheBatchFailedEntry {
85 index,
86 failure,
87 entry_elapsed: self.entry_elapsed[index],
88 })
89 }
90
91 #[must_use]
93 pub const fn total(&self) -> Duration {
94 self.total
95 }
96
97 #[must_use]
99 pub fn is_success(&self) -> bool {
100 self.results.iter().all(Result::is_ok)
101 }
102
103 #[must_use]
105 pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
106 let mut metrics = ArtifactCacheBatchMetrics {
107 entries: self.results.len(),
108 total: self.total,
109 ..ArtifactCacheBatchMetrics::default()
110 };
111 for result in &self.results {
112 match result {
113 Ok(outcome) => {
114 metrics.succeeded += 1;
115 if outcome.is_reused() {
116 metrics.reused += 1;
117 } else {
118 metrics.built += 1;
119 }
120 metrics.successful_timings = metrics
121 .successful_timings
122 .saturating_add(outcome.record().timings());
123 }
124 Err(_) => metrics.failed += 1,
125 }
126 }
127 metrics
128 }
129}
130
131impl<'a, E> ArtifactCacheBatchFailedEntry<'a, E> {
132 #[must_use]
134 pub const fn index(&self) -> usize {
135 self.index
136 }
137
138 #[must_use]
140 pub const fn failure(&self) -> &'a ArtifactCacheBatchFailure<E> {
141 self.failure
142 }
143
144 #[must_use]
146 pub const fn entry_elapsed(&self) -> Duration {
147 self.entry_elapsed
148 }
149}
150
151impl<E> ArtifactCacheBatchFailure<E> {
152 #[must_use]
154 pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
155 match self {
156 Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
157 Self::Cache { .. } => None,
158 }
159 }
160}
161
162impl ArtifactCacheBatchMetrics {
163 #[must_use]
165 pub const fn entries(self) -> usize {
166 self.entries
167 }
168
169 #[must_use]
171 pub const fn succeeded(self) -> usize {
172 self.succeeded
173 }
174
175 #[must_use]
177 pub const fn failed(self) -> usize {
178 self.failed
179 }
180
181 #[must_use]
183 pub const fn built(self) -> usize {
184 self.built
185 }
186
187 #[must_use]
189 pub const fn reused(self) -> usize {
190 self.reused
191 }
192
193 #[must_use]
195 pub const fn successful_timings(self) -> ArtifactCacheTimings {
196 self.successful_timings
197 }
198
199 #[must_use]
201 pub const fn total(self) -> Duration {
202 self.total
203 }
204}
205
206impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
207 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 let metrics = self.metrics();
209 write!(
210 formatter,
211 "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
212 metrics.entries(),
213 metrics.succeeded(),
214 metrics.failed(),
215 metrics.built(),
216 metrics.reused(),
217 metrics.successful_timings(),
218 metrics.total(),
219 )
220 }
221}
222
223#[must_use]
234pub fn build_artifact_caches_batch<E, F>(
235 specs: &[ArtifactCacheSpec],
236 mut populate: F,
237) -> ArtifactCacheBatchReport<E>
238where
239 F: FnMut(usize, &ArtifactBuildTransaction) -> Result<(), E>,
240{
241 let started = Instant::now();
242 let mut results = Vec::with_capacity(specs.len());
243 let mut entry_elapsed = Vec::with_capacity(specs.len());
244 for (index, spec) in specs.iter().enumerate() {
245 let entry_started = Instant::now();
246 let result = match prepare_artifact_cache(spec) {
247 Ok(ArtifactCachePreparation::Reused(record)) => {
248 Ok(ArtifactCacheOutcome::Reused(record))
249 }
250 Ok(ArtifactCachePreparation::Build(transaction)) => {
251 if let Err(source) = populate(index, &transaction) {
252 let cleanup_error = transaction.abort().err().map(Box::new);
253 Err(ArtifactCacheBatchFailure::Build {
254 source: Box::new(source),
255 cleanup_error,
256 })
257 } else {
258 transaction
259 .commit()
260 .map_err(|source| ArtifactCacheBatchFailure::Cache {
261 source: Box::new(source),
262 })
263 }
264 }
265 Err(source) => Err(ArtifactCacheBatchFailure::Cache {
266 source: Box::new(source),
267 }),
268 };
269 results.push(result);
270 entry_elapsed.push(entry_started.elapsed());
271 }
272 ArtifactCacheBatchReport {
273 results,
274 entry_elapsed,
275 total: started.elapsed(),
276 }
277}
278
279impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
280 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 match self {
282 Self::Cache { source } => write!(formatter, "artifact cache failed: {source}"),
283 Self::Build {
284 source,
285 cleanup_error,
286 } => {
287 write!(formatter, "artifact builder failed: {source}")?;
288 if let Some(cleanup_error) = cleanup_error {
289 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
290 }
291 Ok(())
292 }
293 }
294 }
295}
296
297impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
298where
299 E: std::error::Error + 'static,
300{
301 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
302 match self {
303 Self::Cache { source } => Some(source.as_ref()),
304 Self::Build { source, .. } => Some(source.as_ref()),
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests;