Skip to main content

stackless_daemon/
state.rs

1//! The daemon's in-memory bookkeeping: the proxy routing table and the
2//! supervision records. Instance processes are not the daemon's
3//! children (§3) — a starting daemon reconciles these records against
4//! observed reality, which is why they also live in the state store's
5//! checkpoint journal; this map is the hot copy.
6
7use std::collections::BTreeMap;
8use std::sync::RwLock;
9
10use stackless_core::process::ProcessStamp;
11use stackless_core::types::{DnsName, ProxyHost, TcpPort};
12
13use crate::rpc::{Route, SupervisedProcess};
14
15#[derive(Debug, Default)]
16pub struct DaemonState {
17    /// host (no port) → local TCP port.
18    routes: RwLock<BTreeMap<ProxyHost, TcpPort>>,
19    /// (instance, service) → process stamp.
20    supervised: RwLock<BTreeMap<(DnsName, DnsName), ProcessStamp>>,
21}
22
23impl DaemonState {
24    pub fn route_set(&self, host: ProxyHost, port: TcpPort) {
25        if let Ok(mut routes) = self.routes.write() {
26            routes.insert(host, port);
27        }
28    }
29
30    pub fn route_delete(&self, host: &ProxyHost) {
31        if let Ok(mut routes) = self.routes.write() {
32            routes.remove(host);
33        }
34    }
35
36    pub fn route_lookup(&self, host: &str) -> Option<TcpPort> {
37        let key = ProxyHost::try_new(host).ok()?;
38        self.routes.read().ok()?.get(&key).copied()
39    }
40
41    pub fn routes(&self) -> Vec<Route> {
42        self.routes
43            .read()
44            .map(|routes| {
45                routes
46                    .iter()
47                    .map(|(host, port)| Route {
48                        host: host.clone(),
49                        port: *port,
50                    })
51                    .collect()
52            })
53            .unwrap_or_default()
54    }
55
56    pub fn supervise(&self, instance: DnsName, service: DnsName, stamp: ProcessStamp) {
57        if let Ok(mut map) = self.supervised.write() {
58            map.insert((instance, service), stamp);
59        }
60    }
61
62    pub fn forget(&self, instance: &str) {
63        let Ok(instance_name) = DnsName::try_new(instance) else {
64            return;
65        };
66        if let Ok(mut map) = self.supervised.write() {
67            map.retain(|(i, _), _| i != &instance_name);
68        }
69        if let Ok(mut routes) = self.routes.write() {
70            routes.retain(|host, _| {
71                host.as_str() != format!("{instance}.localhost")
72                    && !host.as_str().ends_with(&format!(".{instance}.localhost"))
73            });
74        }
75    }
76
77    /// Observed now — supervision is by PID + start time, so a
78    /// recycled PID reads as dead (§3).
79    pub fn instance_processes(&self, instance: &str) -> Vec<SupervisedProcess> {
80        let Ok(instance_name) = DnsName::try_new(instance) else {
81            return Vec::new();
82        };
83        self.supervised
84            .read()
85            .map(|map| {
86                map.iter()
87                    .filter(|((i, _), _)| i == &instance_name)
88                    .map(|((i, s), stamp)| SupervisedProcess {
89                        instance: i.clone(),
90                        service: s.clone(),
91                        pid: stamp.pid,
92                        start_time: stamp.start_time,
93                        alive: stamp.is_alive(),
94                    })
95                    .collect()
96            })
97            .unwrap_or_default()
98    }
99}