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#[non_exhaustive]
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub enum ArtifactCacheBatchContractError {
40 EmptyLabel {
42 index: usize,
44 },
45 DuplicateLabel {
47 label: String,
49 first_index: usize,
51 duplicate_index: usize,
53 },
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum ArtifactCacheBatchFailurePhase {
59 Preparation,
61 Callback,
63 Commit,
65}
66
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
69pub struct ArtifactCacheBatchFailureTimings {
70 preparation: Duration,
71 callback: Option<Duration>,
72 cleanup: Option<Duration>,
73 commit: Option<Duration>,
74 total: Duration,
75}
76
77#[derive(Debug)]
79pub enum ArtifactCacheBatchFailure<E> {
80 Cache {
82 phase: ArtifactCacheBatchFailurePhase,
84 source: Box<ArtifactCacheError>,
86 timings: ArtifactCacheBatchFailureTimings,
88 },
89 Build {
91 source: Box<E>,
93 cleanup_error: Option<Box<ArtifactCacheError>>,
95 timings: ArtifactCacheBatchFailureTimings,
97 },
98}
99
100#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
102pub struct ArtifactCacheBatchMetrics {
103 entries: usize,
104 built: usize,
105 reused: usize,
106 successful_timings: ArtifactCacheTimings,
107 total: Duration,
108}
109
110impl LabeledArtifactCacheSpec {
111 #[must_use]
113 pub fn new(label: impl Into<String>, spec: ArtifactCacheSpec) -> Self {
114 Self {
115 label: label.into(),
116 spec,
117 }
118 }
119
120 #[must_use]
122 pub fn label(&self) -> &str {
123 &self.label
124 }
125
126 #[must_use]
128 pub const fn spec(&self) -> &ArtifactCacheSpec {
129 &self.spec
130 }
131
132 #[must_use]
134 pub fn into_parts(self) -> (String, ArtifactCacheSpec) {
135 (self.label, self.spec)
136 }
137}
138
139impl<E> ArtifactCacheBatchReport<E> {
140 #[must_use]
142 pub fn entries(&self) -> &[ArtifactCacheBatchEntry<E>] {
143 &self.entries
144 }
145
146 #[must_use]
148 pub fn into_entries(self) -> Vec<ArtifactCacheBatchEntry<E>> {
149 self.entries
150 }
151
152 pub fn outcomes(&self) -> impl Iterator<Item = &ArtifactCacheBatchEntry<E>> {
154 self.entries.iter().filter(|entry| entry.is_success())
155 }
156
157 pub fn failures(&self) -> impl Iterator<Item = &ArtifactCacheBatchEntry<E>> {
159 self.entries.iter().filter(|entry| !entry.is_success())
160 }
161
162 #[must_use]
164 pub const fn total(&self) -> Duration {
165 self.total
166 }
167
168 #[must_use]
170 pub fn is_success(&self) -> bool {
171 self.entries.iter().all(ArtifactCacheBatchEntry::is_success)
172 }
173
174 #[must_use]
176 pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
177 let mut metrics = ArtifactCacheBatchMetrics {
178 entries: self.entries.len(),
179 total: self.total,
180 ..ArtifactCacheBatchMetrics::default()
181 };
182 for outcome in self
183 .entries
184 .iter()
185 .filter_map(ArtifactCacheBatchEntry::outcome)
186 {
187 if outcome.is_reused() {
188 metrics.reused += 1;
189 } else {
190 metrics.built += 1;
191 }
192 metrics.successful_timings = metrics
193 .successful_timings
194 .saturating_add(outcome.record().timings());
195 }
196 metrics
197 }
198}
199
200impl<E> ArtifactCacheBatchEntry<E> {
201 #[must_use]
203 pub const fn index(&self) -> usize {
204 self.index
205 }
206
207 #[must_use]
209 pub fn label(&self) -> &str {
210 &self.label
211 }
212
213 pub const fn result(&self) -> Result<&ArtifactCacheOutcome, &ArtifactCacheBatchFailure<E>> {
218 self.result.as_ref()
219 }
220
221 #[must_use]
223 pub fn outcome(&self) -> Option<&ArtifactCacheOutcome> {
224 self.result.as_ref().ok()
225 }
226
227 #[must_use]
229 pub fn failure(&self) -> Option<&ArtifactCacheBatchFailure<E>> {
230 self.result.as_ref().err()
231 }
232
233 #[must_use]
235 pub const fn entry_elapsed(&self) -> Duration {
236 self.entry_elapsed
237 }
238
239 #[must_use]
241 pub const fn is_success(&self) -> bool {
242 self.result.is_ok()
243 }
244
245 pub fn into_parts(
247 self,
248 ) -> (
249 usize,
250 String,
251 Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>,
252 Duration,
253 ) {
254 (self.index, self.label, self.result, self.entry_elapsed)
255 }
256}
257
258impl<E> ArtifactCacheBatchFailure<E> {
259 #[must_use]
261 pub const fn phase(&self) -> ArtifactCacheBatchFailurePhase {
262 match self {
263 Self::Cache { phase, .. } => *phase,
264 Self::Build { .. } => ArtifactCacheBatchFailurePhase::Callback,
265 }
266 }
267
268 #[must_use]
270 pub const fn timings(&self) -> ArtifactCacheBatchFailureTimings {
271 match self {
272 Self::Cache { timings, .. } | Self::Build { timings, .. } => *timings,
273 }
274 }
275
276 #[must_use]
278 pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
279 match self {
280 Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
281 Self::Cache { .. } => None,
282 }
283 }
284}
285
286impl ArtifactCacheBatchFailureTimings {
287 #[must_use]
289 pub const fn preparation(self) -> Duration {
290 self.preparation
291 }
292
293 #[must_use]
295 pub const fn callback(self) -> Option<Duration> {
296 self.callback
297 }
298
299 #[must_use]
301 pub const fn cleanup(self) -> Option<Duration> {
302 self.cleanup
303 }
304
305 #[must_use]
307 pub const fn commit(self) -> Option<Duration> {
308 self.commit
309 }
310
311 #[must_use]
313 pub const fn total(self) -> Duration {
314 self.total
315 }
316}
317
318impl ArtifactCacheBatchMetrics {
319 #[must_use]
321 pub const fn entries(self) -> usize {
322 self.entries
323 }
324
325 #[must_use]
327 pub const fn succeeded(self) -> usize {
328 self.built + self.reused
329 }
330
331 #[must_use]
333 pub const fn failed(self) -> usize {
334 self.entries - self.succeeded()
335 }
336
337 #[must_use]
339 pub const fn built(self) -> usize {
340 self.built
341 }
342
343 #[must_use]
345 pub const fn reused(self) -> usize {
346 self.reused
347 }
348
349 #[must_use]
351 pub const fn successful_timings(self) -> ArtifactCacheTimings {
352 self.successful_timings
353 }
354
355 #[must_use]
357 pub const fn total(self) -> Duration {
358 self.total
359 }
360}
361
362impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
363 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 let metrics = self.metrics();
365 write!(
366 formatter,
367 "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
368 metrics.entries(),
369 metrics.succeeded(),
370 metrics.failed(),
371 metrics.built(),
372 metrics.reused(),
373 metrics.successful_timings(),
374 metrics.total(),
375 )
376 }
377}
378
379pub fn build_artifact_caches_batch<E, F>(
396 specs: &[LabeledArtifactCacheSpec],
397 mut populate: F,
398) -> Result<ArtifactCacheBatchReport<E>, ArtifactCacheBatchContractError>
399where
400 F: FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
401{
402 validate_batch_labels(specs)?;
403 let started = Instant::now();
404 let mut entries = Vec::with_capacity(specs.len());
405 for (index, labeled) in specs.iter().enumerate() {
406 entries.push(build_artifact_cache_entry(index, labeled, &mut populate));
407 }
408 Ok(ArtifactCacheBatchReport {
409 entries,
410 total: started.elapsed(),
411 })
412}
413
414fn build_artifact_cache_entry<E>(
415 index: usize,
416 labeled: &LabeledArtifactCacheSpec,
417 populate: &mut impl FnMut(&str, &ArtifactBuildTransaction) -> Result<(), E>,
418) -> ArtifactCacheBatchEntry<E> {
419 let entry_started = Instant::now();
420 let mut timings = ArtifactCacheBatchFailureTimings::default();
421 let mut result = (|| {
422 let preparation_started = Instant::now();
423 let preparation = prepare_artifact_cache(&labeled.spec);
424 timings.preparation = preparation_started.elapsed();
425 let transaction = match preparation.map_err(|source| ArtifactCacheBatchFailure::Cache {
426 phase: ArtifactCacheBatchFailurePhase::Preparation,
427 source: Box::new(source),
428 timings,
429 })? {
430 ArtifactCachePreparation::Reused(record) => {
431 return Ok(ArtifactCacheOutcome::Reused(record));
432 }
433 ArtifactCachePreparation::Build(transaction) => transaction,
434 };
435
436 let callback_started = Instant::now();
437 let callback_result = populate(&labeled.label, &transaction);
438 timings.callback = Some(callback_started.elapsed());
439 if let Err(source) = callback_result {
440 let cleanup_started = Instant::now();
441 let cleanup_error = transaction.abort().err().map(Box::new);
442 timings.cleanup = Some(cleanup_started.elapsed());
443 return Err(ArtifactCacheBatchFailure::Build {
444 source: Box::new(source),
445 cleanup_error,
446 timings,
447 });
448 }
449
450 let commit_started = Instant::now();
451 let committed = transaction.commit();
452 timings.commit = Some(commit_started.elapsed());
453 committed.map_err(|source| ArtifactCacheBatchFailure::Cache {
454 phase: ArtifactCacheBatchFailurePhase::Commit,
455 source: Box::new(source),
456 timings,
457 })
458 })();
459 let entry_elapsed = entry_started.elapsed();
460 if let Err(
461 ArtifactCacheBatchFailure::Cache { timings, .. }
462 | ArtifactCacheBatchFailure::Build { timings, .. },
463 ) = &mut result
464 {
465 timings.total = entry_elapsed;
466 }
467 ArtifactCacheBatchEntry {
468 index,
469 label: labeled.label.clone(),
470 result,
471 entry_elapsed,
472 }
473}
474
475fn validate_batch_labels(
476 specs: &[LabeledArtifactCacheSpec],
477) -> Result<(), ArtifactCacheBatchContractError> {
478 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
479 {
480 BatchLabelError::Empty { index } => ArtifactCacheBatchContractError::EmptyLabel { index },
481 BatchLabelError::Duplicate {
482 label,
483 first_index,
484 duplicate_index,
485 } => ArtifactCacheBatchContractError::DuplicateLabel {
486 label,
487 first_index,
488 duplicate_index,
489 },
490 })
491}
492
493impl std::fmt::Display for ArtifactCacheBatchContractError {
494 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495 match self {
496 Self::EmptyLabel { index } => {
497 write!(formatter, "artifact batch label at index {index} is empty")
498 }
499 Self::DuplicateLabel {
500 label,
501 first_index,
502 duplicate_index,
503 } => write!(
504 formatter,
505 "artifact batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
506 ),
507 }
508 }
509}
510
511impl std::error::Error for ArtifactCacheBatchContractError {}
512
513impl std::fmt::Display for ArtifactCacheBatchFailurePhase {
514 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 formatter.write_str(match self {
516 Self::Preparation => "preparation",
517 Self::Callback => "callback",
518 Self::Commit => "commit",
519 })
520 }
521}
522
523impl std::fmt::Display for ArtifactCacheBatchFailureTimings {
524 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 write!(
526 formatter,
527 "total={:?} preparation={:?} callback={:?} cleanup={:?} commit={:?}",
528 self.total, self.preparation, self.callback, self.cleanup, self.commit,
529 )
530 }
531}
532
533impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
534 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535 match self {
536 Self::Cache {
537 phase,
538 source,
539 timings,
540 } => write!(
541 formatter,
542 "artifact cache {phase} failed: {source}; timings=({timings})",
543 ),
544 Self::Build {
545 source,
546 cleanup_error,
547 timings,
548 } => {
549 write!(
550 formatter,
551 "artifact callback failed: {source}; timings=({timings})"
552 )?;
553 if let Some(cleanup_error) = cleanup_error {
554 write!(formatter, "; cleanup also failed: {cleanup_error}")?;
555 }
556 Ok(())
557 }
558 }
559 }
560}
561
562impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
563where
564 E: std::error::Error + 'static,
565{
566 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
567 match self {
568 Self::Cache { source, .. } => Some(source.as_ref()),
569 Self::Build { source, .. } => Some(source.as_ref()),
570 }
571 }
572}
573
574#[cfg(test)]
575mod tests;