Skip to main content

ic_testkit/artifacts/
transaction_batch.rs

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