Skip to main content

bacnet_network/router/
shutdown.rs

1//! Retained teardown: cancellation drops a waiter, never its resource owners.
2
3use std::fmt;
4use std::future::{Future, poll_fn};
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::sync::Arc;
7use std::task::Poll;
8
9use bacnet_transport::port::TransportPort;
10use bacnet_types::error::Error;
11use tokio::sync::watch;
12use tokio::task::JoinHandle;
13
14/// One failure from the router's single shutdown attempt.
15#[derive(Debug)]
16pub enum RouterStopFailure {
17    /// A transport returned its original cleanup error.
18    Transport {
19        /// The configured BACnet network number.
20        network_number: u16,
21        /// The original error, including any owned I/O cause.
22        error: Error,
23    },
24    /// A worker panicked or its owner disappeared unexpectedly.
25    Worker {
26        /// The port's configured network number, when the worker belongs to a port.
27        network_number: Option<u16>,
28        /// The worker or cleanup operation that failed.
29        worker: &'static str,
30        /// The retained panic or join diagnostic.
31        detail: String,
32    },
33}
34
35/// Failures retained from one settled router shutdown attempt.
36///
37/// Clones share the original errors. Repeated stop calls replay this report;
38/// they do not retry failed cleanup. Errors mean cleanup attempts settled,
39/// but do not guarantee that every external resource was released.
40#[derive(Clone, Debug)]
41pub struct RouterStopError(Arc<[RouterStopFailure]>);
42
43impl RouterStopError {
44    /// All failures. Transport owners are joined in configured-port order.
45    pub fn failures(&self) -> &[RouterStopFailure] {
46        &self.0
47    }
48
49    fn from_failures(failures: Vec<RouterStopFailure>) -> Result<(), Self> {
50        if failures.is_empty() {
51            Ok(())
52        } else {
53            Err(Self(failures.into()))
54        }
55    }
56}
57
58impl fmt::Display for RouterStopError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "router shutdown failed: {:?}", self.0)
61    }
62}
63
64impl std::error::Error for RouterStopError {
65    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
66        self.0.iter().find_map(|failure| match failure {
67            RouterStopFailure::Transport { error, .. } => {
68                Some(error as &(dyn std::error::Error + 'static))
69            }
70            RouterStopFailure::Worker { .. } => None,
71        })
72    }
73}
74
75/// Catch a worker's panic without surrendering the transport it borrowed.
76/// This is local to owned router work; ordinary transport errors stay typed.
77pub(super) async fn observe<F: Future>(future: F) -> Result<F::Output, String> {
78    let mut future = std::pin::pin!(future);
79    poll_fn(
80        |cx| match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(cx))) {
81            Ok(Poll::Pending) => Poll::Pending,
82            Ok(Poll::Ready(value)) => Poll::Ready(Ok(value)),
83            Err(panic) => {
84                let detail = panic
85                    .downcast_ref::<String>()
86                    .cloned()
87                    .or_else(|| panic.downcast_ref::<&str>().map(|s| (*s).to_owned()))
88                    .unwrap_or_else(|| "non-string panic payload".into());
89                Poll::Ready(Err(detail))
90            }
91        },
92    )
93    .await
94}
95
96/// Abort is only a fallback when async cleanup failed or was interrupted by
97/// runtime shutdown. Successful cleanup disarms it.
98pub(super) struct TransportOwner<T: TransportPort> {
99    pub transport: T,
100    pub stopped: bool,
101}
102
103impl<T: TransportPort> Drop for TransportOwner<T> {
104    fn drop(&mut self) {
105        if !self.stopped {
106            self.transport.abort();
107        }
108    }
109}
110
111pub(super) struct Workers {
112    pub dispatch: Vec<JoinHandle<()>>,
113    pub senders: Vec<(u16, JoinHandle<Vec<RouterStopFailure>>)>,
114    pub aging: JoinHandle<()>,
115    pub limits: Vec<JoinHandle<()>>,
116    pub stop_senders: watch::Sender<bool>,
117}
118
119impl Workers {
120    async fn stop(mut self) -> Result<(), RouterStopError> {
121        // Signal every sender before awaiting any worker, so a pending send
122        // cannot block another port's cleanup.
123        self.stop_senders.send_replace(true);
124        for task in &self.dispatch {
125            task.abort();
126        }
127        self.aging.abort();
128        for task in &self.limits {
129            task.abort();
130        }
131        let mut failures = Vec::new();
132        for task in &mut self.dispatch {
133            joined(task, "dispatch", &mut failures).await;
134        }
135        joined(&mut self.aging, "aging", &mut failures).await;
136        for task in &mut self.limits {
137            joined(task, "limit", &mut failures).await;
138        }
139        for (network_number, task) in &mut self.senders {
140            match task.await {
141                Ok(port_failures) => failures.extend(port_failures),
142                Err(error) => failures.push(RouterStopFailure::Worker {
143                    network_number: Some(*network_number),
144                    worker: "sender owner",
145                    detail: error.to_string(),
146                }),
147            }
148        }
149        RouterStopError::from_failures(failures)
150    }
151}
152
153impl Drop for Workers {
154    fn drop(&mut self) {
155        self.stop_senders.send_replace(true);
156        for task in &self.dispatch {
157            task.abort();
158        }
159        self.aging.abort();
160        for task in &self.limits {
161            task.abort();
162        }
163        // Sender owners must keep running to await transport cleanup. Their
164        // watch receiver also observes this sender's closure on runtime exit.
165    }
166}
167
168async fn joined(
169    task: &mut JoinHandle<()>,
170    worker: &'static str,
171    failures: &mut Vec<RouterStopFailure>,
172) {
173    if let Err(error) = task.await
174        && !error.is_cancelled()
175    {
176        failures.push(RouterStopFailure::Worker {
177            network_number: None,
178            worker,
179            detail: error.to_string(),
180        });
181    }
182}
183
184pub(super) struct Lifecycle {
185    pub workers: Option<Workers>,
186    cleanup: Option<JoinHandle<Result<(), RouterStopError>>>,
187    settled: Option<Result<(), RouterStopError>>,
188}
189
190impl Lifecycle {
191    pub fn new(workers: Workers) -> Self {
192        Self {
193            workers: Some(workers),
194            cleanup: None,
195            settled: None,
196        }
197    }
198
199    pub fn begin(&mut self) {
200        if let Some(workers) = self.workers.take() {
201            if let Ok(runtime) = tokio::runtime::Handle::try_current() {
202                self.cleanup = Some(runtime.spawn(workers.stop()));
203            } else {
204                // No async completion promise is possible without a runtime.
205                drop(workers);
206            }
207        }
208    }
209
210    pub async fn stop(&mut self) -> Result<(), RouterStopError> {
211        self.begin();
212        if self.settled.is_none() {
213            let result = match self.cleanup.as_mut() {
214                Some(task) => match task.await {
215                    Ok(result) => result,
216                    Err(error) => RouterStopError::from_failures(vec![RouterStopFailure::Worker {
217                        network_number: None,
218                        worker: "shutdown owner",
219                        detail: error.to_string(),
220                    }]),
221                },
222                None => RouterStopError::from_failures(vec![RouterStopFailure::Worker {
223                    network_number: None,
224                    worker: "shutdown owner",
225                    detail: "cleanup owner unavailable".into(),
226                }]),
227            };
228            self.cleanup.take();
229            self.settled = Some(result);
230        }
231        self.settled.as_ref().unwrap().clone()
232    }
233}
234
235impl Drop for Lifecycle {
236    fn drop(&mut self) {
237        self.begin();
238    }
239}