1use std::collections::{BTreeMap, HashMap};
18use std::sync::atomic::{AtomicU32, Ordering};
19use std::sync::{Arc, OnceLock};
20use std::time::{Duration, Instant};
21
22use crate::backend::agent_state::{AgentState, SinceTracker};
23use crate::backend::fleet_feed::{AgentKind, FleetObservation};
24use crate::backend::reactive::registry::AgentEntry;
25use crate::backend::storage::store::Store;
26
27pub const MAX_CHANNELS_RUNNING: u32 = 99;
29
30const CHANNELS_REREAD: Duration = Duration::from_secs(5);
34
35static LATEST_CHANNELS_RUNNING: AtomicU32 = AtomicU32::new(0);
37
38static INSTALL_ID: OnceLock<String> = OnceLock::new();
39
40pub fn install_id() -> Option<String> {
45 if let Some(id) = INSTALL_ID.get() {
46 return Some(id.clone());
47 }
48 let wan = crate::backend::storage::wan_identity::global()?;
49 let instance = wan
50 .instance_ensure(&crate::backend::reactive::registry::local_host_label())
51 .ok()?;
52 Some(INSTALL_ID.get_or_init(|| instance.instance_id).clone())
53}
54
55pub fn count_channels_running(entries: &[AgentEntry], own_channel: &str, own_url: &str, now_ms: u64) -> u32 {
60 let others = crate::backend::swarm_remote::other_channels(entries, &Default::default(), own_channel, own_url, now_ms).len();
61 u32::try_from(others)
62 .unwrap_or(u32::MAX)
63 .saturating_add(1)
64 .clamp(1, MAX_CHANNELS_RUNNING)
65}
66
67pub fn latest_channels_running() -> Option<u32> {
70 match LATEST_CHANNELS_RUNNING.load(Ordering::Relaxed) {
71 0 => None,
72 n => Some(n),
73 }
74}
75
76pub fn kinds_by_name<'a>(
79 agents: impl IntoIterator<Item = (&'a str, &'a str)>,
80 kind_of_block: impl Fn(&str) -> Option<AgentKind>,
81) -> BTreeMap<String, AgentKind> {
82 agents
83 .into_iter()
84 .filter_map(|(name, block_id)| kind_of_block(block_id).map(|k| (name.to_string(), k)))
85 .collect()
86}
87
88type AgentList = Box<dyn Fn() -> Vec<(String, String)> + Send + Sync>;
89type KindOfBlock = Box<dyn Fn(&str) -> Option<AgentKind> + Send + Sync>;
90type StateOfBlock = Box<dyn Fn(&str) -> Option<AgentState> + Send + Sync>;
91type LineOfBlock = Box<dyn Fn(&str) -> Option<String> + Send + Sync>;
92type RegistryRead = Box<dyn Fn() -> Vec<AgentEntry> + Send + Sync>;
93
94pub struct FleetSource {
96 agents: AgentList,
98 kind_of_block: KindOfBlock,
99 state_of_block: StateOfBlock,
102 line_of_block: LineOfBlock,
105 since: &'static SinceTracker,
107 shared_registry: RegistryRead,
108 own_channel: String,
109 own_url: String,
110 kinds: parking_lot::Mutex<HashMap<String, AgentKind>>,
113 channels: parking_lot::Mutex<Option<(Instant, u32)>>,
114}
115
116impl FleetSource {
117 pub fn live(
120 handler: &'static crate::backend::reactive::handler::ReactiveHandler,
121 mstore: Arc<Store>,
122 config: Arc<crate::backend::wconfig::ConfigState>,
123 own_url: String,
124 ) -> Self {
125 let state_store = Arc::clone(&mstore);
126 let line_store = Arc::clone(&mstore);
127 Self::new(
128 Box::new(move || {
129 handler
130 .list_agents()
131 .into_iter()
132 .map(|a| (a.agent_id, a.block_id))
133 .collect()
134 }),
135 Box::new(move |block_id| crate::backend::operator_config_seed::agent_kind_of_block(&mstore, block_id)),
136 Box::new(move |block_id| crate::backend::agent_state::current_state(&state_store, block_id)),
137 Box::new(move |block_id| {
138 crate::backend::agent_line::sharing_on(&config.get_full_config().settings)
139 .then(|| crate::backend::agent_line::agent_line_of_block(&line_store, block_id))
140 .flatten()
141 }),
142 crate::backend::agent_state::global_tracker(),
143 Box::new(|| {
144 crate::registry::resolve_shared_reactive_dir()
145 .map(|dir| crate::backend::reactive::registry::list_all_shared(&dir))
146 .unwrap_or_default()
147 }),
148 crate::backend::reactive::registry::local_channel_id(),
149 own_url,
150 )
151 }
152
153 fn new(
154 agents: AgentList,
155 kind_of_block: KindOfBlock,
156 state_of_block: StateOfBlock,
157 line_of_block: LineOfBlock,
158 since: &'static SinceTracker,
159 shared_registry: RegistryRead,
160 own_channel: String,
161 own_url: String,
162 ) -> Self {
163 Self {
164 agents,
165 kind_of_block,
166 state_of_block,
167 line_of_block,
168 since,
169 shared_registry,
170 own_channel,
171 own_url,
172 kinds: parking_lot::Mutex::new(HashMap::new()),
173 channels: parking_lot::Mutex::new(None),
174 }
175 }
176
177 pub fn observe(&self) -> FleetObservation {
179 let listed = (self.agents)();
180 self.since.retain(|block_id| listed.iter().any(|(_, b)| b == block_id));
181 let now_ms = agentmux_common::time::now_ms_u64();
182 let agent_status = listed
183 .iter()
184 .filter_map(|(name, block_id)| {
185 let status = self.since.record(block_id, (self.state_of_block)(block_id), now_ms)?;
186 Some((name.clone(), status))
187 })
188 .collect();
189 let agent_lines = listed
190 .iter()
191 .filter_map(|(name, block_id)| Some((name.clone(), (self.line_of_block)(block_id)?)))
192 .collect();
193 let agents = {
194 let mut cache = self.kinds.lock();
195 cache.retain(|block_id, _| listed.iter().any(|(_, b)| b == block_id));
196 listed
197 .into_iter()
198 .map(|(name, block_id)| {
199 let kind = match cache.get(&block_id) {
200 Some(kind) => Some(*kind),
201 None => {
202 let kind = (self.kind_of_block)(&block_id);
203 if let Some(kind) = kind {
204 cache.insert(block_id, kind);
205 }
206 kind
207 }
208 };
209 (name, kind)
210 })
211 .collect()
212 };
213 FleetObservation {
214 agents,
215 channels_running: self.channels_running(),
216 agent_status,
217 agent_lines,
218 }
219 }
220
221 fn channels_running(&self) -> u32 {
222 let mut last = self.channels.lock();
223 if let Some((at, n)) = *last {
224 if at.elapsed() < CHANNELS_REREAD {
225 return n;
226 }
227 }
228 let n = count_channels_running(
229 &(self.shared_registry)(),
230 &self.own_channel,
231 &self.own_url,
232 agentmux_common::time::now_ms_u64(),
233 );
234 *last = Some((Instant::now(), n));
235 LATEST_CHANNELS_RUNNING.store(n, Ordering::Relaxed);
236 n
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use std::sync::atomic::AtomicUsize;
244
245 const NOW: u64 = 1_000_000_000;
246
247 fn entry(name: &str, channel: &str, age_ms: u64) -> AgentEntry {
248 AgentEntry {
249 agent_id: name.to_string(),
250 local_url: format!("http://127.0.0.1/{channel}"),
251 block_id: format!("block-{name}"),
252 pid: 1,
253 updated_at: NOW - age_ms,
254 auth_key: String::new(),
255 channel: channel.to_string(),
256 registration_nonce: 0,
257 jekt_public_key: String::new(),
258 uid: String::new(),
259 uid_public_key: String::new(),
260 }
261 }
262
263 #[test]
264 fn channels_running_counts_this_channel_and_live_others() {
265 assert_eq!(count_channels_running(&[], "stable", "", NOW), 1);
266 let entries = [
267 entry("a", "stable", 0),
268 entry("b", "dev", 0),
269 entry("c", "dev", 0),
270 entry("d", "local-main", 1_000),
271 entry("e", "crashed", crate::backend::swarm_remote::HIDE_AFTER_MS + 1),
272 ];
273 assert_eq!(
274 count_channels_running(&entries, "stable", "", NOW),
275 3,
276 "this one, dev and local-main; not the hidden one"
277 );
278 let many: Vec<_> = (0..150).map(|i| entry("x", &format!("ch-{i}"), 0)).collect();
279 assert_eq!(count_channels_running(&many, "stable", "", NOW), MAX_CHANNELS_RUNNING);
280 }
281
282 #[test]
283 fn kinds_leave_out_unreadable_blocks() {
284 let kinds = kinds_by_name([("AgentX", "b1"), ("Camper", "b2"), ("Ghost", "b3")], |b| match b {
285 "b1" => Some("container"),
286 "b2" => Some("host"),
287 _ => None,
288 });
289 assert_eq!(
290 kinds,
291 BTreeMap::from([("AgentX".to_string(), "container"), ("Camper".to_string(), "host")])
292 );
293 }
294
295 #[test]
296 fn a_block_is_read_once_while_listed_and_an_unreadable_one_is_retried() {
297 let reads = Arc::new(AtomicUsize::new(0));
298 let counted = reads.clone();
299 let ready = Arc::new(std::sync::atomic::AtomicBool::new(false));
300 let ready_read = ready.clone();
301 let source = FleetSource::new(
302 Box::new(|| vec![("AgentX".into(), "b1".into()), ("Late".into(), "b2".into())]),
303 Box::new(move |b| {
304 counted.fetch_add(1, Ordering::SeqCst);
305 match b {
306 "b1" => Some("container"),
307 _ if ready_read.load(Ordering::SeqCst) => Some("host"),
308 _ => None,
309 }
310 }),
311 Box::new(|_| None),
312 Box::new(|_| None),
313 tracker(),
314 Box::new(Vec::new),
315 "stable".into(),
316 String::new(),
317 );
318 let first = source.observe();
319 assert_eq!(
320 first.agents,
321 vec![("AgentX".to_string(), Some("container")), ("Late".to_string(), None)]
322 );
323 assert_eq!(first.channels_running, 1);
324 assert_eq!(reads.load(Ordering::SeqCst), 2);
325 source.observe();
326 assert_eq!(reads.load(Ordering::SeqCst), 3, "b1 cached, b2 retried");
327 ready.store(true, Ordering::SeqCst);
328 assert_eq!(source.observe().agents[1], ("Late".to_string(), Some("host")));
329 source.observe();
330 assert_eq!(reads.load(Ordering::SeqCst), 4, "both cached now");
331 }
332
333 fn tracker() -> &'static SinceTracker {
335 Box::leak(Box::new(SinceTracker::default()))
336 }
337
338 #[test]
339 fn states_are_read_every_look_and_keep_their_start_while_unchanged() {
340 let states = Arc::new(parking_lot::Mutex::new(HashMap::from([
341 ("b1".to_string(), AgentState::Working),
342 ("b2".to_string(), AgentState::Idle),
343 ])));
344 let read = states.clone();
345 let listed = Arc::new(parking_lot::Mutex::new(vec![
346 ("AgentX".to_string(), "b1".to_string()),
347 ("Camper".to_string(), "b2".to_string()),
348 ("Term".to_string(), "b3".to_string()),
349 ]));
350 let list = listed.clone();
351 let since = tracker();
352 let source = FleetSource::new(
353 Box::new(move || list.lock().clone()),
354 Box::new(|_| Some("host")),
355 Box::new(move |b| read.lock().get(b).copied()),
356 Box::new(|_| None),
357 since,
358 Box::new(Vec::new),
359 "stable".into(),
360 String::new(),
361 );
362 let first = source.observe().agent_status;
363 assert_eq!(
364 first.keys().collect::<Vec<_>>(),
365 ["AgentX", "Camper"],
366 "the agent with no state is left out"
367 );
368 assert_eq!(first["AgentX"].state, AgentState::Working);
369
370 std::thread::sleep(Duration::from_millis(5));
371 let second = source.observe().agent_status;
372 assert_eq!(second, first, "nothing changed, the starts held");
373
374 states.lock().insert("b1".into(), AgentState::Idle);
375 let third = source.observe().agent_status;
376 assert_eq!(third["AgentX"].state, AgentState::Idle);
377 assert!(third["AgentX"].since_ms > first["AgentX"].since_ms, "a change starts again");
378 assert_eq!(third["Camper"], first["Camper"]);
379
380 listed.lock().remove(0);
381 source.observe();
382 assert_eq!(since.len(), 1, "an agent no longer listed is forgotten");
383 }
384}