ic_backup/policy/execution_progress/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 attempt_journal::{AttemptJournalRecord, AttemptJournalView},
6 operation_plan::MAX_PLAN_OPERATIONS,
7 operation_plan::OperationPlanRecord,
8};
9use serde::Serialize;
10use std::collections::BTreeMap;
11use thiserror::Error;
12
13#[derive(Clone, Debug)]
15pub struct ExecutionProgressRequest<'a> {
16 pub plan: &'a OperationPlanRecord,
18 pub journals: &'a [&'a AttemptJournalRecord],
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum OperationProgressState {
26 AwaitingDependencies,
28 MutationAvailable,
30 MutationExhausted,
32 MutationUnresolved,
34 ObservationUnresolved,
36 ReconciliationExhausted,
38 Applied,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
44pub struct OperationProgressView {
45 pub operation_sequence: u64,
47 pub state: OperationProgressState,
49 pub attempts: AttemptJournalView,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
55pub struct ExecutionAttemptTotalsView {
56 pub mutations_used: u32,
58 pub observations_used: u32,
60 pub mutations_remaining: u32,
62 pub observations_remaining: u32,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
71pub struct ExecutionProgressView {
72 pub intent: ArtifactChecksumRecord,
74 pub applied_operations: usize,
76 pub attempts: ExecutionAttemptTotalsView,
78 pub operations: Vec<OperationProgressView>,
80}
81
82impl ExecutionProgressView {
83 pub(crate) const fn is_unattempted(&self) -> bool {
84 self.attempts.mutations_used == 0 && self.attempts.observations_used == 0
85 }
86
87 pub(crate) fn all_applied(&self) -> bool {
88 self.applied_operations == self.operations.len()
89 }
90}
91
92pub fn progress(
103 request: &ExecutionProgressRequest<'_>,
104) -> Result<ExecutionProgressView, ExecutionProgressError> {
105 admit_progress(request.plan, request.journals.iter().copied())?.project(request.plan)
106}
107
108pub(crate) struct AdmittedProgress<'a> {
110 intent: ArtifactChecksumRecord,
111 pub(crate) journals: BTreeMap<u64, &'a AttemptJournalRecord>,
112}
113
114impl AdmittedProgress<'_> {
115 pub(crate) fn dependencies_applied(&self, dependencies: &[u64]) -> bool {
116 dependencies
117 .iter()
118 .all(|sequence| self.journals[sequence].applied_receipt().is_some())
119 }
120
121 pub(crate) fn project(
122 self,
123 plan: &OperationPlanRecord,
124 ) -> Result<ExecutionProgressView, ExecutionProgressError> {
125 let mut totals = ExecutionAttemptTotalsView {
126 mutations_used: 0,
127 observations_used: 0,
128 mutations_remaining: 0,
129 observations_remaining: 0,
130 };
131 let mut operations = Vec::with_capacity(self.journals.len());
132 for node in plan.ordered_operations() {
133 let sequence = node.operation_sequence();
134 let attempts = self.journals[&sequence].view();
135 totals.add(&attempts)?;
136 operations.push(OperationProgressView {
137 operation_sequence: sequence,
138 state: condition(&attempts, !self.dependencies_applied(node.depends_on())),
139 attempts,
140 });
141 }
142 Ok(ExecutionProgressView {
143 intent: self.intent,
144 applied_operations: operations
145 .iter()
146 .filter(|operation| operation.attempts.applied)
147 .count(),
148 attempts: totals,
149 operations,
150 })
151 }
152}
153
154pub(crate) fn admit_progress<'a>(
155 plan: &OperationPlanRecord,
156 originals: impl ExactSizeIterator<Item = &'a AttemptJournalRecord>,
157) -> Result<AdmittedProgress<'a>, ExecutionProgressError> {
158 let (intent, journals) = admit_journals(plan, originals)?;
159 for node in plan.ordered_operations() {
162 let sequence = node.operation_sequence();
163 if journals[&sequence].mutations_used() != 0
164 && let Some(prerequisite) = node
165 .depends_on()
166 .iter()
167 .find(|dependency| journals[*dependency].applied_receipt().is_none())
168 {
169 return Err(ExecutionProgressError::PrematureAttempt {
170 operation_sequence: sequence,
171 prerequisite: *prerequisite,
172 });
173 }
174 }
175 Ok(AdmittedProgress { intent, journals })
176}
177fn admit_journals<'a>(
179 plan: &OperationPlanRecord,
180 originals: impl ExactSizeIterator<Item = &'a AttemptJournalRecord>,
181) -> Result<
182 (
183 ArtifactChecksumRecord,
184 BTreeMap<u64, &'a AttemptJournalRecord>,
185 ),
186 ExecutionProgressError,
187> {
188 if originals.len() > MAX_PLAN_OPERATIONS {
189 return Err(ExecutionProgressError::TooManyJournals);
190 }
191 let intent = plan.digest();
192 let mut journals = BTreeMap::new();
193 for journal in originals {
194 let sequence = journal.authority().binding().operation_sequence();
195 let operation = plan
196 .operation(sequence)
197 .map_err(|_| ExecutionProgressError::UnknownOperation(sequence))?;
198 if journals.contains_key(&sequence) {
199 return Err(ExecutionProgressError::DuplicateJournal(sequence));
200 }
201 if !plan.matches_authority(operation, &intent, journal.authority()) {
202 return Err(ExecutionProgressError::AuthorityMismatch(sequence));
203 }
204 journals.insert(sequence, journal);
205 }
206 for operation in plan.operations() {
207 if !journals.contains_key(&operation.operation_sequence()) {
208 return Err(ExecutionProgressError::MissingJournal(
209 operation.operation_sequence(),
210 ));
211 }
212 }
213 Ok((intent, journals))
214}
215
216impl ExecutionAttemptTotalsView {
217 fn add(&mut self, attempts: &AttemptJournalView) -> Result<(), ExecutionProgressError> {
218 self.mutations_used = sum(self.mutations_used, attempts.mutations_used)?;
219 self.observations_used = sum(self.observations_used, attempts.observations_used)?;
220 self.mutations_remaining = sum(self.mutations_remaining, attempts.mutations_remaining)?;
221 self.observations_remaining =
222 sum(self.observations_remaining, attempts.observations_remaining)?;
223 Ok(())
224 }
225}
226fn sum(left: u32, right: u32) -> Result<u32, ExecutionProgressError> {
227 left.checked_add(right)
228 .ok_or(ExecutionProgressError::AccountingOverflow)
229}
230fn condition(attempts: &AttemptJournalView, unmet_dependencies: bool) -> OperationProgressState {
231 if attempts.applied {
232 OperationProgressState::Applied
233 } else if attempts.pending_observation.is_some() {
234 OperationProgressState::ObservationUnresolved
235 } else if attempts.pending_mutation.is_some() {
236 if attempts.observations_remaining == 0 {
237 OperationProgressState::ReconciliationExhausted
238 } else {
239 OperationProgressState::MutationUnresolved
240 }
241 } else if unmet_dependencies {
242 OperationProgressState::AwaitingDependencies
243 } else if attempts.mutations_remaining == 0 {
244 OperationProgressState::MutationExhausted
245 } else {
246 OperationProgressState::MutationAvailable
247 }
248}
249
250#[derive(Debug, Error, Eq, PartialEq)]
252pub enum ExecutionProgressError {
253 #[error("execution progress exceeds {MAX_PLAN_OPERATIONS} journals")]
255 TooManyJournals,
256 #[error("duplicate journal for operation {0}")]
258 DuplicateJournal(u64),
259 #[error("journal operation {0} is absent from the original plan")]
261 UnknownOperation(u64),
262 #[error("missing original journal for operation {0}")]
264 MissingJournal(u64),
265 #[error("original journal authority mismatch for operation {0}")]
267 AuthorityMismatch(u64),
268 #[error(
270 "operation {operation_sequence} was attempted without applied prerequisite {prerequisite}"
271 )]
272 PrematureAttempt {
273 operation_sequence: u64,
275 prerequisite: u64,
277 },
278 #[error("execution attempt accounting overflow")]
280 AccountingOverflow,
281}
282
283#[cfg(test)]
284mod tests;