clawft_kernel/workload_ctl/
facts.rs1use clawft_types::placement::engine::{self, Liveness, PlacementFacts};
12use clawft_types::placement::{Capability, TrustTier as FactsTier};
13
14use crate::cluster::{ClusterMembership, NodeState};
15use crate::node_facts::{CachedNodeFacts, NodeFactsCache};
16use crate::workload_governance::NodeTrustTier;
17
18#[derive(Debug, Clone)]
20pub struct LiveNodeFacts {
21 pub cached: CachedNodeFacts,
23 pub liveness: Liveness,
25 caps: Vec<Capability>,
28}
29
30fn route_for(cap: &Capability) -> Option<&'static [&'static str]> {
33 let id = cap.id.as_str();
34 if id == "runtime.native" || id.starts_with("runtime.native.") {
35 Some(&["native"])
36 } else if id == "runtime.container" || id.starts_with("runtime.container.") {
37 Some(&["container", "emulated"])
38 } else {
39 None
40 }
41}
42
43impl LiveNodeFacts {
44 pub fn new(cached: CachedNodeFacts, liveness: Liveness) -> Self {
46 let caps = cached.capabilities().to_vec();
47 Self {
48 cached,
49 liveness,
50 caps,
51 }
52 }
53
54 pub fn restrict_to_routes(&mut self, routes: &[String]) {
59 self.caps.retain(|c| match route_for(c) {
60 None => true,
61 Some(need) => need.iter().any(|r| routes.iter().any(|x| x == r)),
62 });
63 }
64}
65
66pub fn engine_tier(t: FactsTier) -> engine::TrustTier {
68 match t {
69 FactsTier::Discovered => engine::TrustTier::Discovered,
70 FactsTier::Paired => engine::TrustTier::Paired,
71 FactsTier::Pinned => engine::TrustTier::Pinned,
72 }
73}
74
75pub fn governance_tier(t: FactsTier) -> NodeTrustTier {
77 match t {
78 FactsTier::Discovered => NodeTrustTier::Discovered,
79 FactsTier::Paired => NodeTrustTier::Paired,
80 FactsTier::Pinned => NodeTrustTier::Pinned,
81 }
82}
83
84pub fn liveness_of(state: &NodeState) -> Liveness {
86 match state {
87 NodeState::Active => Liveness::Alive,
88 NodeState::Suspect => Liveness::Suspect,
89 NodeState::Unreachable | NodeState::Leaving | NodeState::Left => Liveness::Dead,
90 _ => Liveness::Unknown,
91 }
92}
93
94impl PlacementFacts for LiveNodeFacts {
95 fn node_id(&self) -> &str {
96 self.cached.node_id()
97 }
98
99 fn capabilities(&self) -> &[Capability] {
100 &self.caps
101 }
102
103 fn liveness(&self) -> Liveness {
104 self.liveness
105 }
106
107 fn trust_tier(&self) -> engine::TrustTier {
108 engine_tier(self.cached.trust_tier())
109 }
110
111 fn facts_expire_at_ms(&self) -> Option<u64> {
112 Some(self.cached.facts.expires_at().saturating_mul(1000))
113 }
114
115 fn load(&self) -> Option<f64> {
117 let l = self.cached.load();
118 (l.total > 0).then(|| l.busy as f64 / l.total as f64)
119 }
120}
121
122pub fn placement_view(
126 cache: &NodeFactsCache,
127 now_secs: u64,
128 local: Option<&str>,
129 membership: Option<&ClusterMembership>,
130 contact: &dyn Fn(&str) -> Option<Liveness>,
131) -> Vec<LiveNodeFacts> {
132 cache
133 .list(now_secs)
134 .into_iter()
135 .map(|cached| {
136 let id = cached.node_id().to_string();
137 let liveness = if local == Some(id.as_str()) {
138 Liveness::Alive
139 } else if let Some(p) = membership.and_then(|m| m.get_peer(&id)) {
140 liveness_of(&p.state)
141 } else {
142 contact(&id).unwrap_or(Liveness::Unknown)
143 };
144 LiveNodeFacts::new(cached, liveness)
145 })
146 .collect()
147}