1use crate::batch::{BatchLabelError, validate_labels};
2
3use std::time::{Duration, Instant};
4
5use super::transaction::{
6 ArtifactBuildTransaction, ArtifactCacheError, ArtifactCacheOutcome, ArtifactCachePreparation,
7 ArtifactCacheSpec, ArtifactCacheTimings, prepare_artifact_cache,
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct LabeledArtifactCacheSpec {
16 label: String,
17 spec: ArtifactCacheSpec,
18}
19
20#[derive(Debug)]
22pub struct ArtifactCacheBatchReport<E> {
23 entries: Vec<ArtifactCacheBatchEntry<E>>,
24 total: Duration,
25}
26
27#[derive(Debug)]
29pub struct ArtifactCacheBatchEntry<E> {
30 index: usize,
31 label: String,
32 result: Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
33 entry_elapsed: Duration,
34}
35
36#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
38pub struct ArtifactCacheBatchFailureTimings {
39 preparation: Duration,
40 callback: Option<Duration>,
41 cleanup: Option<Duration>,
42 commit: Option<Duration>,
43 total: Duration,
44}
45
46#[derive(Debug)]
48pub enum ArtifactCacheBatchFailure<E> {
49 Preparation {
51 source: Box<ArtifactCacheError>,
53 timings: ArtifactCacheBatchFailureTimings,
55 },
56 Commit {
58 source: Box<ArtifactCacheError>,
60 timings: ArtifactCacheBatchFailureTimings,
62 },
63 Callback {
65 source: Box<E>,
67 cleanup_error: Option<Box<ArtifactCacheError>>,
69 timings: ArtifactCacheBatchFailureTimings,
71 },
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub struct ArtifactCacheBatchMetrics {
77 entries: usize,
78 built: usize,
79 reused: usize,
80 successful_timings: ArtifactCacheTimings,
81 total: Duration,
82}
83
84impl LabeledArtifactCacheSpec {
85 #[must_use]
87 pub fn new(label: impl Into<String>, spec: ArtifactCacheSpec) -> Self {
88 Self {
89 label: label.into(),
90 spec,
91 }
92 }
93
94 #[must_use]
96 pub fn label(&self) -> &str {
97 &self.label
98 }
99
100 #[must_use]
102 pub const fn spec(&self) -> &ArtifactCacheSpec {
103 &self.spec
104 }
105
106 #[must_use]
108 pub fn into_parts(self) -> (String, ArtifactCacheSpec) {
109 (self.label, self.spec)
110 }
111}
112
113impl<E> ArtifactCacheBatchReport<E> {
114 #[must_use]
116 pub fn entries(&self) -> &[ArtifactCacheBatchEntry<E>] {
117 &self.entries
118 }
119
120 #[must_use]
122 pub fn into_entries(self) -> Vec<ArtifactCacheBatchEntry<E>> {
123 self.entries
124 }
125
126 pub fn outcomes(&self) -> impl Iterator<Item = &ArtifactCacheBatchEntry<E>> {
128 self.entries.iter().filter(|entry| entry.is_success())
129 }
130
131 pub fn failures(&self) -> impl Iterator<Item = &ArtifactCacheBatchEntry<E>> {
133 self.entries.iter().filter(|entry| !entry.is_success())
134 }
135
136 #[must_use]
138 pub const fn total(&self) -> Duration {
139 self.total
140 }
141
142 #[must_use]
144 pub fn is_success(&self) -> bool {
145 self.entries.iter().all(ArtifactCacheBatchEntry::is_success)
146 }
147
148 #[must_use]
150 pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
151 let mut metrics = ArtifactCacheBatchMetrics {
152 entries: self.entries.len(),
153 total: self.total,
154 ..ArtifactCacheBatchMetrics::default()
155 };
156 for outcome in self
157 .entries
158 .iter()
159 .filter_map(ArtifactCacheBatchEntry::outcome)
160 {
161 if outcome.is_reused() {
162 metrics.reused += 1;
163 } else {
164 metrics.built += 1;
165 }
166 metrics.successful_timings = metrics
167 .successful_timings
168 .saturating_add(outcome.record().timings());
169 }
170 metrics
171 }
172}
173
174impl<E> ArtifactCacheBatchEntry<E> {
175 #[must_use]
177 pub const fn index(&self) -> usize {
178 self.index
179 }
180
181 #[must_use]
183 pub fn label(&self) -> &str {
184 &self.label
185 }
186
187 pub const fn result(&self) -> Result<&ArtifactCacheOutcome, &ArtifactCacheBatchFailure<E>> {
192 self.result.as_ref()
193 }
194
195 #[must_use]
197 pub fn outcome(&self) -> Option<&ArtifactCacheOutcome> {
198 self.result.as_ref().ok()
199 }
200
201 #[must_use]
203 pub fn failure(&self) -> Option<&ArtifactCacheBatchFailure<E>> {
204 self.result.as_ref().err()
205 }
206
207 #[must_use]
209 pub const fn entry_elapsed(&self) -> Duration {
210 self.entry_elapsed
211 }
212
213 #[must_use]
215 pub const fn is_success(&self) -> bool {
216 self.result.is_ok()
217 }
218
219 pub fn into_parts(
221 self,
222 ) -> (
223 usize,
224 String,
225 Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
226 Duration,
227 ) {
228 (self.index, self.label, self.result, self.entry_elapsed)
229 }
230}
231
232impl<E> ArtifactCacheBatchFailure<E> {
233 #[must_use]
235 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
236 match self {
237 Self::Preparation { timings, .. }
238 | Self::Commit { timings, .. }
239 | Self::Callback { timings, .. } => *timings,
240 }
241 }
242
243 #[must_use]
245 pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
246 match self {
247 Self::Callback { cleanup_error, .. } => cleanup_error.as_deref(),
248 Self::Preparation { .. } | Self::Commit { .. } => None,
249 }
250 }
251}
252
253impl ArtifactCacheBatchFailureTimings {
254 #[must_use]
256 pub const fn preparation(self) -> Duration {
257 self.preparation
258 }
259
260 #[must_use]
262 pub const fn callback(self) -> Option<Duration> {
263 self.callback
264 }
265
266 #[must_use]
268 pub const fn cleanup(self) -> Option<Duration> {
269 self.cleanup
270 }
271
272 #[must_use]
274 pub const fn commit(self) -> Option<Duration> {
275 self.commit
276 }
277
278 #[must_use]
280 pub const fn total(self) -> Duration {
281 self.total
282 }
283}
284
285impl ArtifactCacheBatchMetrics {
286 #[must_use]
288 pub const fn entries(self) -> usize {
289 self.entries
290 }
291
292 #[must_use]
294 pub const fn succeeded(self) -> usize {
295 self.built + self.reused
296 }
297
298 #[must_use]
300 pub const fn failed(self) -> usize {
301 self.entries - self.succeeded()
302 }
303
304 #[must_use]
306 pub const fn built(self) -> usize {
307 self.built
308 }
309
310 #[must_use]
312 pub const fn reused(self) -> usize {
313 self.reused
314 }
315
316 #[must_use]
318 pub const fn successful_timings(self) -> ArtifactCacheTimings {
319 self.successful_timings
320 }
321
322 #[must_use]
324 pub const fn total(self) -> Duration {
325 self.total
326 }
327}
328
329impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
330 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 let metrics = self.metrics();
332 write!(
333 formatter,
334 "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
335 metrics.entries(),
336 metrics.succeeded(),
337 metrics.failed(),
338 metrics.built(),
339 metrics.reused(),
340 metrics.successful_timings(),
341 metrics.total(),
342 )
343 }
344}
345
346pub fn build_artifact_caches_batch<E, F>(
363 specs: &[LabeledArtifactCacheSpec],
364 mut populate: F,
365) -> Result<ArtifactCacheBatchReport<E>, BatchLabelError>
366where
367 F: FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
368{
369 validate_labels(specs.iter().map(LabeledArtifactCacheSpec::label))?;
370 let started = Instant::now();
371 let mut entries = Vec::with_capacity(specs.len());
372 for (index, labeled) in specs.iter().enumerate() {
373 entries.push(build_artifact_cache_entry(index, labeled, &mut populate));
374 }
375 Ok(ArtifactCacheBatchReport {
376 entries,
377 total: started.elapsed(),
378 })
379}
380
381fn build_artifact_cache_entry<E>(
382 index: usize,
383 labeled: &LabeledArtifactCacheSpec,
384 populate: &mut impl FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
385) -> ArtifactCacheBatchEntry<E> {
386 let entry_started = Instant::now();
387 let mut timings = ArtifactCacheBatchFailureTimings::default();
388 let mut result = (|| {
389 let preparation_started = Instant::now();
390 let preparation = prepare_artifact_cache(&labeled.spec);
391 timings.preparation = preparation_started.elapsed();
392 let transaction =
393 match preparation.map_err(|source| ArtifactCacheBatchFailure::Preparation {
394 source: Box::new(source),
395 timings,
396 })? {
397 ArtifactCachePreparation::Reused(record) => {
398 return Ok(ArtifactCacheOutcome::Reused(record));
399 }
400 ArtifactCachePreparation::Build(transaction) => transaction,
401 };
402
403 let callback_started = Instant::now();
404 let callback_result = populate(&labeled.label, &transaction);
405 timings.callback = Some(callback_started.elapsed());
406 if let Err(source) = callback_result {
407 let cleanup_started = Instant::now();
408 let cleanup_error = transaction.abort().err().map(Box::new);
409 timings.cleanup = Some(cleanup_started.elapsed());
410 return Err(ArtifactCacheBatchFailure::Callback {
411 source: Box::new(source),
412 cleanup_error,
413 timings,
414 });
415 }
416
417 let commit_started = Instant::now();
418 let committed = transaction.commit();
419 timings.commit = Some(commit_started.elapsed());
420 committed.map_err(|source| ArtifactCacheBatchFailure::Commit {
421 source: Box::new(source),
422 timings,
423 })
424 })();
425 let entry_elapsed = entry_started.elapsed();
426 if let Err(
427 ArtifactCacheBatchFailure::Preparation { timings, .. }
428 | ArtifactCacheBatchFailure::Commit { timings, .. }
429 | ArtifactCacheBatchFailure::Callback { timings, .. },
430 ) = &mut result
431 {
432 timings.total = entry_elapsed;
433 }
434 ArtifactCacheBatchEntry {
435 index,
436 label: labeled.label.clone(),
437 result,
438 entry_elapsed,
439 }
440}
441
442impl std::fmt::Display for ArtifactCacheBatchFailureTimings {
443 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 write!(
445 formatter,
446 "total={:?} preparation={:?} callback={:?} cleanup={:?} commit={:?}",
447 self.total, self.preparation, self.callback, self.cleanup, self.commit,
448 )
449 }
450}
451
452impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
453 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 match self {
455 Self::Preparation { source, timings } => write!(
456 formatter,
457 "artifact cache preparation failed: {source}; timings=({timings})",
458 ),
459 Self::Commit { source, timings } => write!(
460 formatter,
461 "artifact cache commit failed: {source}; timings=({timings})",
462 ),
463 Self::Callback {
464 source,
465 cleanup_error,
466 timings,
467 } => {
468 write!(
469 formatter,
470 "artifact callback failed: {source}; timings=({timings})"
471 )?;
472 if let Some(cleanup_error) = cleanup_error {
473 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
474 }
475 Ok(())
476 }
477 }
478 }
479}
480
481impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
482where
483 E: std::error::Error + 'static,
484{
485 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
486 match self {
487 Self::Preparation { source, .. } | Self::Commit { source, .. } => Some(source.as_ref()),
488 Self::Callback { source, .. } => Some(source.as_ref()),
489 }
490 }
491}
492
493#[cfg(test)]
494mod tests;