ic_testkit/artifacts/
transaction_batch.rs1use 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#[derive(Debug)]
13pub struct ArtifactCacheBatchReport<E> {
14 results: Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>>,
15 total: Duration,
16}
17
18#[derive(Debug)]
20pub enum ArtifactCacheBatchFailure<E> {
21 Cache {
23 source: Box<ArtifactCacheError>,
25 },
26 Build {
28 source: Box<E>,
30 cleanup_error: Option<Box<ArtifactCacheError>>,
32 },
33}
34
35#[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 pub fn results(&self) -> &[Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>] {
50 &self.results
51 }
52
53 #[must_use]
55 pub fn into_results(self) -> Vec<Result<ArtifactCacheOutcome, ArtifactCacheBatchFailure<E>>> {
56 self.results
57 }
58
59 pub fn outcomes(&self) -> impl Iterator<Item = (usize, &ArtifactCacheOutcome)> {
61 indexed_outcomes(&self.results)
62 }
63
64 pub fn failures(&self) -> impl Iterator<Item = (usize, &ArtifactCacheBatchFailure<E>)> {
66 indexed_failures(&self.results)
67 }
68
69 #[must_use]
71 pub const fn total(&self) -> Duration {
72 self.total
73 }
74
75 #[must_use]
77 pub fn is_success(&self) -> bool {
78 self.results.iter().all(Result::is_ok)
79 }
80
81 #[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 #[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 #[must_use]
123 pub const fn entries(self) -> usize {
124 self.entries
125 }
126
127 #[must_use]
129 pub const fn succeeded(self) -> usize {
130 self.succeeded
131 }
132
133 #[must_use]
135 pub const fn failed(self) -> usize {
136 self.failed
137 }
138
139 #[must_use]
141 pub const fn built(self) -> usize {
142 self.built
143 }
144
145 #[must_use]
147 pub const fn reused(self) -> usize {
148 self.reused
149 }
150
151 #[must_use]
153 pub const fn successful_timings(self) -> ArtifactCacheTimings {
154 self.successful_timings
155 }
156
157 #[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#[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;