1use std::collections::{BTreeMap, BTreeSet};
9use std::time::Instant;
10
11use super::error::ConsoleError;
12use super::records::{
13 Connection, Device, DeviceBox, DeviceKind, Line, LineKind, LineLife, LineState, Link, Mount,
14 Station, StationKind, StationState, TopologyDocument,
15};
16use super::wall::WallBase;
17use crate::server::connection::is_console_subject;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum TopologyChange {
22 Service {
24 subject: String,
26 present: bool,
28 },
29 Channel {
31 name: String,
33 life: LineLife,
35 state: LineState,
37 },
38 ConnectionOpened {
40 id: u64,
42 mount: Mount,
44 participant: Option<String>,
46 },
47 ConnectionClosed {
49 id: u64,
51 },
52 Worker {
55 connection: u64,
57 identity: String,
59 },
60 Flow {
62 connection: u64,
64 line: String,
66 role: FlowRole,
68 present: bool,
70 },
71 Peer {
73 name: String,
75 present: bool,
77 },
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
84pub enum FlowRole {
85 Publishes,
87 Subscribes,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct ThisDevice {
94 pub id: String,
96 pub liminal: String,
98 pub started: Instant,
100 pub depot: bool,
103}
104
105#[derive(Clone, Debug)]
107pub struct TopologyModel {
108 this: ThisDevice,
109 rev: u64,
110 stations: BTreeSet<String>,
111 lines: BTreeMap<String, (LineLife, LineState)>,
112 connections: BTreeMap<u64, (Mount, Option<String>)>,
113 peers: BTreeSet<String>,
114 workers: BTreeMap<u64, String>,
116 flows: BTreeMap<String, BTreeSet<(FlowRole, u64)>>,
118}
119
120impl TopologyModel {
121 #[must_use]
124 pub fn new(
125 this: ThisDevice,
126 stations: impl IntoIterator<Item = String>,
127 lines: impl IntoIterator<Item = (String, LineLife, LineState)>,
128 connections: impl IntoIterator<Item = (u64, Mount, Option<String>)>,
129 peers: impl IntoIterator<Item = String>,
130 ) -> Self {
131 Self {
132 this,
133 rev: 1,
134 stations: stations.into_iter().collect(),
135 lines: lines
136 .into_iter()
137 .filter(|(name, _, _)| !is_console_subject(name))
138 .map(|(name, life, state)| (name, (life, state)))
139 .collect(),
140 connections: connections
141 .into_iter()
142 .map(|(id, mount, participant)| (id, (mount, participant)))
143 .collect(),
144 peers: peers.into_iter().collect(),
145 workers: BTreeMap::new(),
146 flows: BTreeMap::new(),
147 }
148 }
149
150 #[must_use]
152 pub const fn depot(&self) -> bool {
153 self.this.depot
154 }
155
156 #[must_use]
158 pub const fn rev(&self) -> u64 {
159 self.rev
160 }
161
162 pub fn apply(&mut self, change: TopologyChange) -> bool {
167 let changed = match change {
168 TopologyChange::Service { subject, present } => {
169 if present {
170 self.stations.insert(subject)
171 } else {
172 self.stations.remove(&subject)
173 }
174 }
175 TopologyChange::Channel { name, life, state } => {
176 !is_console_subject(&name)
177 && self.lines.insert(name, (life, state)) != Some((life, state))
178 }
179 TopologyChange::ConnectionOpened {
180 id,
181 mount,
182 participant,
183 } => {
184 let entry = (mount, participant);
185 if self.connections.get(&id) == Some(&entry) {
186 false
187 } else {
188 self.connections.insert(id, entry);
189 true
190 }
191 }
192 TopologyChange::ConnectionClosed { id } => {
193 let closed = self.connections.remove(&id).is_some();
194 let worker = self.workers.remove(&id).is_some();
195 for users in self.flows.values_mut() {
196 users.retain(|(_, connection)| *connection != id);
197 }
198 self.flows.retain(|_, users| !users.is_empty());
199 closed || worker
200 }
201 TopologyChange::Worker {
202 connection,
203 identity,
204 } => self.workers.insert(connection, identity.clone()) != Some(identity),
205 TopologyChange::Flow {
206 connection,
207 line,
208 role,
209 present,
210 } => {
211 if is_console_subject(&line) {
212 false
213 } else {
214 let moved = if present {
215 self.flows
216 .entry(line)
217 .or_default()
218 .insert((role, connection))
219 } else if let Some(users) = self.flows.get_mut(&line) {
220 let removed = users.remove(&(role, connection));
221 if users.is_empty() {
222 self.flows.remove(&line);
223 }
224 removed
225 } else {
226 false
227 };
228 moved && self.workers.contains_key(&connection)
229 }
230 }
231 TopologyChange::Peer { name, present } => {
232 if present {
233 self.peers.insert(name)
234 } else {
235 self.peers.remove(&name)
236 }
237 }
238 };
239 if changed {
240 self.rev = self.rev.saturating_add(1);
241 }
242 changed
243 }
244
245 fn stops(&self, line: &str, line_life: LineLife) -> Vec<String> {
251 let mut stops: Vec<String> = Vec::new();
252 let depot = (self.this.depot && line_life == LineLife::Kept).then(|| self.depot_id());
253 let mut own = depot.into_iter().chain([self.this.id.clone()]);
255 let users = self.flows.get(line).into_iter().flatten();
256 for (role, connection) in users {
257 if *role == FlowRole::Subscribes {
258 stops.extend(own.by_ref());
259 }
260 if let Some(identity) = self.workers.get(connection)
261 && self.reserved(identity).is_none()
262 && !stops.contains(identity)
263 {
264 stops.push(identity.clone());
265 }
266 }
267 stops.extend(own);
268 stops
269 }
270
271 #[must_use]
276 pub fn reserved(&self, id: &str) -> Option<&'static str> {
277 if id == self.this.id {
278 Some("exchange")
279 } else if self.this.depot
280 && id
281 .strip_prefix(self.this.id.as_str())
282 .is_some_and(|rest| rest == ".depot")
283 {
284 Some("depot")
285 } else {
286 None
287 }
288 }
289
290 fn depot_id(&self) -> String {
292 format!("{}.depot", self.this.id)
293 }
294
295 pub fn incarnation_started(&mut self, started: Instant) -> bool {
298 let moved = self.this.started != started;
299 self.this.started = started;
300 moved
301 }
302
303 #[cfg(test)]
305 pub(crate) fn participants(&self) -> Vec<Option<String>> {
306 self.connections
307 .values()
308 .map(|(_, participant)| participant.clone())
309 .collect()
310 }
311
312 #[cfg(test)]
314 pub(crate) fn connection_count(&self) -> usize {
315 self.connections.len()
316 }
317
318 #[must_use]
326 pub fn document(&self, wall: &WallBase) -> (TopologyDocument, Option<ConsoleError>) {
327 let this = &self.this.id;
328 let (started, fault) = match wall.unix_ms_of(self.this.started) {
329 Ok(started) => (Some(started), None),
330 Err(error) => (None, Some(error)),
331 };
332 let mut devices = vec![Device {
333 id: this.clone(),
334 name: this.clone(),
335 kind: DeviceKind::Device,
336 liminal: Some(self.this.liminal.clone()),
337 started,
338 started_fault: fault.as_ref().map(ToString::to_string),
339 zone: Some(DeviceBox::THIS),
340 }];
341 devices.extend(self.peers.iter().map(|peer| Device {
342 id: peer.clone(),
343 name: peer.clone(),
344 kind: DeviceKind::Device,
345 liminal: None,
346 started: None,
347 started_fault: None,
348 zone: None,
349 }));
350 let depot = self.this.depot.then(|| self.depot_id());
354 let mut stations: BTreeMap<&String, StationKind> = self
355 .workers
356 .values()
357 .filter(|identity| self.reserved(identity).is_none())
358 .map(|identity| (identity, StationKind::Worker))
359 .collect();
360 stations.extend(
361 self.stations
362 .iter()
363 .filter(|subject| self.reserved(subject).is_none())
364 .map(|subject| (subject, StationKind::Service)),
365 );
366 stations.extend(depot.iter().map(|id| (id, StationKind::Depot)));
367 stations.insert(this, StationKind::Exchange);
368 let document = TopologyDocument::new(
369 self.rev,
370 this.clone(),
371 devices,
372 stations
373 .into_iter()
374 .map(|(subject, kind)| Station {
375 id: subject.clone(),
376 name: subject.clone(),
377 device: this.clone(),
378 kind,
379 state: StationState::Live,
380 })
381 .collect(),
382 self.lines
383 .iter()
384 .map(|(name, (life, state))| Line {
385 id: name.clone(),
386 kind: LineKind::Stream,
387 life: *life,
388 state: *state,
389 stops: self.stops(name, *life),
390 })
391 .collect(),
392 self.peers
393 .iter()
394 .map(|peer| Link::to_peer(this, peer))
395 .collect(),
396 self.connections
397 .iter()
398 .map(|(id, (mount, participant))| Connection {
399 id: id.to_string(),
400 mount: *mount,
401 participant: participant.clone(),
402 })
403 .collect(),
404 );
405 (document, fault)
406 }
407}