ic_backup/model/effect_graph/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4mod node;
5pub use node::{EffectNodeRecord, EffectNodeRequest, MAX_EFFECT_DEPENDENCIES};
6
7use crate::model::{artifacts::ArtifactChecksumRecord, bounded_sequence};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10use thiserror::Error;
11
12pub const MAX_EFFECT_OPERATIONS: usize = 8192;
14pub const MAX_EFFECT_EDGES: usize = 65536;
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(try_from = "GraphFields")]
24pub struct EffectGraphRecord {
25 version: u16,
26 nodes: Vec<EffectNodeRecord>,
27 #[serde(skip)]
28 order: Vec<usize>,
29}
30
31#[derive(Deserialize)]
32#[serde(deny_unknown_fields)]
33struct GraphFields {
34 version: u16,
35 #[serde(deserialize_with = "bounded_nodes")]
36 nodes: Vec<EffectNodeRecord>,
37}
38impl TryFrom<GraphFields> for EffectGraphRecord {
39 type Error = EffectGraphError;
40 fn try_from(fields: GraphFields) -> Result<Self, Self::Error> {
41 if fields.version != 1 {
42 return Err(EffectGraphError::UnsupportedVersion(fields.version));
43 }
44 Self::new(fields.nodes)
45 }
46}
47
48impl EffectGraphRecord {
49 pub fn new(mut nodes: Vec<EffectNodeRecord>) -> Result<Self, EffectGraphError> {
54 if nodes.is_empty() {
55 return Err(EffectGraphError::EmptyGraph);
56 }
57 if nodes.len() > MAX_EFFECT_OPERATIONS {
58 return Err(EffectGraphError::TooManyOperations);
59 }
60 nodes.sort_by_key(EffectNodeRecord::operation_sequence);
61 for pair in nodes.windows(2) {
62 if pair[0].operation_sequence() == pair[1].operation_sequence() {
63 return Err(EffectGraphError::DuplicateOperation(
64 pair[0].operation_sequence(),
65 ));
66 }
67 }
68 let mut edges = 0;
69 for node in &nodes {
70 edges = add_edges(edges, node.depends_on().len())?;
71 }
72 let order = topological_order(&nodes)?;
73 Ok(Self {
74 version: 1,
75 nodes,
76 order,
77 })
78 }
79 #[must_use]
81 pub fn nodes(&self) -> &[EffectNodeRecord] {
82 &self.nodes
83 }
84 pub fn node(&self, sequence: u64) -> Result<&EffectNodeRecord, EffectGraphError> {
89 self.nodes
90 .binary_search_by_key(&sequence, EffectNodeRecord::operation_sequence)
91 .map(|index| &self.nodes[index])
92 .map_err(|_| EffectGraphError::UnknownOperation(sequence))
93 }
94 pub fn ordered_nodes(
98 &self,
99 ) -> impl ExactSizeIterator<Item = &EffectNodeRecord> + DoubleEndedIterator + '_ {
100 self.order.iter().map(|index| &self.nodes[*index])
101 }
102 #[must_use]
104 pub fn digest(&self) -> ArtifactChecksumRecord {
105 let mut bytes = EvidenceDigest::new(b"ic-backup/effect-graph/v1\0");
106 append_count(&mut bytes, self.nodes.len());
107 for node in &self.nodes {
108 bytes.append(&node.operation_sequence().to_be_bytes());
109 append_count(&mut bytes, node.depends_on().len());
110 for dependency in node.depends_on() {
111 bytes.append(&dependency.to_be_bytes());
112 }
113 }
114 bytes.finish()
115 }
116}
117
118#[expect(
119 clippy::cast_possible_truncation,
120 reason = "validated operation/dependency counts are at most 8192"
121)]
122fn append_count(bytes: &mut EvidenceDigest, count: usize) {
123 bytes.append(&(count as u32).to_be_bytes());
124}
125
126fn topological_order(nodes: &[EffectNodeRecord]) -> Result<Vec<usize>, EffectGraphError> {
127 let indices: BTreeMap<_, _> = nodes
128 .iter()
129 .enumerate()
130 .map(|(index, node)| (node.operation_sequence(), index))
131 .collect();
132 let mut dependents = vec![Vec::new(); nodes.len()];
133 let mut remaining: Vec<_> = nodes.iter().map(|node| node.depends_on().len()).collect();
134 for (index, node) in nodes.iter().enumerate() {
135 for dependency in node.depends_on() {
136 let parent = indices
137 .get(dependency)
138 .ok_or(EffectGraphError::MissingDependency {
139 operation_sequence: node.operation_sequence(),
140 dependency: *dependency,
141 })?;
142 dependents[*parent].push(index);
143 }
144 }
145 let mut ready: BTreeSet<_> = remaining
146 .iter()
147 .enumerate()
148 .filter_map(|(index, count)| (*count == 0).then_some(index))
149 .collect();
150 let mut order = Vec::with_capacity(nodes.len());
151 while let Some(index) = ready.pop_first() {
152 order.push(index);
153 for dependent in &dependents[index] {
154 remaining[*dependent] -= 1;
155 if remaining[*dependent] == 0 {
156 ready.insert(*dependent);
157 }
158 }
159 }
160 if order.len() != nodes.len() {
161 return Err(EffectGraphError::Cycle);
162 }
163 Ok(order)
164}
165
166fn add_edges(current: usize, additional: usize) -> Result<usize, EffectGraphError> {
167 current
168 .checked_add(additional)
169 .filter(|total| *total <= MAX_EFFECT_EDGES)
170 .ok_or(EffectGraphError::TooManyEdges)
171}
172
173fn bounded_nodes<'de, D: Deserializer<'de>>(
174 deserializer: D,
175) -> Result<Vec<EffectNodeRecord>, D::Error> {
176 let mut edges = 0;
177 bounded_sequence::deserialize_checked::<D, EffectNodeRecord, _, _, MAX_EFFECT_OPERATIONS>(
178 deserializer,
179 "bounded explicit effect nodes and total edges",
180 EffectGraphError::TooManyOperations,
181 |node| {
182 edges = add_edges(edges, node.depends_on().len())?;
183 Ok::<(), EffectGraphError>(())
184 },
185 )
186}
187
188#[derive(Debug, Error)]
190pub enum EffectGraphError {
191 #[error("unsupported effect graph version {0}")]
193 UnsupportedVersion(u16),
194 #[error("effect graph contains no operations")]
196 EmptyGraph,
197 #[error("effect graph exceeds {MAX_EFFECT_OPERATIONS} operations")]
199 TooManyOperations,
200 #[error("effect node exceeds {MAX_EFFECT_DEPENDENCIES} dependencies")]
202 TooManyDependencies,
203 #[error("effect graph exceeds {MAX_EFFECT_EDGES} edges")]
205 TooManyEdges,
206 #[error("duplicate effect operation {0}")]
208 DuplicateOperation(u64),
209 #[error("unknown effect operation {0}")]
211 UnknownOperation(u64),
212 #[error("effect operation {operation_sequence} repeats dependency {dependency}")]
214 DuplicateDependency {
215 operation_sequence: u64,
217 dependency: u64,
219 },
220 #[error("effect operation {operation_sequence} has absent dependency {dependency}")]
222 MissingDependency {
223 operation_sequence: u64,
225 dependency: u64,
227 },
228 #[error("effect graph contains a dependency cycle")]
230 Cycle,
231}
232
233#[cfg(test)]
234mod tests;