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