mod constraints;
mod decision;
mod explain;
mod facts;
mod request;
mod score;
mod spec;
#[cfg(test)]
mod tests;
use std::collections::BTreeSet;
pub use decision::{
Constraint, Decision, Flag, NodeReport, Placement, PlacementError, Rejection, ScoreBreakdown,
Tier,
};
pub use explain::explain;
pub use facts::{ClusterState, InstanceRecord, Liveness, PlacementFacts, TrustTier, WorkloadRef};
pub use request::{
Affinity, DEV_FALLBACK_CLASS, GateInput, GateVerdict, PlacementRequest, ScoringWeights,
};
pub use spec::{
Better, Execution, ExecutionVariant, LatencyClass, PerfTarget, PlacementPolicy, Preference,
WorkloadRequirements, WorkloadSpec,
};
use constraints::{Eligible, check_node};
pub fn place<F: PlacementFacts>(
request: &PlacementRequest,
facts: &[F],
cluster: &ClusterState,
) -> Result<Decision, PlacementError> {
request.validate()?;
let mut seen = BTreeSet::new();
if let Some(dup) = facts
.iter()
.map(|f| f.node_id())
.find(|id| !seen.insert(*id))
{
return Err(PlacementError::DuplicateNode(dup.to_string()));
}
if let Some(pin) = &request.pin
&& !seen.contains(pin.as_str())
{
return Err(PlacementError::UnknownPin(pin.clone()));
}
let revoked_ref = request
.spec
.policy
.revocable_refs
.iter()
.find(|r| cluster.revoked_refs.contains(*r))
.map(String::as_str);
let mut rejected: Vec<NodeReport> = Vec::new();
let mut nodes: Vec<&F> = Vec::new();
let mut survivors: Vec<Eligible> = Vec::new();
for f in facts {
match check_node(request, f, cluster, revoked_ref) {
Ok(e) => {
nodes.push(f);
survivors.push(e);
}
Err(r) => rejected.push(report(f.node_id(), r)),
}
}
let mut eligible: Vec<NodeReport> = Vec::new();
for ((node, e), scored) in nodes
.iter()
.zip(&survivors)
.zip(score::score_all(request, &nodes, &survivors))
{
match scored {
Ok((score, flags)) => eligible.push(NodeReport {
node_id: node.node_id().to_string(),
rejections: vec![],
variant: Some(e.variant.name.clone()),
tier: Some(e.tier),
score: Some(score),
flags,
assigned: assigned_ids(node.capabilities(), e),
}),
Err(r) => rejected.push(report(node.node_id(), vec![r])),
}
}
eligible.sort_by(|a, b| {
tier_rank(a)
.cmp(&tier_rank(b))
.then_with(|| total(b).total_cmp(&total(a)))
.then_with(|| a.node_id.cmp(&b.node_id))
});
rejected.sort_by(|a, b| a.node_id.cmp(&b.node_id));
let chosen = select(request, &mut eligible, &rejected)?;
let placement = chosen.map(|i| {
let c = &eligible[i];
let e = &survivors[nodes
.iter()
.position(|n| n.node_id() == c.node_id)
.expect("eligible node was a survivor")];
Placement {
node_id: c.node_id.clone(),
variant: e.variant.name.clone(),
execution: e.variant.execution,
tier: e.tier,
accelerator: c
.assigned
.iter()
.flatten()
.find(|id| id.starts_with("accel."))
.cloned(),
score: c.score.clone().unwrap_or_default(),
flags: c.flags.clone(),
}
});
eligible.extend(rejected);
Ok(Decision {
kind: request.spec.kind.clone(),
name: request.spec.name.clone(),
allow_emulated: request.allow_emulated,
pin: request.pin.clone(),
placement,
candidates: eligible,
})
}
fn report(node_id: &str, rejections: Vec<Rejection>) -> NodeReport {
NodeReport {
node_id: node_id.to_string(),
rejections,
variant: None,
tier: None,
score: None,
flags: vec![],
assigned: vec![],
}
}
fn tier_rank(r: &NodeReport) -> u8 {
r.tier.map_or(u8::MAX, Tier::rank)
}
fn total(r: &NodeReport) -> f64 {
r.score.as_ref().map_or(0.0, ScoreBreakdown::total)
}
fn assigned_ids(caps: &[super::Capability], e: &Eligible) -> Vec<Vec<String>> {
e.assignment
.iter()
.map(|v| v.iter().map(|&i| caps[i].id.to_string()).collect())
.collect()
}
fn select(
request: &PlacementRequest,
eligible: &mut [NodeReport],
rejected: &[NodeReport],
) -> Result<Option<usize>, PlacementError> {
if let Some(pin) = &request.pin {
if let Some(r) = rejected.iter().find(|r| &r.node_id == pin) {
return Err(PlacementError::PinIneligible {
node: pin.clone(),
rejections: r.rejections.clone(),
});
}
let i = eligible
.iter()
.position(|c| &c.node_id == pin)
.expect("known pin is eligible or rejected");
add_flag(&mut eligible[i], Flag::Pinned);
return Ok(Some(i));
}
let best = eligible.first().map(tier_rank);
let pool: Vec<usize> = (0..eligible.len())
.filter(|&i| Some(tier_rank(&eligible[i])) == best)
.collect();
let base = pool.first().copied();
let aff = &request.affinity;
let preferred: Vec<usize> = pool
.iter()
.copied()
.filter(|&i| aff.prefer.contains(&eligible[i].node_id))
.collect();
let not_avoided: Vec<usize> = pool
.iter()
.copied()
.filter(|&i| !aff.avoid.contains(&eligible[i].node_id))
.collect();
let chosen = preferred
.first()
.or(not_avoided.first())
.or(pool.first())
.copied();
if let Some(i) = chosen
&& Some(i) != base
{
add_flag(&mut eligible[i], Flag::Affinity);
}
Ok(chosen)
}
fn add_flag(r: &mut NodeReport, f: Flag) {
if !r.flags.contains(&f) {
r.flags.push(f);
r.flags.sort_unstable();
}
}