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}