clawft_kernel/workload_ctl/
plane_reconcile.rs1use serde_json::{Value, json};
21
22use super::msg::method;
23use super::plane::{PlacementControlPlane, PlacementRecord};
24use crate::chain;
25
26impl PlacementControlPlane {
27 pub(super) fn remember_unsettled(&self, template: PlacementRecord) {
28 if let Ok(mut u) = self.unsettled.lock() {
29 u.insert(template.decision_id.clone(), template);
30 }
31 self.persist();
32 }
33
34 pub fn unsettled(&self) -> Vec<PlacementRecord> {
36 self.unsettled
37 .lock()
38 .map(|u| u.values().cloned().collect())
39 .unwrap_or_default()
40 }
41
42 pub async fn settle_unsettled(&self) -> usize {
47 let mut adopted = 0;
48 for t in self.unsettled() {
49 let rows = match self.call(&t.node_id, method::STATUS, None, json!({})).await {
50 Ok(Value::Array(rows)) => rows,
51 _ => continue,
52 };
53 let mine: Vec<&Value> = rows
54 .iter()
55 .filter(|r| r["decision_id"].as_str() == Some(t.decision_id.as_str()))
56 .collect();
57 if mine.iter().any(|r| r["in_flight"] == true) {
58 continue;
59 }
60 for iid in mine.iter().filter_map(|r| r["instance_id"].as_str()) {
61 let rec = PlacementRecord {
62 instance_id: iid.to_string(),
63 ..t.clone()
64 };
65 self.chain_event(
66 chain::EVENT_KIND_WORKLOAD_PLACE,
67 json!({ "phase": "adopted", "decision_id": t.decision_id, "node": t.node_id,
68 "variant": t.variant, "instance_id": iid }),
69 );
70 if let Ok(mut p) = self.placements.lock() {
71 p.insert(iid.to_string(), rec);
72 }
73 adopted += 1;
74 }
75 if let Ok(mut u) = self.unsettled.lock() {
76 u.remove(&t.decision_id);
77 }
78 self.persist();
79 }
80 adopted
81 }
82
83 pub(super) async fn reconcile(&self, node: &str, decision_id: &str) -> (bool, String) {
85 let rows = match self.call(node, method::STATUS, None, json!({})).await {
86 Ok(Value::Array(rows)) => rows,
87 Ok(_) => return (false, "not reconciled: unexpected status answer".into()),
88 Err(e) => return (false, format!("not reconciled: {e}")),
89 };
90 let mine: Vec<&Value> = rows
91 .iter()
92 .filter(|r| r["decision_id"].as_str() == Some(decision_id))
93 .collect();
94 if mine.iter().any(|r| r["in_flight"] == true) {
95 return (
96 false,
97 "not reconciled: the target is still handling this decision".into(),
98 );
99 }
100 let mut unloaded = 0usize;
101 for iid in mine.iter().filter_map(|r| r["instance_id"].as_str()) {
102 let body = json!({ "instance_id": iid });
103 if let Err(e) = self
104 .call(node, method::UNLOAD, Some(decision_id.to_string()), body)
105 .await
106 {
107 return (
108 false,
109 format!("not reconciled: unload of {iid} failed ({e})"),
110 );
111 }
112 unloaded += 1;
113 }
114 (
115 true,
116 format!("reconciled: unloaded {unloaded} instance(s) left by the lost answer"),
117 )
118 }
119}