Skip to main content

ic_testkit/artifacts/
transaction_batch.rs

1use std::time::{Duration, Instant};
2
3use super::{
4    batch::{indexed_failures, indexed_outcomes},
5    transaction::{
6        ArtifactBuildTransaction, ArtifactCacheError, ArtifactCacheOutcome,
7        ArtifactCachePreparation, ArtifactCacheSpec, ArtifactCacheTimings, prepare_artifact_cache,
8    },
9};
10
11/// Ordered outcomes and failures from a collect-all artifact transaction batch.
12#[derive(Debug)]
13pub struct ArtifactCacheBatchReport<E> {
14    results: Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>>,
15    entry_elapsed: Vec<Duration>,
16    total: Duration,
17}
18
19/// One failed generic artifact batch entry with its retained wall-clock time.
20#[derive(Debug)]
21pub struct ArtifactCacheBatchFailedEntry<'a, E> {
22    index: usize,
23    failure: &'a ArtifactCacheBatchFailure<E>,
24    entry_elapsed: Duration,
25}
26
27/// Failure from one entry in a collect-all artifact transaction batch.
28#[derive(Debug)]
29pub enum ArtifactCacheBatchFailure<E> {
30    /// Cache preparation or commit failed.
31    Cache {
32        /// Cache preparation or commit failure.
33        source: Box<ArtifactCacheError>,
34    },
35    /// The caller's population callback failed.
36    Build {
37        /// Caller population failure.
38        source: Box<E>,
39        /// Failure from synchronously aborting the active transaction.
40        cleanup_error: Option<Box<ArtifactCacheError>>,
41    },
42}
43
44/// Aggregate counters and successful-acquisition timings for an artifact batch.
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub struct ArtifactCacheBatchMetrics {
47    entries: usize,
48    succeeded: usize,
49    failed: usize,
50    built: usize,
51    reused: usize,
52    successful_timings: ArtifactCacheTimings,
53    total: Duration,
54}
55
56impl<E> ArtifactCacheBatchReport<E> {
57    /// Per-specification results in the supplied order.
58    pub fn results(&self) -> &[Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>] {
59        &self.results
60    }
61
62    /// Wall-clock time retained for every entry, including failed entries.
63    ///
64    /// Values use the supplied specification order and include preparation,
65    /// caller population, commit, and synchronous failure cleanup.
66    #[must_use]
67    pub fn entry_elapsed(&self) -> &[Duration] {
68        &self.entry_elapsed
69    }
70
71    /// Consume the report and return its ordered per-specification results.
72    #[must_use]
73    pub fn into_results(self) -> Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>> {
74        self.results
75    }
76
77    /// Successful outcomes with their specification indexes.
78    pub fn outcomes(&self) -> impl Iterator<Item = (usize, &ArtifactCacheOutcome)> {
79        indexed_outcomes(&self.results)
80    }
81
82    /// Structured failed entries with indexes, failures, and wall-clock times.
83    pub fn failures(&self) -> impl Iterator<Item = ArtifactCacheBatchFailedEntry<'_, E>> {
84        indexed_failures(&self.results).map(|(index, failure)| ArtifactCacheBatchFailedEntry {
85            index,
86            failure,
87            entry_elapsed: self.entry_elapsed[index],
88        })
89    }
90
91    /// Complete wall-clock time for the sequential collect-all batch.
92    #[must_use]
93    pub const fn total(&self) -> Duration {
94        self.total
95    }
96
97    /// Whether every specification completed successfully.
98    #[must_use]
99    pub fn is_success(&self) -> bool {
100        self.results.iter().all(Result::is_ok)
101    }
102
103    /// Aggregate outcome counters and successful-acquisition timings.
104    #[must_use]
105    pub fn metrics(&self) -> ArtifactCacheBatchMetrics {
106        let mut metrics = ArtifactCacheBatchMetrics {
107            entries: self.results.len(),
108            total: self.total,
109            ..ArtifactCacheBatchMetrics::default()
110        };
111        for result in &self.results {
112            match result {
113                Ok(outcome) => {
114                    metrics.succeeded += 1;
115                    if outcome.is_reused() {
116                        metrics.reused += 1;
117                    } else {
118                        metrics.built += 1;
119                    }
120                    metrics.successful_timings = metrics
121                        .successful_timings
122                        .saturating_add(outcome.record().timings());
123                }
124                Err(_) => metrics.failed += 1,
125            }
126        }
127        metrics
128    }
129}
130
131impl<'a, E> ArtifactCacheBatchFailedEntry<'a, E> {
132    /// Zero-based position in the supplied specification slice.
133    #[must_use]
134    pub const fn index(&self) -> usize {
135        self.index
136    }
137
138    /// Structured cache or caller-build failure.
139    #[must_use]
140    pub const fn failure(&self) -> &'a ArtifactCacheBatchFailure<E> {
141        self.failure
142    }
143
144    /// Complete wall-clock time retained for this failed entry.
145    #[must_use]
146    pub const fn entry_elapsed(&self) -> Duration {
147        self.entry_elapsed
148    }
149}
150
151impl<E> ArtifactCacheBatchFailure<E> {
152    /// Cleanup failure after a caller population error, when one occurred.
153    #[must_use]
154    pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
155        match self {
156            Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
157            Self::Cache { .. } => None,
158        }
159    }
160}
161
162impl ArtifactCacheBatchMetrics {
163    /// Number of supplied specifications.
164    #[must_use]
165    pub const fn entries(self) -> usize {
166        self.entries
167    }
168
169    /// Number of successful specifications.
170    #[must_use]
171    pub const fn succeeded(self) -> usize {
172        self.succeeded
173    }
174
175    /// Number of failed specifications.
176    #[must_use]
177    pub const fn failed(self) -> usize {
178        self.failed
179    }
180
181    /// Number of newly built artifact sets.
182    #[must_use]
183    pub const fn built(self) -> usize {
184        self.built
185    }
186
187    /// Number of artifact sets reused from the exact cache.
188    #[must_use]
189    pub const fn reused(self) -> usize {
190        self.reused
191    }
192
193    /// Sum of timings from successful acquisitions.
194    #[must_use]
195    pub const fn successful_timings(self) -> ArtifactCacheTimings {
196        self.successful_timings
197    }
198
199    /// Complete wall-clock time for the sequential batch.
200    #[must_use]
201    pub const fn total(self) -> Duration {
202        self.total
203    }
204}
205
206impl<E> std::fmt::Display for ArtifactCacheBatchReport<E> {
207    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        let metrics = self.metrics();
209        write!(
210            formatter,
211            "entries={} succeeded={} failed={} built={} reused={} successful_timings=({}) total={:?}",
212            metrics.entries(),
213            metrics.succeeded(),
214            metrics.failed(),
215            metrics.built(),
216            metrics.reused(),
217            metrics.successful_timings(),
218            metrics.total(),
219        )
220    }
221}
222
223/// Build every independent transactional artifact specification.
224///
225/// The callback runs only for misses and receives one live transaction at a
226/// time. Sequential acquisition avoids self-deadlock when entries share a
227/// coordination scope. Every result is retained, and a callback error aborts
228/// and synchronously cleans that transaction before the next entry begins.
229///
230/// This operation is not atomic across specifications. A caller requiring one
231/// all-or-nothing multi-output publication should declare those outputs on one
232/// [`ArtifactCacheSpec`] instead.
233#[must_use]
234pub fn build_artifact_caches_batch<E, F>(
235    specs: &[ArtifactCacheSpec],
236    mut populate: F,
237) -> ArtifactCacheBatchReport<E>
238where
239    F: FnMut(usize, &ArtifactBuildTransaction) -> Result<(), E>,
240{
241    let started = Instant::now();
242    let mut results = Vec::with_capacity(specs.len());
243    let mut entry_elapsed = Vec::with_capacity(specs.len());
244    for (index, spec) in specs.iter().enumerate() {
245        let entry_started = Instant::now();
246        let result = match prepare_artifact_cache(spec) {
247            Ok(ArtifactCachePreparation::Reused(record)) => {
248                Ok(ArtifactCacheOutcome::Reused(record))
249            }
250            Ok(ArtifactCachePreparation::Build(transaction)) => {
251                if let Err(source) = populate(index, &transaction) {
252                    let cleanup_error = transaction.abort().err().map(Box::new);
253                    Err(ArtifactCacheBatchFailure::Build {
254                        source: Box::new(source),
255                        cleanup_error,
256                    })
257                } else {
258                    transaction
259                        .commit()
260                        .map_err(|source| ArtifactCacheBatchFailure::Cache {
261                            source: Box::new(source),
262                        })
263                }
264            }
265            Err(source) => Err(ArtifactCacheBatchFailure::Cache {
266                source: Box::new(source),
267            }),
268        };
269        results.push(result);
270        entry_elapsed.push(entry_started.elapsed());
271    }
272    ArtifactCacheBatchReport {
273        results,
274        entry_elapsed,
275        total: started.elapsed(),
276    }
277}
278
279impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchFailure<E> {
280    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        match self {
282            Self::Cache { source } => write!(formatter, "artifact cache failed: {source}"),
283            Self::Build {
284                source,
285                cleanup_error,
286            } => {
287                write!(formatter, "artifact builder failed: {source}")?;
288                if let Some(cleanup_error) = cleanup_error {
289                    write!(formatter, "; cleanup also failed: {cleanup_error}")?;
290                }
291                Ok(())
292            }
293        }
294    }
295}
296
297impl<E> std::error::Error for ArtifactCacheBatchFailure<E>
298where
299    E: std::error::Error + 'static,
300{
301    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
302        match self {
303            Self::Cache { source } => Some(source.as_ref()),
304            Self::Build { source, .. } => Some(source.as_ref()),
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests;