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