use crate::{
config::ComponentTopology,
ids::{
CanisterRole, ComponentInstanceId, ComponentSpecAdmission, ComponentSpecId,
FleetSubnetRootBinding,
},
};
use thiserror::Error as ThisError;
pub struct TopLevelComponentAllocationInput<'a> {
pub operation_id: [u8; 32],
pub component_spec: &'a ComponentSpecId,
pub root: &'a FleetSubnetRootBinding,
pub topology: &'a ComponentTopology,
pub next_allocation_sequence: u64,
pub reserved_component_instances: u32,
pub committed_component_instances: u32,
pub managed_descendants: u32,
pub reserved_spec_instances: u32,
pub committed_spec_instances: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TopLevelComponentAllocationDecision {
pub allocation_sequence: u64,
pub component: ComponentInstanceId,
pub component_spec: ComponentSpecId,
pub spec_hash: [u8; 32],
pub role: CanisterRole,
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub enum ComponentAllocationPolicyError {
#[error("Component allocation operation ID must not be all zeroes")]
EmptyOperationId,
#[error("Component allocation sequence is exhausted")]
AllocationSequenceExhausted,
#[error("Component topology cannot be projected for the protected root admissions")]
InvalidRootTopologyProjection,
#[error("protected root admissions do not reproduce its Component Topology digest")]
RootTopologyDigestMismatch,
#[error("Component Spec '{0}' is not admitted on this Fleet Subnet Root")]
ComponentSpecNotAdmitted(ComponentSpecId),
#[error("Component Spec '{0}' does not exist in the protected root topology")]
ComponentSpecUnknown(ComponentSpecId),
#[error("Component Spec '{0}' admission hash differs from its protected topology")]
ComponentSpecHashMismatch(ComponentSpecId),
#[error("root Component instance count overflowed")]
ComponentCountOverflow,
#[error("root Component instance capacity is exhausted")]
ComponentCapacityExhausted,
#[error("Component Spec '{0}' root-local instance count overflowed")]
ComponentSpecCountOverflow(ComponentSpecId),
#[error("Component Spec '{0}' root-local admission is exhausted")]
ComponentSpecCapacityExhausted(ComponentSpecId),
#[error("root managed-Canister count overflowed")]
ManagedCanisterCountOverflow,
#[error("root managed-Canister capacity is exhausted")]
ManagedCanisterCapacityExhausted,
}
pub fn reserve_top_level_component(
input: TopLevelComponentAllocationInput<'_>,
) -> Result<TopLevelComponentAllocationDecision, ComponentAllocationPolicyError> {
if input.operation_id == [0; 32] {
return Err(ComponentAllocationPolicyError::EmptyOperationId);
}
input
.next_allocation_sequence
.checked_add(1)
.ok_or(ComponentAllocationPolicyError::AllocationSequenceExhausted)?;
let projected = input
.topology
.project_for_admissions(&input.root.component_admissions)
.map_err(|_| ComponentAllocationPolicyError::InvalidRootTopologyProjection)?;
let projected_digest = projected
.digest()
.map_err(|_| ComponentAllocationPolicyError::InvalidRootTopologyProjection)?;
if projected_digest != input.root.component_topology_digest {
return Err(ComponentAllocationPolicyError::RootTopologyDigestMismatch);
}
let admission = admission(input.root, input.component_spec)?;
let spec = projected.get(input.component_spec).ok_or_else(|| {
ComponentAllocationPolicyError::ComponentSpecUnknown(input.component_spec.clone())
})?;
if admission.spec_hash != spec.spec_hash {
return Err(ComponentAllocationPolicyError::ComponentSpecHashMismatch(
input.component_spec.clone(),
));
}
let allocated_components = input
.reserved_component_instances
.checked_add(input.committed_component_instances)
.ok_or(ComponentAllocationPolicyError::ComponentCountOverflow)?;
if allocated_components >= input.root.limits.maximum_component_instances {
return Err(ComponentAllocationPolicyError::ComponentCapacityExhausted);
}
let spec_instances = input
.reserved_spec_instances
.checked_add(input.committed_spec_instances)
.ok_or_else(|| {
ComponentAllocationPolicyError::ComponentSpecCountOverflow(input.component_spec.clone())
})?;
if spec_instances >= admission.maximum_root_instances {
return Err(
ComponentAllocationPolicyError::ComponentSpecCapacityExhausted(
input.component_spec.clone(),
),
);
}
let managed_canisters = 1_u32
.checked_add(allocated_components)
.and_then(|count| count.checked_add(input.managed_descendants))
.ok_or(ComponentAllocationPolicyError::ManagedCanisterCountOverflow)?;
if managed_canisters >= input.root.limits.maximum_managed_canisters {
return Err(ComponentAllocationPolicyError::ManagedCanisterCapacityExhausted);
}
let allocation_sequence = input.next_allocation_sequence;
Ok(TopLevelComponentAllocationDecision {
allocation_sequence,
component: ComponentInstanceId::from_root_allocation(
input.root.authority.binding.fleet.fleet,
input.root.authority.epoch,
input.root.fleet_subnet_root,
allocation_sequence,
),
component_spec: spec.component_spec.clone(),
spec_hash: spec.spec_hash,
role: spec.component_role.clone(),
})
}
fn admission<'a>(
root: &'a FleetSubnetRootBinding,
component_spec: &ComponentSpecId,
) -> Result<&'a ComponentSpecAdmission, ComponentAllocationPolicyError> {
root.component_admissions
.binary_search_by(|candidate| candidate.component_spec.cmp(component_spec))
.ok()
.map(|index| &root.component_admissions[index])
.ok_or_else(|| {
ComponentAllocationPolicyError::ComponentSpecNotAdmitted(component_spec.clone())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cdk::types::Cycles,
config::{
ComponentTopology,
schema::{ConfigModel, Validate},
},
ids::{
AppId, CanonicalNetworkId, ComponentSpecAdmission, CyclesFundingBudget, FleetBinding,
FleetCoordinatorBinding, FleetId, FleetKey, FleetRegistryAuthority,
FleetSubnetRootLimits, SubnetId,
},
};
const CONFIG: &str = r#"
[app]
name = "toko"
[roles.root]
package = "root"
kind = "root"
[roles.project_hub]
package = "project_hub"
kind = "canister"
[component_specs.projects]
component_role = "project_hub"
maximum_instances = 4
"#;
#[test]
fn reservation_derives_exact_root_local_identity() {
let (topology, root, component_spec) = fixture();
let decision = reserve_top_level_component(input(&topology, &root, &component_spec))
.expect("reservation decision");
assert_eq!(decision.allocation_sequence, 1);
assert_eq!(decision.component_spec, component_spec);
assert_eq!(decision.spec_hash, root.component_admissions[0].spec_hash);
assert_eq!(decision.role, CanisterRole::new("project_hub"));
assert_eq!(
decision.component,
ComponentInstanceId::from_root_allocation(
root.authority.binding.fleet.fleet,
root.authority.epoch,
root.fleet_subnet_root,
1,
)
);
}
#[test]
fn reservation_rejects_empty_operation_and_exhausted_sequence() {
let (topology, root, component_spec) = fixture();
let mut request = input(&topology, &root, &component_spec);
request.operation_id = [0; 32];
assert_eq!(
reserve_top_level_component(request),
Err(ComponentAllocationPolicyError::EmptyOperationId)
);
let mut request = input(&topology, &root, &component_spec);
request.next_allocation_sequence = u64::MAX;
assert_eq!(
reserve_top_level_component(request),
Err(ComponentAllocationPolicyError::AllocationSequenceExhausted)
);
}
#[test]
fn reservation_enforces_spec_component_and_managed_canister_capacity() {
let (topology, mut root, component_spec) = fixture();
let mut request = input(&topology, &root, &component_spec);
request.reserved_spec_instances = 2;
assert_eq!(
reserve_top_level_component(request),
Err(
ComponentAllocationPolicyError::ComponentSpecCapacityExhausted(
component_spec.clone()
)
)
);
root.limits.maximum_component_instances = 1;
let mut request = input(&topology, &root, &component_spec);
request.committed_component_instances = 1;
assert_eq!(
reserve_top_level_component(request),
Err(ComponentAllocationPolicyError::ComponentCapacityExhausted)
);
root.limits.maximum_component_instances = 10;
root.limits.maximum_managed_canisters = 2;
let mut request = input(&topology, &root, &component_spec);
request.managed_descendants = 1;
assert_eq!(
reserve_top_level_component(request),
Err(ComponentAllocationPolicyError::ManagedCanisterCapacityExhausted)
);
}
#[test]
fn reservation_rejects_unknown_and_hash_drifted_specs() {
let (topology, mut root, component_spec) = fixture();
let unknown = "users".parse().expect("Component Spec ID");
assert_eq!(
reserve_top_level_component(input(&topology, &root, &unknown)),
Err(ComponentAllocationPolicyError::ComponentSpecNotAdmitted(
unknown
))
);
let topology_digest = root.component_topology_digest;
root.component_topology_digest = crate::ids::ComponentTopologyDigest::from_bytes([98; 32]);
assert_eq!(
reserve_top_level_component(input(&topology, &root, &component_spec)),
Err(ComponentAllocationPolicyError::RootTopologyDigestMismatch)
);
root.component_topology_digest = topology_digest;
root.component_admissions[0].spec_hash = [99; 32];
assert_eq!(
reserve_top_level_component(input(&topology, &root, &component_spec)),
Err(ComponentAllocationPolicyError::InvalidRootTopologyProjection)
);
}
fn fixture() -> (ComponentTopology, FleetSubnetRootBinding, ComponentSpecId) {
let config: ConfigModel = toml::from_str(CONFIG).expect("config");
config.validate().expect("valid config");
let topology = ComponentTopology::compile(&config).expect("topology");
let component_spec = "projects".parse().expect("Component Spec ID");
let spec = topology.get(&component_spec).expect("projects Spec");
let admissions = vec![ComponentSpecAdmission {
component_spec: component_spec.clone(),
spec_hash: spec.spec_hash,
maximum_root_instances: 2,
}];
let projected = topology
.project_for_admissions(&admissions)
.expect("root topology projection");
let root = FleetSubnetRootBinding {
authority: FleetRegistryAuthority {
binding: FleetCoordinatorBinding {
fleet: FleetBinding {
fleet: FleetKey {
canonical_network_id: CanonicalNetworkId::public_ic(),
fleet_id: FleetId::from_generated_bytes([1; 32]),
},
app: AppId::from("toko"),
},
coordinator_subnet: SubnetId::from_principal(candid::Principal::from_slice(
&[2; 29],
)),
coordinator: candid::Principal::from_slice(&[3; 29]),
},
epoch: 1,
},
placement_subnet: SubnetId::from_principal(candid::Principal::from_slice(&[4; 29])),
fleet_subnet_root: candid::Principal::from_slice(&[5; 29]),
component_admissions: admissions,
component_topology_digest: projected.digest().expect("root topology digest"),
limits: FleetSubnetRootLimits {
maximum_component_instances: 4,
maximum_managed_canisters: 20,
maximum_registry_bytes: 1_000_000,
maximum_wasm_store_bytes: 1_000_000,
cycles_funding: CyclesFundingBudget {
window_secs: 3_600,
maximum_cycles: Cycles::new(1_000_000_000_000),
},
},
};
(topology, root, component_spec)
}
fn input<'a>(
topology: &'a ComponentTopology,
root: &'a FleetSubnetRootBinding,
component_spec: &'a ComponentSpecId,
) -> TopLevelComponentAllocationInput<'a> {
TopLevelComponentAllocationInput {
operation_id: [7; 32],
component_spec,
root,
topology,
next_allocation_sequence: 1,
reserved_component_instances: 0,
committed_component_instances: 0,
managed_descendants: 0,
reserved_spec_instances: 0,
committed_spec_instances: 0,
}
}
}