use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::super::PlacementTypeError;
use super::spec::{MAX_LIST, MAX_NAME_LEN, WorkloadSpec};
pub const DEV_FALLBACK_CLASS: &str = "node.class.dev-mac";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "verdict", rename_all = "snake_case")]
pub enum GateVerdict {
Permit,
Deny {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum GateInput {
Uniform {
verdict: GateVerdict,
},
PerNode {
verdicts: BTreeMap<String, GateVerdict>,
},
}
impl GateInput {
pub fn permit_all() -> Self {
GateInput::Uniform {
verdict: GateVerdict::Permit,
}
}
pub fn verdict_for(&self, node_id: &str) -> GateVerdict {
match self {
GateInput::Uniform { verdict } => verdict.clone(),
GateInput::PerNode { verdicts } => {
verdicts.get(node_id).cloned().unwrap_or(GateVerdict::Deny {
reason: "no gate decision for this node".to_string(),
})
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Affinity {
#[serde(default)]
pub prefer: Vec<String>,
#[serde(default)]
pub avoid: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ScoringWeights {
pub native: f64,
pub dev_fallback: f64,
pub emulated: f64,
pub dev_fallback_class: String,
pub accel_fit: f64,
pub perf: f64,
pub interactive_perf_multiplier: f64,
pub load: f64,
pub stickiness: f64,
pub sticky: f64,
}
impl Default for ScoringWeights {
fn default() -> Self {
Self {
native: 100.0,
dev_fallback: 20.0,
emulated: 40.0,
dev_fallback_class: DEV_FALLBACK_CLASS.to_string(),
accel_fit: 10.0,
perf: 30.0,
interactive_perf_multiplier: 2.0,
load: 10.0,
stickiness: 15.0,
sticky: 1000.0,
}
}
}
impl ScoringWeights {
fn validate(&self) -> Result<(), PlacementTypeError> {
let all = [
self.native,
self.dev_fallback,
self.emulated,
self.accel_fit,
self.perf,
self.interactive_perf_multiplier,
self.load,
self.stickiness,
self.sticky,
];
if all.iter().any(|w| !w.is_finite() || *w < 0.0) {
return Err(PlacementTypeError::InvalidRequirement(
"scoring weights must be finite and non-negative".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlacementRequest {
pub spec: WorkloadSpec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pin: Option<String>,
#[serde(default)]
pub affinity: Affinity,
#[serde(default)]
pub allow_emulated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_node: Option<String>,
pub gate: GateInput,
#[serde(default)]
pub weights: ScoringWeights,
}
impl PlacementRequest {
pub fn new(spec: WorkloadSpec) -> Self {
Self {
spec,
pin: None,
affinity: Affinity::default(),
allow_emulated: false,
current_node: None,
gate: GateInput::permit_all(),
weights: ScoringWeights::default(),
}
}
pub fn validate(&self) -> Result<(), PlacementTypeError> {
self.spec.validate()?;
self.weights.validate()?;
let ids = self
.pin
.iter()
.chain(self.current_node.iter())
.chain(self.affinity.prefer.iter())
.chain(self.affinity.avoid.iter());
for id in ids {
if id.is_empty() || id.len() > MAX_NAME_LEN || id.chars().any(char::is_control) {
return Err(PlacementTypeError::InvalidRequirement(
"node ids must be 1..=256 printable bytes".into(),
));
}
}
if self.affinity.prefer.len() > MAX_LIST || self.affinity.avoid.len() > MAX_LIST {
return Err(PlacementTypeError::InvalidRequirement(
"too many affinity entries".into(),
));
}
Ok(())
}
}