ic_testkit/artifacts/
transaction_batch.rs1use std::time::{Duration, Instant};
2
3use super::transaction::{
4 ArtifactBuildTransaction, ArtifactCacheError, ArtifactCacheOutcome, ArtifactCachePreparation,
5 ArtifactCacheSpec, ArtifactCacheTimings, prepare_artifact_cache,
6};
7
8#[derive(Debug)]
10pub struct ArtifactCacheBatchReport<E> {
11 results: Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>>,
12 total: Duration,
13}
14
15#[derive(Debug)]
17pub enum ArtifactCacheBatchFailure<E> {
18 Cache {
20 source: Box<ArtifactCacheError>,
22 },
23 Build {
25 source: Box<E>,
27 cleanup_error: Option<Box<ArtifactCacheError>>,
29 },
30}
31
32#[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 pub fn results(&self) -> &[Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>] {
47 &self.results
48 }
49
50 #[must_use]
52 pub fn into_results(self) -> Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>> {
53 self.results
54 }
55
56 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 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 #[must_use]
74 pub const fn total(&self) -> Duration {
75 self.total
76 }
77
78 #[must_use]
80 pub fn is_success(&self) -> bool {
81 self.results.iter().all(Result::is_ok)
82 }
83
84 #[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 #[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 #[must_use]
126 pub const fn entries(self) -> usize {
127 self.entries
128 }
129
130 #[must_use]
132 pub const fn succeeded(self) -> usize {
133 self.succeeded
134 }
135
136 #[must_use]
138 pub const fn failed(self) -> usize {
139 self.failed
140 }
141
142 #[must_use]
144 pub const fn built(self) -> usize {
145 self.built
146 }
147
148 #[must_use]
150 pub const fn reused(self) -> usize {
151 self.reused
152 }
153
154 #[must_use]
156 pub const fn successful_timings(self) -> ArtifactCacheTimings {
157 self.successful_timings
158 }
159
160 #[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#[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;