clawft_kernel/workload_ctl/
plane_state.rs1use std::io::Write;
15use std::path::{Path, PathBuf};
16
17use serde::{Deserialize, Serialize};
18
19use super::plane::{PlacementControlPlane, PlacementRecord, PlaneError, TargetInfo};
20use super::plane_lifecycle::ControllerLife;
21use super::plane_seed::SeedEntry;
22
23const STATE_VERSION: u32 = 1;
24const MAX_STATE_BYTES: u64 = 8 * 1024 * 1024;
25
26#[derive(Debug, Default, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28struct PlaneState {
29 version: u32,
30 targets: Vec<TargetInfo>,
31 placements: Vec<PlacementRecord>,
32 unsettled: Vec<PlacementRecord>,
33 #[serde(default)]
35 seed_instances: Vec<SeedEntry>,
36 #[serde(default)]
38 lives: Vec<(String, ControllerLife)>,
39}
40
41fn read_state(path: &Path) -> Result<Option<PlaneState>, PlaneError> {
42 let meta = match std::fs::symlink_metadata(path) {
43 Err(_) => return Ok(None),
44 Ok(m) => m,
45 };
46 if !meta.file_type().is_file() || meta.len() > MAX_STATE_BYTES {
47 return Err(PlaneError::Invalid(format!(
48 "{} is not a regular state file of at most {MAX_STATE_BYTES} bytes",
49 path.display()
50 )));
51 }
52 let text = std::fs::read_to_string(path).map_err(|e| PlaneError::Invalid(e.to_string()))?;
53 let st: PlaneState = serde_json::from_str(&text)
54 .map_err(|e| PlaneError::Invalid(format!("{}: {e}", path.display())))?;
55 if st.version != STATE_VERSION {
56 return Err(PlaneError::Invalid(format!(
57 "{}: unsupported state version {}",
58 path.display(),
59 st.version
60 )));
61 }
62 Ok(Some(st))
63}
64
65fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
66 use std::os::unix::fs::OpenOptionsExt;
67 let tmp: PathBuf = path.with_extension("json.tmp");
68 {
69 let mut f = std::fs::OpenOptions::new()
70 .create(true)
71 .write(true)
72 .truncate(true)
73 .mode(0o600)
74 .open(&tmp)?;
75 f.write_all(bytes)?;
76 f.sync_all()?;
77 }
78 std::fs::rename(&tmp, path)
79}
80
81impl PlacementControlPlane {
82 pub fn with_state_file(mut self, path: impl Into<PathBuf>) -> Result<Self, PlaneError> {
86 let path = path.into();
87 if let Some(st) = read_state(&path)? {
88 if let Ok(mut t) = self.targets.write() {
89 for mut target in st.targets {
90 target.reachable = false;
91 t.insert(target.node_id.clone(), target);
92 }
93 }
94 if let Ok(mut p) = self.placements.lock() {
95 for r in st.placements {
96 p.insert(r.instance_id.clone(), r);
97 }
98 }
99 if let Ok(mut m) = self.seed_handles.lock() {
100 for e in st.seed_instances {
101 m.insert(e.handle.instance_id.clone(), e);
102 }
103 }
104 if let Ok(mut l) = self.lives.lock() {
105 for (id, life) in st.lives {
106 l.insert(id, life);
107 }
108 }
109 if let Ok(mut u) = self.unsettled.lock() {
110 for r in st.unsettled {
111 u.insert(r.decision_id.clone(), r);
112 }
113 }
114 }
115 self.state_file = Some(path);
116 Ok(self)
117 }
118
119 pub(super) fn persist(&self) {
122 let Some(path) = &self.state_file else {
123 return;
124 };
125 let _guard = self.state_lock.lock();
126 let st = PlaneState {
127 version: STATE_VERSION,
128 targets: self.targets(),
129 placements: self.placements(),
130 unsettled: self.unsettled(),
131 seed_instances: self
132 .seed_handles
133 .lock()
134 .map(|m| m.values().cloned().collect())
135 .unwrap_or_default(),
136 lives: self
137 .lives
138 .lock()
139 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
140 .unwrap_or_default(),
141 };
142 let res = serde_json::to_vec_pretty(&st)
143 .map_err(std::io::Error::other)
144 .and_then(|b| write_atomic(path, &b));
145 if let Err(e) = res {
146 tracing::warn!(path = %path.display(), error = %e, "placement state not saved");
147 }
148 }
149
150 pub fn forget_instances(&self, ids: &[String]) -> usize {
154 let mut n = 0;
155 if let Ok(mut p) = self.placements.lock() {
156 n = ids.iter().filter(|i| p.remove(*i).is_some()).count();
157 }
158 if let Ok(mut h) = self.seed_handles.lock() {
159 for i in ids {
160 h.remove(i);
161 }
162 }
163 if let Ok(mut l) = self.lives.lock() {
164 for i in ids {
165 l.remove(i);
166 }
167 }
168 if n > 0 {
169 self.persist();
170 }
171 n
172 }
173
174 pub(super) async fn ensure_described(&self, node_id: &str) {
177 if self
178 .facts
179 .get(node_id, super::plane::now_ms() / 1000)
180 .is_some()
181 {
182 return;
183 }
184 if let Ok((t, pk)) = self.target(node_id) {
185 let _ = self.add_target_expecting(&t.addr, t.tier, Some(pk)).await;
186 }
187 }
188}