mod node;
pub use node::{EffectNodeRecord, EffectNodeRequest, MAX_EFFECT_DEPENDENCIES};
use crate::model::artifacts::ArtifactChecksumRecord;
use serde::{Deserialize, Deserializer, Serialize, de};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use thiserror::Error;
pub const MAX_EFFECT_OPERATIONS: usize = 8192;
pub const MAX_EFFECT_EDGES: usize = 65536;
pub const MAX_EFFECT_GRAPH_BYTES: u64 = 1024 * 1024;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "GraphFields")]
pub struct EffectGraphRecord {
version: u16,
nodes: Vec<EffectNodeRecord>,
#[serde(skip)]
order: Vec<usize>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct GraphFields {
version: u16,
#[serde(deserialize_with = "bounded_nodes")]
nodes: Vec<EffectNodeRecord>,
}
impl TryFrom<GraphFields> for EffectGraphRecord {
type Error = EffectGraphError;
fn try_from(fields: GraphFields) -> Result<Self, Self::Error> {
if fields.version != 1 {
return Err(EffectGraphError::UnsupportedVersion(fields.version));
}
Self::new(fields.nodes)
}
}
impl EffectGraphRecord {
pub fn new(mut nodes: Vec<EffectNodeRecord>) -> Result<Self, EffectGraphError> {
if nodes.is_empty() {
return Err(EffectGraphError::EmptyGraph);
}
if nodes.len() > MAX_EFFECT_OPERATIONS {
return Err(EffectGraphError::TooManyOperations);
}
nodes.sort_by_key(EffectNodeRecord::operation_sequence);
for pair in nodes.windows(2) {
if pair[0].operation_sequence() == pair[1].operation_sequence() {
return Err(EffectGraphError::DuplicateOperation(
pair[0].operation_sequence(),
));
}
}
let mut edges = 0;
for node in &nodes {
edges = add_edges(edges, node.depends_on().len())?;
}
let order = topological_order(&nodes)?;
Ok(Self {
version: 1,
nodes,
order,
})
}
#[must_use]
pub fn nodes(&self) -> &[EffectNodeRecord] {
&self.nodes
}
pub fn node(&self, sequence: u64) -> Result<&EffectNodeRecord, EffectGraphError> {
self.nodes
.binary_search_by_key(&sequence, EffectNodeRecord::operation_sequence)
.map(|index| &self.nodes[index])
.map_err(|_| EffectGraphError::UnknownOperation(sequence))
}
pub fn ordered_nodes(
&self,
) -> impl ExactSizeIterator<Item = &EffectNodeRecord> + DoubleEndedIterator + '_ {
self.order.iter().map(|index| &self.nodes[*index])
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/effect-graph/v1\0".to_vec();
append_count(&mut bytes, self.nodes.len());
for node in &self.nodes {
bytes.extend_from_slice(&node.operation_sequence().to_be_bytes());
append_count(&mut bytes, node.depends_on().len());
for dependency in node.depends_on() {
bytes.extend_from_slice(&dependency.to_be_bytes());
}
}
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "validated operation/dependency counts are at most 8192"
)]
fn append_count(bytes: &mut Vec<u8>, count: usize) {
bytes.extend_from_slice(&(count as u32).to_be_bytes());
}
fn topological_order(nodes: &[EffectNodeRecord]) -> Result<Vec<usize>, EffectGraphError> {
let indices: BTreeMap<_, _> = nodes
.iter()
.enumerate()
.map(|(index, node)| (node.operation_sequence(), index))
.collect();
let mut dependents = vec![Vec::new(); nodes.len()];
let mut remaining: Vec<_> = nodes.iter().map(|node| node.depends_on().len()).collect();
for (index, node) in nodes.iter().enumerate() {
for dependency in node.depends_on() {
let parent = indices
.get(dependency)
.ok_or(EffectGraphError::MissingDependency {
operation_sequence: node.operation_sequence(),
dependency: *dependency,
})?;
dependents[*parent].push(index);
}
}
let mut ready: BTreeSet<_> = remaining
.iter()
.enumerate()
.filter_map(|(index, count)| (*count == 0).then_some(index))
.collect();
let mut order = Vec::with_capacity(nodes.len());
while let Some(index) = ready.pop_first() {
order.push(index);
for dependent in &dependents[index] {
remaining[*dependent] -= 1;
if remaining[*dependent] == 0 {
ready.insert(*dependent);
}
}
}
if order.len() != nodes.len() {
return Err(EffectGraphError::Cycle);
}
Ok(order)
}
fn add_edges(current: usize, additional: usize) -> Result<usize, EffectGraphError> {
current
.checked_add(additional)
.filter(|total| *total <= MAX_EFFECT_EDGES)
.ok_or(EffectGraphError::TooManyEdges)
}
fn bounded_nodes<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<EffectNodeRecord>, D::Error> {
struct NodesVisitor;
impl<'de> de::Visitor<'de> for NodesVisitor {
type Value = Vec<EffectNodeRecord>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("bounded explicit effect nodes and total edges")
}
fn visit_seq<A: de::SeqAccess<'de>>(
self,
mut sequence: A,
) -> Result<Self::Value, A::Error> {
let mut nodes = Vec::new();
let mut edges = 0;
while nodes.len() < MAX_EFFECT_OPERATIONS {
match sequence.next_element::<EffectNodeRecord>()? {
Some(node) => {
edges =
add_edges(edges, node.depends_on().len()).map_err(de::Error::custom)?;
nodes.push(node);
}
None => return Ok(nodes),
}
}
if sequence.next_element::<de::IgnoredAny>()?.is_some() {
return Err(de::Error::custom(EffectGraphError::TooManyOperations));
}
Ok(nodes)
}
}
deserializer.deserialize_seq(NodesVisitor)
}
#[derive(Debug, Error)]
pub enum EffectGraphError {
#[error("unsupported effect graph version {0}")]
UnsupportedVersion(u16),
#[error("effect graph contains no operations")]
EmptyGraph,
#[error("effect graph exceeds {MAX_EFFECT_OPERATIONS} operations")]
TooManyOperations,
#[error("effect node exceeds {MAX_EFFECT_DEPENDENCIES} dependencies")]
TooManyDependencies,
#[error("effect graph exceeds {MAX_EFFECT_EDGES} edges")]
TooManyEdges,
#[error("duplicate effect operation {0}")]
DuplicateOperation(u64),
#[error("unknown effect operation {0}")]
UnknownOperation(u64),
#[error("effect operation {operation_sequence} repeats dependency {dependency}")]
DuplicateDependency {
operation_sequence: u64,
dependency: u64,
},
#[error("effect operation {operation_sequence} has absent dependency {dependency}")]
MissingDependency {
operation_sequence: u64,
dependency: u64,
},
#[error("effect graph contains a dependency cycle")]
Cycle,
}
#[cfg(test)]
mod tests;