Skip to main content

bacnet_server/server/
shutdown.rs

1use super::*;
2
3#[cfg(test)]
4#[path = "request_tasks_tests.rs"]
5mod request_tasks_tests;
6
7#[cfg(test)]
8#[path = "producer_shutdown_tests.rs"]
9mod producer_shutdown_tests;
10
11#[cfg(test)]
12#[path = "transport_shutdown_tests.rs"]
13mod transport_shutdown_tests;
14
15#[cfg(test)]
16#[path = "owned_shutdown_tests.rs"]
17mod owned_shutdown_tests;
18
19#[cfg(test)]
20#[path = "log_persistence_tests.rs"]
21mod log_persistence_tests;
22
23async fn stop_producer(slot: &mut Option<JoinHandle<()>>) {
24    // Borrow across the join so cancellation leaves the handle recoverable.
25    // Clear synchronously after completion: a later stop must not poll it twice.
26    if let Some(task) = slot.as_mut() {
27        task.abort();
28        let _ = task.await;
29    }
30    *slot = None;
31}
32
33impl<T: TransportPort + 'static> BACnetServer<T> {
34    /// Seal egress, join admitted work, and stop the owned transport.
35    ///
36    /// Call it before dropping the server. A server dropped without it in
37    /// async code aborts its tasks and lets the object database go on Tokio's
38    /// blocking pool once they have let go of it (#1409), so the drop returns
39    /// at once, but nothing waits for the objects' last saves. Storage may
40    /// still change after such a drop returns, as those saves and the
41    /// put-back of a staged write (#1363) land, so `stop().await` before
42    /// building another server on the same storage.
43    ///
44    /// Once its requests are joined, a write a Notification Forwarder,
45    /// Notification Class, Access Rights object or Audit Log still holds
46    /// staged for one of them is dropped, and stop waits until every save
47    /// those objects have queued has run, so storage holds the state each
48    /// object serves (#1363); see
49    /// [`DurableWrites::settle_forgotten_writes`]. A Trend Log, Trend Log
50    /// Multiple or Event Log kept in storage commits once more there, if it
51    /// changed since its last commit, and stop waits for that commit too
52    /// (#1541). That wait has no limit:
53    /// storage that stalls holds stop up, and a warning naming the objects
54    /// still saving is logged after 5 s and every 30 s after that. Stop does
55    /// not wait while the application holds the database: the objects then
56    /// settle from a task once it lets go, and put storage back when they
57    /// are dropped. That task lets go of its handle through
58    /// [`drop_database_off_runtime`], so if it holds the last one the
59    /// objects drop on Tokio's blocking pool.
60    ///
61    /// A request's abort lands when its task next yields, so a request
62    /// already running when stop begins may find its save done and make its
63    /// write first. Its answer is sealed, but the object serves that write,
64    /// and storage holds it (#1457).
65    ///
66    /// [`DurableWrites::settle_forgotten_writes`]: bacnet_objects::durable::DurableWrites::settle_forgotten_writes
67    ///
68    /// Cancelling this waiter leaves cleanup owned by the server. A later stop
69    /// joins it; after transport cleanup begins, dropping the server lets that
70    /// cleanup finish. Local mutation and broadcasts are rejected from the first
71    /// stop poll; local reads and database inspection remain available.
72    pub async fn stop(&mut self) -> Result<(), Error> {
73        if let Some(network) = &self.network {
74            network.seal_responses();
75        }
76        self.broadcaster.seal();
77        self.request_tasks.close();
78        stop_producer(&mut self.network_number_task).await;
79        if let Some(runtime) = &self.target_audit {
80            // Only the target drain retains ingress for ACK/control progress.
81            self.notification_transactions
82                .application_sealed
83                .store(true, Ordering::Release);
84            runtime.seal();
85            runtime.batches.begin_stop();
86            for task in [
87                &self.fault_detection_task,
88                &self.event_enrollment_task,
89                &self.trend_log_task,
90                &self.schedule_tick_task,
91                &self.intrinsic_reporting_task,
92                &self.binary_lighting_operation_task,
93                &self.cov_purge_task,
94                &self.cov_revisit_task,
95            ]
96            .into_iter()
97            .flatten()
98            {
99                task.abort();
100            }
101            {
102                let mut timer = self.dcc_timer.lock().await;
103                super::dcc_timer::cancel(&mut timer).await;
104            }
105            loop {
106                let finished = runtime.batches.finished.notified();
107                tokio::pin!(finished);
108                finished.as_mut().enable();
109                if runtime.batches.stopped() {
110                    break;
111                }
112                finished.await;
113            }
114        } else {
115            // Other profiles retain their sequential, cancellation-safe shutdown.
116            self.notification_transactions.close();
117        }
118        // Keep the handle in self until joined: cancellation must not detach
119        // dispatch and allow a later stop to race its join consumer.
120        if let Some(task) = self.dispatch_task.as_mut() {
121            task.abort();
122            let _ = task.await;
123        }
124        self.dispatch_task = None;
125        while let Some(result) = self.request_tasks.join_next().await {
126            super::request_tasks::RequestTasks::observe(Some(result));
127        }
128        {
129            let mut timer = self.dcc_timer.lock().await;
130            super::dcc_timer::cancel(&mut timer).await;
131        }
132        stop_producer(&mut self.fault_detection_task).await;
133        stop_producer(&mut self.event_enrollment_task).await;
134        stop_producer(&mut self.trend_log_task).await;
135        stop_producer(&mut self.schedule_tick_task).await;
136        stop_producer(&mut self.intrinsic_reporting_task).await;
137        stop_producer(&mut self.binary_lighting_operation_task).await;
138        stop_producer(&mut self.cov_purge_task).await;
139        stop_producer(&mut self.cov_revisit_task).await;
140        // No request is left to take or release a staged save, and the
141        // operation task that would drop one is gone: put storage back to
142        // what each object serves before stop returns (#1363). A
143        // `write_local` still in flight isn't joined; if it staged, it finds
144        // its stage gone and saves in place under the guard.
145        super::durable_writes::settle_forgotten(&self.db).await;
146        // Nothing can own a Command or Channel run any more. End the runs let
147        // go of while the database was busy where they stood, then any run no
148        // task took up, so none is left in progress (#1252). An application
149        // holding the database doesn't hold up stop: they end once it's free.
150        crate::command_lists::end_unowned(&self.db, self.request_tasks.take_stranded());
151        // Dispatch has relinquished the sole join-consumer role. Retain the
152        // set in self across await so a cancelled stop can finish this drain.
153        while let Some(result) = self.notification_transactions.join_next().await {
154            NotificationTransactions::observe(Some(result));
155        }
156        if let Some(runtime) = &self.target_audit {
157            runtime.uninstall(&mut *self.db.write().await);
158        }
159        self.target_audit = None;
160        if let Some(error) = &self.transport_cleanup_error {
161            return Err(Error::Encoding(error.clone()));
162        }
163        if self.transport_cleanup.is_none() {
164            if let Some(network) = self.network.take() {
165                let mut network = match Arc::try_unwrap(network) {
166                    Ok(network) => network,
167                    Err(network) => {
168                        self.network = Some(network);
169                        return Err(Error::Encoding(
170                            "server network still has an internal owner".into(),
171                        ));
172                    }
173                };
174                // Own the complete future, including cancellation-unsafe custom
175                // transport cleanup. Drop deliberately leaves this task running.
176                self.transport_cleanup = Some(tokio::spawn(async move {
177                    let result = network.stop().await;
178                    (network, result)
179                }));
180            }
181        }
182        let Some(cleanup) = self.transport_cleanup.as_mut() else {
183            return Ok(());
184        };
185        let outcome = cleanup.await;
186        self.transport_cleanup = None;
187        match outcome {
188            Ok((network, result)) => {
189                if result.is_err() {
190                    self.network = Some(Arc::new(network));
191                }
192                result
193            }
194            Err(error) => {
195                let message = format!("transport cleanup task failed: {error}");
196                self.transport_cleanup_error = Some(message.clone());
197                Err(Error::Encoding(message))
198            }
199        }
200    }
201}
202
203impl<T: TransportPort> Drop for BACnetServer<T> {
204    /// Seal the server and abort its tasks. In async code the object
205    /// database then drops on Tokio's blocking pool (#1409): the drop returns
206    /// before the objects' last saves land, so storage may still change after
207    /// it. Call [`stop`](BACnetServer::stop) first to wait for them. A clone
208    /// of [`database`](BACnetServer::database) the application still holds
209    /// keeps the objects alive; let go of it with
210    /// [`drop_database_off_runtime`] (#1513).
211    fn drop(&mut self) {
212        if let Some(network) = &self.network {
213            network.seal_responses();
214        }
215        self.broadcaster.seal();
216        self.notification_transactions
217            .application_sealed
218            .store(true, Ordering::Release);
219        if let Some(runtime) = &self.target_audit {
220            runtime.seal();
221            drop(runtime.batches.close());
222        }
223        self.request_tasks.close();
224        self.notification_transactions.close();
225        let mut tasks: Vec<JoinHandle<()>> = [
226            &mut self.dispatch_task,
227            &mut self.network_number_task,
228            &mut self.cov_purge_task,
229            &mut self.fault_detection_task,
230            &mut self.event_enrollment_task,
231            &mut self.trend_log_task,
232            &mut self.schedule_tick_task,
233            &mut self.intrinsic_reporting_task,
234            &mut self.binary_lighting_operation_task,
235            &mut self.cov_revisit_task,
236        ]
237        .into_iter()
238        .filter_map(Option::take)
239        .collect();
240        if let Ok(mut timer) = self.dcc_timer.try_lock() {
241            tasks.extend(timer.take());
242        }
243        for task in &tasks {
244            task.abort();
245        }
246        self.let_database_go(tasks);
247    }
248}
249
250impl<T: TransportPort> BACnetServer<T> {
251    /// Let the object database go off the async runtime as the server is
252    /// dropped (#1409).
253    ///
254    /// Dropping the database drops its objects, and a durable object waits
255    /// for the saves it has queued, after putting storage back to the state
256    /// it serves when a write is still staged (#1363). A server dropped in
257    /// async code without `stop()` would block a Tokio worker for as long as
258    /// storage takes. So when a runtime is current, the server's handle goes
259    /// to a task that waits until `tasks`, aborted, and the server's request
260    /// and notification tasks have let go of theirs; then it lets go of the
261    /// handle through [`drop_database_off_runtime`], so the database drops
262    /// on the blocking pool if nothing else holds it, as DeleteObject drops a
263    /// removed object. The server's drop returns first, so storage may still
264    /// change after it. An application still holding the database lets go
265    /// of the last handle itself, the same way (#1513). With no runtime
266    /// current, the database drops here, as it always has.
267    ///
268    /// A DCC timer the drop could not take, because a request or the timer's
269    /// own expiry held its slot, is cancelled here once the requests are
270    /// joined: with Audit reporting, it holds the database too (#1560).
271    fn let_database_go(&mut self, tasks: Vec<JoinHandle<()>>) {
272        let Ok(runtime) = tokio::runtime::Handle::try_current() else {
273            return;
274        };
275        let db = std::mem::take(&mut self.db);
276        let requests = Arc::clone(&self.request_tasks);
277        let notifications = Arc::clone(&self.notification_transactions);
278        let dcc_timer = Arc::clone(&self.dcc_timer);
279        runtime.spawn(async move {
280            for task in tasks {
281                let _ = task.await;
282            }
283            while let Some(result) = requests.join_next().await {
284                super::request_tasks::RequestTasks::observe(Some(result));
285            }
286            // No request is left to hold the slot. The guard goes before the
287            // join, as the timer may be queued for the slot itself.
288            let timer = dcc_timer.lock().await.take();
289            if let Some(timer) = timer {
290                timer.abort();
291                let _ = timer.await;
292            }
293            while let Some(result) = notifications.join_next().await {
294                NotificationTransactions::observe(Some(result));
295            }
296            drop(drop_database_off_runtime(db));
297        });
298    }
299}
300
301/// Let go of a handle on an object database without parking an async
302/// runtime's worker thread (#1513).
303///
304/// The last handle to go drops every object, and a durable object (a
305/// Notification Forwarder, Notification Class, Access Rights object or Audit
306/// Log with storage) waits as it drops for the saves it has queued, after
307/// putting storage back to the state it serves if a write is still staged
308/// (#1363). Dropped in async code, that wait would hold a Tokio worker for as
309/// long as storage takes. So if `db` is the last handle and a Tokio runtime is
310/// current, the database drops on the runtime's blocking pool, and the
311/// returned task finishes once it has. Any other handle just lets go and
312/// returns `None`, as does a last one with no runtime current, which drops
313/// the database on this thread. A runtime that is shutting down runs nothing
314/// more on its blocking pool: Tokio shuts the new task down at once, and the
315/// database drops on this thread then too.
316///
317/// A dropped server lets go of its own handle this way once its tasks are
318/// done, whether or not it was stopped, and so do the tasks a `stop()`
319/// leaves to settle staged writes and end runs once the application lets
320/// go of the database. Any of them may still hold a handle when the
321/// application lets go of its clone of [`BACnetServer::database`], so
322/// `None` is the usual outcome. What waits until storage holds the state
323/// each object serves is [`stop`](BACnetServer::stop), not this. This keeps
324/// the application's own handle, should it be the last, from dropping the
325/// objects on a runtime worker: in async code, release it here rather than
326/// dropping it.
327///
328/// ```no_run
329/// # use bacnet_transport::bip::BipTransport;
330/// # async fn demo(mut server: bacnet_server::server::BACnetServer<BipTransport>) {
331/// use bacnet_server::server::drop_database_off_runtime;
332///
333/// let db = std::sync::Arc::clone(server.database());
334/// // ... use the database while the server runs ...
335/// // stop() waits until storage holds what each object serves.
336/// let _ = server.stop().await;
337/// drop(server);
338/// // The server's own tasks may still hold the database; whichever handle
339/// // goes last, it drops off the runtime.
340/// drop(drop_database_off_runtime(db));
341/// # }
342/// ```
343pub fn drop_database_off_runtime(db: Arc<RwLock<ObjectDatabase>>) -> Option<JoinHandle<()>> {
344    // `Arc::into_inner` hands the database to exactly one of two handles let
345    // go of at once, where a count check followed by a drop could leave both
346    // thinking the other was last.
347    let db = Arc::into_inner(db)?;
348    let Ok(runtime) = tokio::runtime::Handle::try_current() else {
349        drop(db);
350        return None;
351    };
352    Some(runtime.spawn_blocking(move || drop(db)))
353}