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, prepare_artifact_cache,
6};
7
8/// Successful outcomes from sequential independent artifact transactions.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct ArtifactCacheBatchOutcome {
11    outcomes: Vec<ArtifactCacheOutcome>,
12    total: Duration,
13}
14
15/// Failure from a sequential independent artifact transaction batch.
16#[derive(Debug)]
17pub enum ArtifactCacheBatchError<E> {
18    /// Cache preparation or commit failed.
19    Cache {
20        /// Zero-based position of the failed specification.
21        failed_index: usize,
22        /// Successful outcomes completed before the failure.
23        completed: Vec<ArtifactCacheOutcome>,
24        /// Wall-clock time through the failure.
25        total: Duration,
26        /// Cache preparation or commit failure.
27        source: Box<ArtifactCacheError>,
28    },
29    /// The caller's population callback failed.
30    Build {
31        /// Zero-based position of the failed specification.
32        failed_index: usize,
33        /// Successful outcomes completed before the failure.
34        completed: Vec<ArtifactCacheOutcome>,
35        /// Wall-clock time through the failure.
36        total: Duration,
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
44impl ArtifactCacheBatchOutcome {
45    /// Successful outcomes in specification order.
46    #[must_use]
47    pub fn outcomes(&self) -> &[ArtifactCacheOutcome] {
48        &self.outcomes
49    }
50
51    /// Consume the report and return its ordered outcomes.
52    #[must_use]
53    pub fn into_outcomes(self) -> Vec<ArtifactCacheOutcome> {
54        self.outcomes
55    }
56
57    /// Complete wall-clock time for the sequential batch.
58    #[must_use]
59    pub const fn total(&self) -> Duration {
60        self.total
61    }
62}
63
64impl<E> ArtifactCacheBatchError<E> {
65    /// Zero-based index of the failed independent specification.
66    #[must_use]
67    pub const fn failed_index(&self) -> usize {
68        match self {
69            Self::Cache { failed_index, .. } | Self::Build { failed_index, .. } => *failed_index,
70        }
71    }
72
73    /// Successful outcomes completed before the failure.
74    #[must_use]
75    pub fn completed(&self) -> &[ArtifactCacheOutcome] {
76        match self {
77            Self::Cache { completed, .. } | Self::Build { completed, .. } => completed,
78        }
79    }
80
81    /// Wall-clock time through the failure.
82    #[must_use]
83    pub const fn total(&self) -> Duration {
84        match self {
85            Self::Cache { total, .. } | Self::Build { total, .. } => *total,
86        }
87    }
88
89    /// Cleanup failure after a caller population error, when one occurred.
90    #[must_use]
91    pub fn cleanup_error(&self) -> Option<&ArtifactCacheError> {
92        match self {
93            Self::Build { cleanup_error, .. } => cleanup_error.as_deref(),
94            Self::Cache { .. } => None,
95        }
96    }
97}
98
99impl std::fmt::Display for ArtifactCacheBatchOutcome {
100    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        let reused = self
102            .outcomes
103            .iter()
104            .filter(|outcome| outcome.is_reused())
105            .count();
106        write!(
107            formatter,
108            "entries={} built={} reused={} total={:?}",
109            self.outcomes.len(),
110            self.outcomes.len().saturating_sub(reused),
111            reused,
112            self.total,
113        )
114    }
115}
116
117/// Build or reuse multiple independent transactional artifact sets.
118///
119/// The callback runs only for misses and receives one live transaction at a
120/// time. Sequential acquisition avoids self-deadlock when entries share a
121/// coordination scope. A callback error aborts and synchronously cleans that
122/// transaction before returning. Completed entries remain valid when a later
123/// entry fails.
124///
125/// This operation is not atomic across specifications. A caller requiring one
126/// all-or-nothing multi-output publication should declare those outputs on one
127/// [`ArtifactCacheSpec`] instead.
128pub fn build_artifact_caches_batch<E, F>(
129    specs: &[ArtifactCacheSpec],
130    mut populate: F,
131) -> Result<ArtifactCacheBatchOutcome, ArtifactCacheBatchError<E>>
132where
133    F: FnMut(usize, &ArtifactBuildTransaction) -> Result<(), E>,
134{
135    let started = Instant::now();
136    let mut outcomes = Vec::with_capacity(specs.len());
137    for (index, spec) in specs.iter().enumerate() {
138        let preparation = match prepare_artifact_cache(spec) {
139            Ok(preparation) => preparation,
140            Err(source) => {
141                return Err(ArtifactCacheBatchError::Cache {
142                    failed_index: index,
143                    completed: outcomes,
144                    total: started.elapsed(),
145                    source: Box::new(source),
146                });
147            }
148        };
149        let outcome = match preparation {
150            ArtifactCachePreparation::Reused(record) => ArtifactCacheOutcome::Reused(record),
151            ArtifactCachePreparation::Build(transaction) => {
152                if let Err(source) = populate(index, &transaction) {
153                    let cleanup_error = transaction.abort().err().map(Box::new);
154                    return Err(ArtifactCacheBatchError::Build {
155                        failed_index: index,
156                        completed: outcomes,
157                        total: started.elapsed(),
158                        source: Box::new(source),
159                        cleanup_error,
160                    });
161                }
162                match transaction.commit() {
163                    Ok(outcome) => outcome,
164                    Err(source) => {
165                        return Err(ArtifactCacheBatchError::Cache {
166                            failed_index: index,
167                            completed: outcomes,
168                            total: started.elapsed(),
169                            source: Box::new(source),
170                        });
171                    }
172                }
173            }
174        };
175        outcomes.push(outcome);
176    }
177    Ok(ArtifactCacheBatchOutcome {
178        outcomes,
179        total: started.elapsed(),
180    })
181}
182
183impl<E: std::fmt::Display> std::fmt::Display for ArtifactCacheBatchError<E> {
184    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        match self {
186            Self::Cache {
187                failed_index,
188                completed,
189                source,
190                ..
191            } => write!(
192                formatter,
193                "artifact cache batch entry {failed_index} failed after {} successful entry/entries: {source}",
194                completed.len(),
195            ),
196            Self::Build {
197                failed_index,
198                completed,
199                source,
200                cleanup_error,
201                ..
202            } => {
203                write!(
204                    formatter,
205                    "artifact cache batch builder {failed_index} failed after {} successful entry/entries: {source}",
206                    completed.len(),
207                )?;
208                if let Some(cleanup_error) = cleanup_error {
209                    write!(formatter, "; cleanup also failed: {cleanup_error}")?;
210                }
211                Ok(())
212            }
213        }
214    }
215}
216
217impl<E> std::error::Error for ArtifactCacheBatchError<E>
218where
219    E: std::error::Error + 'static,
220{
221    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
222        match self {
223            Self::Cache { source, .. } => Some(source.as_ref()),
224            Self::Build { source, .. } => Some(source.as_ref()),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests;