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