use std::collections::BTreeSet;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostExclusivity {
Shared,
Exclusive,
}
impl Default for HostExclusivity {
fn default() -> Self {
HostExclusivity::Shared
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HardwareNeeds {
pub requires_gpu: bool,
pub requires_large_mem: bool,
pub network_isolated: bool,
pub listing_only: bool,
pub local_debug: bool,
}
impl HardwareNeeds {
pub fn combine(self, other: HardwareNeeds) -> Result<HardwareNeeds, ConstraintConflictError> {
let merged = HardwareNeeds {
requires_gpu: self.requires_gpu || other.requires_gpu,
requires_large_mem: self.requires_large_mem || other.requires_large_mem,
network_isolated: self.network_isolated || other.network_isolated,
listing_only: self.listing_only || other.listing_only,
local_debug: self.local_debug || other.local_debug,
};
if merged.requires_gpu && merged.requires_large_mem {
return Err(ConstraintConflictError("Cannot satisfy both gpu and large-mem hardware needs."));
}
Ok(merged)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstraintConflictError(pub &'static str);
impl fmt::Display for ConstraintConflictError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Constraint conflict: {}", self.0)
}
}
impl std::error::Error for ConstraintConflictError {}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SchedulingProfile {
pub hardware: HardwareNeeds,
pub exclusivity: HostExclusivity,
pub local_resources: BTreeSet<String>,
}
impl SchedulingProfile {
pub fn combine(mut self, other: SchedulingProfile) -> Result<SchedulingProfile, ConstraintConflictError> {
self.hardware = self.hardware.combine(other.hardware)?;
if other.exclusivity == HostExclusivity::Exclusive {
self.exclusivity = HostExclusivity::Exclusive;
}
self.local_resources.extend(other.local_resources);
Ok(self)
}
}
pub fn profile_from_labels(labels: &[String]) -> Result<SchedulingProfile, ConstraintConflictError> {
let mut profile = SchedulingProfile::default();
for l in labels {
let suffix = l.split_once(':').unwrap_or(("", l)).1;
let mut single = SchedulingProfile::default();
match suffix {
"gpu" => single.hardware.requires_gpu = true,
"large-mem" => single.hardware.requires_large_mem = true,
"network-private" => single.hardware.network_isolated = true,
"exclusive" | "serial_global_state" => single.exclusivity = HostExclusivity::Exclusive,
_ => {
if let Some(r) = suffix.strip_prefix("local-resource=") {
single.local_resources.insert(r.to_owned());
}
}
}
profile = profile.combine(single)?;
}
Ok(profile)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gpu_selects_gpu() {
let req = profile_from_labels(&["rust:gpu".to_owned()]).unwrap();
assert!(req.hardware.requires_gpu);
assert!(!req.hardware.requires_large_mem);
assert!(req.local_resources.is_empty());
}
#[test]
fn conflict_gpu_and_large_mem() {
let req = profile_from_labels(&["python:large-mem".to_owned(), "rust:gpu".to_owned()]);
assert!(req.is_err());
}
#[test]
fn local_resources_accumulate() {
let req = profile_from_labels(&[
"rust:local-resource=postgres".to_owned(),
"python:local-resource=redis".to_owned(),
]).unwrap();
let mut expected = BTreeSet::new();
expected.insert("postgres".to_owned());
expected.insert("redis".to_owned());
assert_eq!(req.local_resources, expected);
}
}