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 entries.push(build_artifact_cache_entry(index, labeled, &mut populate));
498 }
499 Ok(ArtifactCacheBatchReport {
500 entries,
501 total: started.elapsed(),
502 })
503}
504
505fn build_artifact_cache_entry<E>(
506 index: usize,
507 labeled: &LabeledArtifactCacheSpec,
508 populate: &mut impl FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
509) -> ArtifactCacheBatchEntry<E> {
510 let entry_started = Instant::now();
511 let mut timings = ArtifactCacheBatchFailureTimings::default();
512 let mut result = (|| {
513 let preparation_started = Instant::now();
514 let preparation = prepare_artifact_cache(&labeled.spec);
515 timings.preparation = preparation_started.elapsed();
516 let transaction = match preparation.map_err(|source| ArtifactCacheBatchFailure::Cache {
517 phase: ArtifactCacheBatchFailurePhase::Preparation,
518 source: Box::new(source),
519 timings,
520 })? {
521 ArtifactCachePreparation::Reused(record) => {
522 return Ok(ArtifactCacheOutcome::Reused(record));
523 }
524 ArtifactCachePreparation::Build(transaction) => transaction,
525 };
526
527 let callback_started = Instant::now();
528 let callback_result = populate(&labeled.label, &transaction);
529 timings.callback = Some(callback_started.elapsed());
530 if let Err(source) = callback_result {
531 let cleanup_started = Instant::now();
532 let cleanup_error = transaction.abort().err().map(Box::new);
533 timings.cleanup = Some(cleanup_started.elapsed());
534 return Err(ArtifactCacheBatchFailure::Build {
535 source: Box::new(source),
536 cleanup_error,
537 timings,
538 });
539 }
540
541 let commit_started = Instant::now();
542 let committed = transaction.commit();
543 timings.commit = Some(commit_started.elapsed());
544 committed.map_err(|source| ArtifactCacheBatchFailure::Cache {
545 phase: ArtifactCacheBatchFailurePhase::Commit,
546 source: Box::new(source),
547 timings,
548 })
549 })();
550 let entry_elapsed = entry_started.elapsed();
551 if let Err(
552 ArtifactCacheBatchFailure::Cache { timings, .. }
553 | ArtifactCacheBatchFailure::Build { timings, .. },
554 ) = &mut result
555 {
556 timings.total = entry_elapsed;
557 }
558 ArtifactCacheBatchEntry {
559 index,
560 label: labeled.label.clone(),
561 result,
562 entry_elapsed,
563 }
564}
565
566fn validate_batch_labels(
567 specs: &[LabeledArtifactCacheSpec],
568) -> Result<(), ArtifactCacheBatchContractError> {
569 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
570 {
571 BatchLabelError::Empty { index } => ArtifactCacheBatchContractError::EmptyLabel { index },
572 BatchLabelError::Duplicate {
573 label,
574 first_index,
575 duplicate_index,
576 } => ArtifactCacheBatchContractError::DuplicateLabel {
577 label,
578 first_index,
579 duplicate_index,
580 },
581 })
582}
583
584impl std::fmt::Display for ArtifactCacheBatchContractError {
585 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 match self {
587 Self::EmptyLabel { index } => {
588 write!(formatter, "artifact batch label at index {index} is empty")
589 }
590 Self::DuplicateLabel {
591 label,
592 first_index,
593 duplicate_index,
594 } => write!(
595 formatter,
596 "artifact batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
597 ),
598 }
599 }
600}
601
602impl std::error::Error for ArtifactCacheBatchContractError {}
603
604impl std::fmt::Display for ArtifactCacheBatchFailurePhase {
605 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 formatter.write_str(match self {
607 Self::Preparation => "preparation",
608 Self::Callback => "callback",
609 Self::Commit => "commit",
610 })
611 }
612}
613
614impl std::fmt::Display for ArtifactCacheBatchFailureTimings {
615 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616 write!(
617 formatter,
618 "total={:?} preparation={:?} callback={:?} cleanup={:?} commit={:?}",
619 self.total, self.preparation, self.callback, self.cleanup, self.commit,
620 )
621 }
622}
623
624impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
625 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626 match self {
627 Self::Cache {
628 phase,
629 source,
630 timings,
631 } => write!(
632 formatter,
633 "artifact cache {phase} failed: {source}; timings=({timings})",
634 ),
635 Self::Build {
636 source,
637 cleanup_error,
638 timings,
639 } => {
640 write!(
641 formatter,
642 "artifact callback failed: {source}; timings=({timings})"
643 )?;
644 if let Some(cleanup_error) = cleanup_error {
645 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
646 }
647 Ok(())
648 }
649 }
650 }
651}
652
653impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
654where
655 E: std::error::Error + 'static,
656{
657 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
658 match self {
659 Self::Cache { source, .. } => Some(source.as_ref()),
660 Self::Build { source, .. } => Some(source.as_ref()),
661 }
662 }
663}
664
665#[cfg(test)]
666mod tests;