1use std::{
2 collections::HashMap,
3 time::{Duration, Instant},
4};
5
6use super::transaction::{
7 ArtifactBuildTransaction, ArtifactCacheError, ArtifactCacheOutcome, ArtifactCachePreparation,
8 ArtifactCacheSpec, ArtifactCacheTimings, prepare_artifact_cache,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct LabeledArtifactCacheSpec {
17 label: String,
18 spec: ArtifactCacheSpec,
19}
20
21#[derive(Debug)]
23pub struct ArtifactCacheBatchReport<E> {
24 entries: Vec<ArtifactCacheBatchEntry<E>>,
25 total: Duration,
26}
27
28#[derive(Debug)]
30pub struct ArtifactCacheBatchEntry<E> {
31 index: usize,
32 label: String,
33 result: Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
34 entry_elapsed: Duration,
35}
36
37#[derive(Clone, Copy, Debug)]
39pub struct ArtifactCacheBatchOutcomeEntry<'a> {
40 index: usize,
41 label: &'a str,
42 outcome: &'a ArtifactCacheOutcome,
43 entry_elapsed: Duration,
44}
45
46#[derive(Debug)]
48pub struct ArtifactCacheBatchFailedEntry<'a, E> {
49 index: usize,
50 label: &'a str,
51 failure: &'a ArtifactCacheBatchFailure<E>,
52 entry_elapsed: Duration,
53}
54
55#[non_exhaustive]
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub enum ArtifactCacheBatchContractError {
59 EmptyLabel {
61 index: usize,
63 },
64 DuplicateLabel {
66 label: String,
68 first_index: usize,
70 duplicate_index: usize,
72 },
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub enum ArtifactCacheBatchFailurePhase {
78 Preparation,
80 Callback,
82 Commit,
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
88pub struct ArtifactCacheBatchFailureTimings {
89 preparation: Duration,
90 callback: Option<Duration>,
91 cleanup: Option<Duration>,
92 commit: Option<Duration>,
93 total: Duration,
94}
95
96#[derive(Debug)]
98pub enum ArtifactCacheBatchFailure<E> {
99 Cache {
101 phase: ArtifactCacheBatchFailurePhase,
103 source: Box<ArtifactCacheError>,
105 timings: ArtifactCacheBatchFailureTimings,
107 },
108 Build {
110 source: Box<E>,
112 cleanup_error: Option<Box<ArtifactCacheError>>,
114 timings: ArtifactCacheBatchFailureTimings,
116 },
117}
118
119#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
121pub struct ArtifactCacheBatchMetrics {
122 entries: usize,
123 succeeded: usize,
124 failed: usize,
125 built: usize,
126 reused: usize,
127 successful_timings: ArtifactCacheTimings,
128 total: Duration,
129}
130
131impl LabeledArtifactCacheSpec {
132 #[must_use]
134 pub fn new(label: impl Into<String>, spec: ArtifactCacheSpec) -> Self {
135 Self {
136 label: label.into(),
137 spec,
138 }
139 }
140
141 #[must_use]
143 pub fn label(&self) -> &str {
144 &self.label
145 }
146
147 #[must_use]
149 pub const fn spec(&self) -> &ArtifactCacheSpec {
150 &self.spec
151 }
152
153 #[must_use]
155 pub fn into_parts(self) -> (String, ArtifactCacheSpec) {
156 (self.label, self.spec)
157 }
158}
159
160impl<E> ArtifactCacheBatchReport<E> {
161 #[must_use]
163 pub fn entries(&self) -> &[ArtifactCacheBatchEntry<E>] {
164 &self.entries
165 }
166
167 #[must_use]
169 pub fn into_entries(self) -> Vec<ArtifactCacheBatchEntry<E>> {
170 self.entries
171 }
172
173 pub fn outcomes(&self) -> impl Iterator<Item = ArtifactCacheBatchOutcomeEntry<'_>> {
175 self.entries.iter().filter_map(|entry| {
176 entry
177 .outcome()
178 .map(|outcome| ArtifactCacheBatchOutcomeEntry {
179 index: entry.index,
180 label: &entry.label,
181 outcome,
182 entry_elapsed: entry.entry_elapsed,
183 })
184 })
185 }
186
187 pub fn failures(&self) -> impl Iterator<Item = ArtifactCacheBatchFailedEntry<'_, E>> {
189 self.entries.iter().filter_map(|entry| {
190 entry
191 .failure()
192 .map(|failure| ArtifactCacheBatchFailedEntry {
193 index: entry.index,
194 label: &entry.label,
195 failure,
196 entry_elapsed: entry.entry_elapsed,
197 })
198 })
199 }
200
201 #[must_use]
203 pub const fn total(&self) -> Duration {
204 self.total
205 }
206
207 #[must_use]
209 pub fn is_success(&self) -> bool {
210 self.entries.iter().all(ArtifactCacheBatchEntry::is_success)
211 }
212
213 #[must_use]
215 pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
216 let mut metrics = ArtifactCacheBatchMetrics {
217 entries: self.entries.len(),
218 total: self.total,
219 ..ArtifactCacheBatchMetrics::default()
220 };
221 for entry in &self.entries {
222 match &entry.result {
223 Ok(outcome) => {
224 metrics.succeeded += 1;
225 if outcome.is_reused() {
226 metrics.reused += 1;
227 } else {
228 metrics.built += 1;
229 }
230 metrics.successful_timings = metrics
231 .successful_timings
232 .saturating_add(outcome.record().timings());
233 }
234 Err(_) => metrics.failed += 1,
235 }
236 }
237 metrics
238 }
239}
240
241impl<E> ArtifactCacheBatchEntry<E> {
242 #[must_use]
244 pub const fn index(&self) -> usize {
245 self.index
246 }
247
248 #[must_use]
250 pub fn label(&self) -> &str {
251 &self.label
252 }
253
254 pub const fn result(&self) -> Result<&ArtifactCacheOutcome, &ArtifactCacheBatchFailure<E>> {
256 self.result.as_ref()
257 }
258
259 #[must_use]
261 pub fn outcome(&self) -> Option<&ArtifactCacheOutcome> {
262 self.result.as_ref().ok()
263 }
264
265 #[must_use]
267 pub fn failure(&self) -> Option<&ArtifactCacheBatchFailure<E>> {
268 self.result.as_ref().err()
269 }
270
271 #[must_use]
273 pub const fn entry_elapsed(&self) -> Duration {
274 self.entry_elapsed
275 }
276
277 #[must_use]
279 pub const fn is_success(&self) -> bool {
280 self.result.is_ok()
281 }
282
283 pub fn into_parts(
285 self,
286 ) -> (
287 usize,
288 String,
289 Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
290 Duration,
291 ) {
292 (self.index, self.label, self.result, self.entry_elapsed)
293 }
294}
295
296impl<'a> ArtifactCacheBatchOutcomeEntry<'a> {
297 #[must_use]
299 pub const fn index(self) -> usize {
300 self.index
301 }
302
303 #[must_use]
305 pub const fn label(self) -> &'a str {
306 self.label
307 }
308
309 #[must_use]
311 pub const fn outcome(self) -> &'a ArtifactCacheOutcome {
312 self.outcome
313 }
314
315 #[must_use]
317 pub const fn entry_elapsed(self) -> Duration {
318 self.entry_elapsed
319 }
320}
321
322impl<'a, E> ArtifactCacheBatchFailedEntry<'a, E> {
323 #[must_use]
325 pub const fn index(&self) -> usize {
326 self.index
327 }
328
329 #[must_use]
331 pub const fn label(&self) -> &'a str {
332 self.label
333 }
334
335 #[must_use]
337 pub const fn failure(&self) -> &'a ArtifactCacheBatchFailure<E> {
338 self.failure
339 }
340
341 #[must_use]
343 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
344 self.failure.timings()
345 }
346
347 #[must_use]
349 pub const fn entry_elapsed(&self) -> Duration {
350 self.entry_elapsed
351 }
352}
353
354impl<E> ArtifactCacheBatchFailure<E> {
355 #[must_use]
357 pub const fn phase(&self) -> ArtifactCacheBatchFailurePhase {
358 match self {
359 Self::Cache { phase, .. } => *phase,
360 Self::Build { .. } => ArtifactCacheBatchFailurePhase::Callback,
361 }
362 }
363
364 #[must_use]
366 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
367 match self {
368 Self::Cache { timings, .. } | Self::Build { timings, .. } => *timings,
369 }
370 }
371
372 #[must_use]
374 pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
375 match self {
376 Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
377 Self::Cache { .. } => None,
378 }
379 }
380}
381
382impl ArtifactCacheBatchFailureTimings {
383 #[must_use]
385 pub const fn preparation(self) -> Duration {
386 self.preparation
387 }
388
389 #[must_use]
391 pub const fn callback(self) -> Option<Duration> {
392 self.callback
393 }
394
395 #[must_use]
397 pub const fn cleanup(self) -> Option<Duration> {
398 self.cleanup
399 }
400
401 #[must_use]
403 pub const fn commit(self) -> Option<Duration> {
404 self.commit
405 }
406
407 #[must_use]
409 pub const fn total(self) -> Duration {
410 self.total
411 }
412}
413
414impl ArtifactCacheBatchMetrics {
415 #[must_use]
417 pub const fn entries(self) -> usize {
418 self.entries
419 }
420
421 #[must_use]
423 pub const fn succeeded(self) -> usize {
424 self.succeeded
425 }
426
427 #[must_use]
429 pub const fn failed(self) -> usize {
430 self.failed
431 }
432
433 #[must_use]
435 pub const fn built(self) -> usize {
436 self.built
437 }
438
439 #[must_use]
441 pub const fn reused(self) -> usize {
442 self.reused
443 }
444
445 #[must_use]
447 pub const fn successful_timings(self) -> ArtifactCacheTimings {
448 self.successful_timings
449 }
450
451 #[must_use]
453 pub const fn total(self) -> Duration {
454 self.total
455 }
456}
457
458impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
459 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 let metrics = self.metrics();
461 write!(
462 formatter,
463 "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
464 metrics.entries(),
465 metrics.succeeded(),
466 metrics.failed(),
467 metrics.built(),
468 metrics.reused(),
469 metrics.successful_timings(),
470 metrics.total(),
471 )
472 }
473}
474
475pub fn build_artifact_caches_batch<E, F>(
488 specs: &[LabeledArtifactCacheSpec],
489 mut populate: F,
490) -> Result<ArtifactCacheBatchReport<E>, ArtifactCacheBatchContractError>
491where
492 F: FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
493{
494 validate_batch_labels(specs)?;
495 let started = Instant::now();
496 let mut entries = Vec::with_capacity(specs.len());
497 for (index, labeled) in specs.iter().enumerate() {
498 let entry_started = Instant::now();
499 let preparation_started = Instant::now();
500 let preparation = prepare_artifact_cache(&labeled.spec);
501 let preparation_elapsed = preparation_started.elapsed();
502 let (result, entry_elapsed) = match preparation {
503 Ok(ArtifactCachePreparation::Reused(record)) => (
504 Ok(ArtifactCacheOutcome::Reused(record)),
505 entry_started.elapsed(),
506 ),
507 Ok(ArtifactCachePreparation::Build(transaction)) => {
508 let callback_started = Instant::now();
509 let callback_result = populate(&labeled.label, &transaction);
510 let callback_elapsed = callback_started.elapsed();
511 if let Err(source) = callback_result {
512 let cleanup_started = Instant::now();
513 let cleanup_error = transaction.abort().err().map(Box::new);
514 let cleanup_elapsed = cleanup_started.elapsed();
515 let entry_elapsed = entry_started.elapsed();
516 (
517 Err(ArtifactCacheBatchFailure::Build {
518 source: Box::new(source),
519 cleanup_error,
520 timings: ArtifactCacheBatchFailureTimings {
521 preparation: preparation_elapsed,
522 callback: Some(callback_elapsed),
523 cleanup: Some(cleanup_elapsed),
524 commit: None,
525 total: entry_elapsed,
526 },
527 }),
528 entry_elapsed,
529 )
530 } else {
531 let commit_started = Instant::now();
532 match transaction.commit() {
533 Ok(outcome) => (Ok(outcome), entry_started.elapsed()),
534 Err(source) => {
535 let commit_elapsed = commit_started.elapsed();
536 let entry_elapsed = entry_started.elapsed();
537 (
538 Err(ArtifactCacheBatchFailure::Cache {
539 phase: ArtifactCacheBatchFailurePhase::Commit,
540 source: Box::new(source),
541 timings: ArtifactCacheBatchFailureTimings {
542 preparation: preparation_elapsed,
543 callback: Some(callback_elapsed),
544 cleanup: None,
545 commit: Some(commit_elapsed),
546 total: entry_elapsed,
547 },
548 }),
549 entry_elapsed,
550 )
551 }
552 }
553 }
554 }
555 Err(source) => {
556 let entry_elapsed = entry_started.elapsed();
557 (
558 Err(ArtifactCacheBatchFailure::Cache {
559 phase: ArtifactCacheBatchFailurePhase::Preparation,
560 source: Box::new(source),
561 timings: ArtifactCacheBatchFailureTimings {
562 preparation: preparation_elapsed,
563 callback: None,
564 cleanup: None,
565 commit: None,
566 total: entry_elapsed,
567 },
568 }),
569 entry_elapsed,
570 )
571 }
572 };
573 entries.push(ArtifactCacheBatchEntry {
574 index,
575 label: labeled.label.clone(),
576 result,
577 entry_elapsed,
578 });
579 }
580 Ok(ArtifactCacheBatchReport {
581 entries,
582 total: started.elapsed(),
583 })
584}
585
586fn validate_batch_labels(
587 specs: &[LabeledArtifactCacheSpec],
588) -> Result<(), ArtifactCacheBatchContractError> {
589 let mut labels = HashMap::with_capacity(specs.len());
590 for (index, labeled) in specs.iter().enumerate() {
591 if labeled.label.is_empty() {
592 return Err(ArtifactCacheBatchContractError::EmptyLabel { index });
593 }
594 if let Some(first_index) = labels.get(labeled.label.as_str()) {
595 return Err(ArtifactCacheBatchContractError::DuplicateLabel {
596 label: labeled.label.clone(),
597 first_index: *first_index,
598 duplicate_index: index,
599 });
600 }
601 labels.insert(labeled.label.as_str(), index);
602 }
603 Ok(())
604}
605
606impl std::fmt::Display for ArtifactCacheBatchContractError {
607 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 match self {
609 Self::EmptyLabel { index } => {
610 write!(formatter, "artifact batch label at index {index} is empty")
611 }
612 Self::DuplicateLabel {
613 label,
614 first_index,
615 duplicate_index,
616 } => write!(
617 formatter,
618 "artifact batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
619 ),
620 }
621 }
622}
623
624impl std::error::Error for ArtifactCacheBatchContractError {}
625
626impl std::fmt::Display for ArtifactCacheBatchFailurePhase {
627 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628 formatter.write_str(match self {
629 Self::Preparation => "preparation",
630 Self::Callback => "callback",
631 Self::Commit => "commit",
632 })
633 }
634}
635
636impl std::fmt::Display for ArtifactCacheBatchFailureTimings {
637 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638 write!(
639 formatter,
640 "total={:?} preparation={:?} callback={:?} cleanup={:?} commit={:?}",
641 self.total, self.preparation, self.callback, self.cleanup, self.commit,
642 )
643 }
644}
645
646impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
647 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 match self {
649 Self::Cache {
650 phase,
651 source,
652 timings,
653 } => write!(
654 formatter,
655 "artifact cache {phase} failed: {source}; timings=({timings})",
656 ),
657 Self::Build {
658 source,
659 cleanup_error,
660 timings,
661 } => {
662 write!(
663 formatter,
664 "artifact callback failed: {source}; timings=({timings})"
665 )?;
666 if let Some(cleanup_error) = cleanup_error {
667 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
668 }
669 Ok(())
670 }
671 }
672 }
673}
674
675impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
676where
677 E: std::error::Error + 'static,
678{
679 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
680 match self {
681 Self::Cache { source, .. } => Some(source.as_ref()),
682 Self::Build { source, .. } => Some(source.as_ref()),
683 }
684 }
685}
686
687#[cfg(test)]
688mod tests;