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}