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)]
38pub struct ArtifactCacheBatchOutcomeEntry<'a> {
39 index: usize,
40 label: &'a str,
41 outcome: &'a ArtifactCacheOutcome,
42 entry_elapsed: Duration,
43}
44
45#[derive(Debug)]
47pub struct ArtifactCacheBatchFailedEntry<'a, E> {
48 index: usize,
49 label: &'a str,
50 failure: &'a ArtifactCacheBatchFailure<E>,
51 entry_elapsed: Duration,
52}
53
54#[non_exhaustive]
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum ArtifactCacheBatchContractError {
58 EmptyLabel {
60 index: usize,
62 },
63 DuplicateLabel {
65 label: String,
67 first_index: usize,
69 duplicate_index: usize,
71 },
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum ArtifactCacheBatchFailurePhase {
77 Preparation,
79 Callback,
81 Commit,
83}
84
85#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
87pub struct ArtifactCacheBatchFailureTimings {
88 preparation: Duration,
89 callback: Option<Duration>,
90 cleanup: Option<Duration>,
91 commit: Option<Duration>,
92 total: Duration,
93}
94
95#[derive(Debug)]
97pub enum ArtifactCacheBatchFailure<E> {
98 Cache {
100 phase: ArtifactCacheBatchFailurePhase,
102 source: Box<ArtifactCacheError>,
104 timings: ArtifactCacheBatchFailureTimings,
106 },
107 Build {
109 source: Box<E>,
111 cleanup_error: Option<Box<ArtifactCacheError>>,
113 timings: ArtifactCacheBatchFailureTimings,
115 },
116}
117
118#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
120pub struct ArtifactCacheBatchMetrics {
121 entries: usize,
122 built: usize,
123 reused: usize,
124 successful_timings: ArtifactCacheTimings,
125 total: Duration,
126}
127
128impl LabeledArtifactCacheSpec {
129 #[must_use]
131 pub fn new(label: impl Into<String>, spec: ArtifactCacheSpec) -> Self {
132 Self {
133 label: label.into(),
134 spec,
135 }
136 }
137
138 #[must_use]
140 pub fn label(&self) -> &str {
141 &self.label
142 }
143
144 #[must_use]
146 pub const fn spec(&self) -> &ArtifactCacheSpec {
147 &self.spec
148 }
149
150 #[must_use]
152 pub fn into_parts(self) -> (String, ArtifactCacheSpec) {
153 (self.label, self.spec)
154 }
155}
156
157impl<E> ArtifactCacheBatchReport<E> {
158 #[must_use]
160 pub fn entries(&self) -> &[ArtifactCacheBatchEntry<E>] {
161 &self.entries
162 }
163
164 #[must_use]
166 pub fn into_entries(self) -> Vec<ArtifactCacheBatchEntry<E>> {
167 self.entries
168 }
169
170 pub fn outcomes(&self) -> impl Iterator<Item = ArtifactCacheBatchOutcomeEntry<'_>> {
172 self.entries.iter().filter_map(|entry| {
173 entry
174 .outcome()
175 .map(|outcome| ArtifactCacheBatchOutcomeEntry {
176 index: entry.index,
177 label: &entry.label,
178 outcome,
179 entry_elapsed: entry.entry_elapsed,
180 })
181 })
182 }
183
184 pub fn failures(&self) -> impl Iterator<Item = ArtifactCacheBatchFailedEntry<'_, E>> {
186 self.entries.iter().filter_map(|entry| {
187 entry
188 .failure()
189 .map(|failure| ArtifactCacheBatchFailedEntry {
190 index: entry.index,
191 label: &entry.label,
192 failure,
193 entry_elapsed: entry.entry_elapsed,
194 })
195 })
196 }
197
198 #[must_use]
200 pub const fn total(&self) -> Duration {
201 self.total
202 }
203
204 #[must_use]
206 pub fn is_success(&self) -> bool {
207 self.entries.iter().all(ArtifactCacheBatchEntry::is_success)
208 }
209
210 #[must_use]
212 pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
213 let mut metrics = ArtifactCacheBatchMetrics {
214 entries: self.entries.len(),
215 total: self.total,
216 ..ArtifactCacheBatchMetrics::default()
217 };
218 for entry in self.outcomes() {
219 let outcome = entry.outcome();
220 if outcome.is_reused() {
221 metrics.reused += 1;
222 } else {
223 metrics.built += 1;
224 }
225 metrics.successful_timings = metrics
226 .successful_timings
227 .saturating_add(outcome.record().timings());
228 }
229 metrics
230 }
231}
232
233impl<E> ArtifactCacheBatchEntry<E> {
234 #[must_use]
236 pub const fn index(&self) -> usize {
237 self.index
238 }
239
240 #[must_use]
242 pub fn label(&self) -> &str {
243 &self.label
244 }
245
246 pub const fn result(&self) -> Result<&ArtifactCacheOutcome, &ArtifactCacheBatchFailure<E>> {
251 self.result.as_ref()
252 }
253
254 #[must_use]
256 pub fn outcome(&self) -> Option<&ArtifactCacheOutcome> {
257 self.result.as_ref().ok()
258 }
259
260 #[must_use]
262 pub fn failure(&self) -> Option<&ArtifactCacheBatchFailure<E>> {
263 self.result.as_ref().err()
264 }
265
266 #[must_use]
268 pub const fn entry_elapsed(&self) -> Duration {
269 self.entry_elapsed
270 }
271
272 #[must_use]
274 pub const fn is_success(&self) -> bool {
275 self.result.is_ok()
276 }
277
278 pub fn into_parts(
280 self,
281 ) -> (
282 usize,
283 String,
284 Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
285 Duration,
286 ) {
287 (self.index, self.label, self.result, self.entry_elapsed)
288 }
289}
290
291impl<'a> ArtifactCacheBatchOutcomeEntry<'a> {
292 #[must_use]
294 pub const fn index(self) -> usize {
295 self.index
296 }
297
298 #[must_use]
300 pub const fn label(self) -> &'a str {
301 self.label
302 }
303
304 #[must_use]
306 pub const fn outcome(self) -> &'a ArtifactCacheOutcome {
307 self.outcome
308 }
309
310 #[must_use]
312 pub const fn entry_elapsed(self) -> Duration {
313 self.entry_elapsed
314 }
315}
316
317impl<'a, E> ArtifactCacheBatchFailedEntry<'a, E> {
318 #[must_use]
320 pub const fn index(&self) -> usize {
321 self.index
322 }
323
324 #[must_use]
326 pub const fn label(&self) -> &'a str {
327 self.label
328 }
329
330 #[must_use]
332 pub const fn failure(&self) -> &'a ArtifactCacheBatchFailure<E> {
333 self.failure
334 }
335
336 #[must_use]
338 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
339 self.failure.timings()
340 }
341
342 #[must_use]
344 pub const fn entry_elapsed(&self) -> Duration {
345 self.entry_elapsed
346 }
347}
348
349impl<E> ArtifactCacheBatchFailure<E> {
350 #[must_use]
352 pub const fn phase(&self) -> ArtifactCacheBatchFailurePhase {
353 match self {
354 Self::Cache { phase, .. } => *phase,
355 Self::Build { .. } => ArtifactCacheBatchFailurePhase::Callback,
356 }
357 }
358
359 #[must_use]
361 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
362 match self {
363 Self::Cache { timings, .. } | Self::Build { timings, .. } => *timings,
364 }
365 }
366
367 #[must_use]
369 pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
370 match self {
371 Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
372 Self::Cache { .. } => None,
373 }
374 }
375}
376
377impl ArtifactCacheBatchFailureTimings {
378 #[must_use]
380 pub const fn preparation(self) -> Duration {
381 self.preparation
382 }
383
384 #[must_use]
386 pub const fn callback(self) -> Option<Duration> {
387 self.callback
388 }
389
390 #[must_use]
392 pub const fn cleanup(self) -> Option<Duration> {
393 self.cleanup
394 }
395
396 #[must_use]
398 pub const fn commit(self) -> Option<Duration> {
399 self.commit
400 }
401
402 #[must_use]
404 pub const fn total(self) -> Duration {
405 self.total
406 }
407}
408
409impl ArtifactCacheBatchMetrics {
410 #[must_use]
412 pub const fn entries(self) -> usize {
413 self.entries
414 }
415
416 #[must_use]
418 pub const fn succeeded(self) -> usize {
419 self.built + self.reused
420 }
421
422 #[must_use]
424 pub const fn failed(self) -> usize {
425 self.entries - self.succeeded()
426 }
427
428 #[must_use]
430 pub const fn built(self) -> usize {
431 self.built
432 }
433
434 #[must_use]
436 pub const fn reused(self) -> usize {
437 self.reused
438 }
439
440 #[must_use]
442 pub const fn successful_timings(self) -> ArtifactCacheTimings {
443 self.successful_timings
444 }
445
446 #[must_use]
448 pub const fn total(self) -> Duration {
449 self.total
450 }
451}
452
453impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
454 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 let metrics = self.metrics();
456 write!(
457 formatter,
458 "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
459 metrics.entries(),
460 metrics.succeeded(),
461 metrics.failed(),
462 metrics.built(),
463 metrics.reused(),
464 metrics.successful_timings(),
465 metrics.total(),
466 )
467 }
468}
469
470pub fn build_artifact_caches_batch<E, F>(
487 specs: &[LabeledArtifactCacheSpec],
488 mut populate: F,
489) -> Result<ArtifactCacheBatchReport<E>, ArtifactCacheBatchContractError>
490where
491 F: FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
492{
493 validate_batch_labels(specs)?;
494 let started = Instant::now();
495 let mut entries = Vec::with_capacity(specs.len());
496 for (index, labeled) in specs.iter().enumerate() {
497 let entry_started = Instant::now();
498 let preparation_started = Instant::now();
499 let preparation = prepare_artifact_cache(&labeled.spec);
500 let preparation_elapsed = preparation_started.elapsed();
501 let (result, entry_elapsed) = match preparation {
502 Ok(ArtifactCachePreparation::Reused(record)) => (
503 Ok(ArtifactCacheOutcome::Reused(record)),
504 entry_started.elapsed(),
505 ),
506 Ok(ArtifactCachePreparation::Build(transaction)) => {
507 let callback_started = Instant::now();
508 let callback_result = populate(&labeled.label, &transaction);
509 let callback_elapsed = callback_started.elapsed();
510 if let Err(source) = callback_result {
511 let cleanup_started = Instant::now();
512 let cleanup_error = transaction.abort().err().map(Box::new);
513 let cleanup_elapsed = cleanup_started.elapsed();
514 let entry_elapsed = entry_started.elapsed();
515 (
516 Err(ArtifactCacheBatchFailure::Build {
517 source: Box::new(source),
518 cleanup_error,
519 timings: ArtifactCacheBatchFailureTimings {
520 preparation: preparation_elapsed,
521 callback: Some(callback_elapsed),
522 cleanup: Some(cleanup_elapsed),
523 commit: None,
524 total: entry_elapsed,
525 },
526 }),
527 entry_elapsed,
528 )
529 } else {
530 let commit_started = Instant::now();
531 match transaction.commit() {
532 Ok(outcome) => (Ok(outcome), entry_started.elapsed()),
533 Err(source) => {
534 let commit_elapsed = commit_started.elapsed();
535 let entry_elapsed = entry_started.elapsed();
536 (
537 Err(ArtifactCacheBatchFailure::Cache {
538 phase: ArtifactCacheBatchFailurePhase::Commit,
539 source: Box::new(source),
540 timings: ArtifactCacheBatchFailureTimings {
541 preparation: preparation_elapsed,
542 callback: Some(callback_elapsed),
543 cleanup: None,
544 commit: Some(commit_elapsed),
545 total: entry_elapsed,
546 },
547 }),
548 entry_elapsed,
549 )
550 }
551 }
552 }
553 }
554 Err(source) => {
555 let entry_elapsed = entry_started.elapsed();
556 (
557 Err(ArtifactCacheBatchFailure::Cache {
558 phase: ArtifactCacheBatchFailurePhase::Preparation,
559 source: Box::new(source),
560 timings: ArtifactCacheBatchFailureTimings {
561 preparation: preparation_elapsed,
562 callback: None,
563 cleanup: None,
564 commit: None,
565 total: entry_elapsed,
566 },
567 }),
568 entry_elapsed,
569 )
570 }
571 };
572 entries.push(ArtifactCacheBatchEntry {
573 index,
574 label: labeled.label.clone(),
575 result,
576 entry_elapsed,
577 });
578 }
579 Ok(ArtifactCacheBatchReport {
580 entries,
581 total: started.elapsed(),
582 })
583}
584
585fn validate_batch_labels(
586 specs: &[LabeledArtifactCacheSpec],
587) -> Result<(), ArtifactCacheBatchContractError> {
588 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
589 {
590 BatchLabelError::Empty { index } => ArtifactCacheBatchContractError::EmptyLabel { index },
591 BatchLabelError::Duplicate {
592 label,
593 first_index,
594 duplicate_index,
595 } => ArtifactCacheBatchContractError::DuplicateLabel {
596 label,
597 first_index,
598 duplicate_index,
599 },
600 })
601}
602
603impl std::fmt::Display for ArtifactCacheBatchContractError {
604 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
605 match self {
606 Self::EmptyLabel { index } => {
607 write!(formatter, "artifact batch label at index {index} is empty")
608 }
609 Self::DuplicateLabel {
610 label,
611 first_index,
612 duplicate_index,
613 } => write!(
614 formatter,
615 "artifact batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
616 ),
617 }
618 }
619}
620
621impl std::error::Error for ArtifactCacheBatchContractError {}
622
623impl std::fmt::Display for ArtifactCacheBatchFailurePhase {
624 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625 formatter.write_str(match self {
626 Self::Preparation => "preparation",
627 Self::Callback => "callback",
628 Self::Commit => "commit",
629 })
630 }
631}
632
633impl std::fmt::Display for ArtifactCacheBatchFailureTimings {
634 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635 write!(
636 formatter,
637 "total={:?} preparation={:?} callback={:?} cleanup={:?} commit={:?}",
638 self.total, self.preparation, self.callback, self.cleanup, self.commit,
639 )
640 }
641}
642
643impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
644 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645 match self {
646 Self::Cache {
647 phase,
648 source,
649 timings,
650 } => write!(
651 formatter,
652 "artifact cache {phase} failed: {source}; timings=({timings})",
653 ),
654 Self::Build {
655 source,
656 cleanup_error,
657 timings,
658 } => {
659 write!(
660 formatter,
661 "artifact callback failed: {source}; timings=({timings})"
662 )?;
663 if let Some(cleanup_error) = cleanup_error {
664 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
665 }
666 Ok(())
667 }
668 }
669 }
670}
671
672impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
673where
674 E: std::error::Error + 'static,
675{
676 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
677 match self {
678 Self::Cache { source, .. } => Some(source.as_ref()),
679 Self::Build { source, .. } => Some(source.as_ref()),
680 }
681 }
682}
683
684#[cfg(test)]
685mod tests;