tsoracle_standalone/
transport.rs1use std::future::Future;
25use std::pin::Pin;
26use std::sync::Arc;
27use std::sync::atomic::{AtomicBool, Ordering};
28
29use tokio::sync::{oneshot, watch};
30use tokio::task::JoinHandle;
31
32#[derive(Clone)]
38pub struct FatalSignal {
39 component: watch::Sender<Option<&'static str>>,
40}
41
42impl FatalSignal {
43 pub fn new() -> Self {
45 Self {
46 component: watch::Sender::new(None),
47 }
48 }
49
50 pub fn trip(&self, component: &'static str) {
54 self.component.send_if_modified(|current| {
55 if current.is_none() {
56 *current = Some(component);
57 true
58 } else {
59 false
60 }
61 });
62 }
63
64 pub fn check(&self) -> Option<&'static str> {
66 *self.component.borrow()
67 }
68
69 pub async fn tripped(&self) -> &'static str {
72 let mut watcher = self.component.subscribe();
73 loop {
74 if let Some(component) = *watcher.borrow_and_update() {
75 return component;
76 }
77 if watcher.changed().await.is_err() {
80 std::future::pending::<()>().await;
81 }
82 }
83 }
84}
85
86impl Default for FatalSignal {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92pub struct TransportHandle {
95 cancel: Option<oneshot::Sender<()>>,
96 join: Option<JoinHandle<()>>,
97}
98
99impl TransportHandle {
100 pub fn noop() -> Self {
102 Self {
103 cancel: None,
104 join: None,
105 }
106 }
107
108 pub fn new(cancel: oneshot::Sender<()>, join: JoinHandle<()>) -> Self {
110 Self {
111 cancel: Some(cancel),
112 join: Some(join),
113 }
114 }
115
116 pub fn spawn_supervised<F, Fut, E>(
123 component: &'static str,
124 fatal: FatalSignal,
125 serve: F,
126 ) -> Self
127 where
128 F: FnOnce(Pin<Box<dyn Future<Output = ()> + Send>>) -> Fut,
129 Fut: Future<Output = Result<(), E>> + Send + 'static,
130 E: std::fmt::Debug + Send + 'static,
131 {
132 let (cancel_tx, cancel_rx) = oneshot::channel::<()>();
133 let stop_requested = Arc::new(AtomicBool::new(false));
134 let stop_observed = stop_requested.clone();
135 let shutdown: Pin<Box<dyn Future<Output = ()> + Send>> = Box::pin(async move {
136 let _ = cancel_rx.await;
139 stop_observed.store(true, Ordering::Release);
140 });
141 let server = tokio::spawn(serve(shutdown));
144 let supervisor = tokio::spawn(async move {
145 match server.await {
146 Ok(Ok(())) if stop_requested.load(Ordering::Acquire) => {}
147 Ok(Ok(())) => {
148 tracing::error!(component, "server exited without a shutdown request");
149 fatal.trip(component);
150 }
151 Ok(Err(err)) => {
152 tracing::error!(error = ?err, component, "server died");
153 fatal.trip(component);
154 }
155 Err(join_error) => {
156 tracing::error!(error = ?join_error, component, "server task panicked or was aborted");
157 fatal.trip(component);
158 }
159 }
160 });
161 Self::new(cancel_tx, supervisor)
162 }
163
164 pub async fn shutdown(&mut self) {
167 if let Some(cancel) = self.cancel.take() {
168 let _ = cancel.send(());
170 }
171 if let Some(join) = self.join.take() {
172 if let Err(err) = join.await {
173 tracing::warn!(error = ?err, "peer transport task join error");
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[tokio::test]
186 async fn shutdown_signals_the_task_and_joins() {
187 let (cancel_tx, cancel_rx) = oneshot::channel::<()>();
188 let join = tokio::spawn(async move {
189 let _ = cancel_rx.await;
192 });
193 let mut handle = TransportHandle::new(cancel_tx, join);
194 handle.shutdown().await;
195 handle.shutdown().await;
197 }
198
199 #[tokio::test]
201 async fn noop_shutdown_is_harmless() {
202 let mut handle = TransportHandle::noop();
203 handle.shutdown().await;
204 }
205
206 #[tokio::test]
209 async fn shutdown_after_task_already_returned() {
210 let (cancel_tx, _cancel_rx) = oneshot::channel::<()>();
211 let join = tokio::spawn(async {});
212 tokio::task::yield_now().await;
214 let mut handle = TransportHandle::new(cancel_tx, join);
215 handle.shutdown().await;
216 }
217
218 #[test]
220 fn fatal_signal_starts_untripped() {
221 assert_eq!(FatalSignal::new().check(), None);
222 }
223
224 #[tokio::test]
226 async fn fatal_trip_records_first_component_only() {
227 let fatal = FatalSignal::new();
228 fatal.trip("peer server");
229 fatal.trip("admin server");
230 assert_eq!(fatal.check(), Some("peer server"));
231 assert_eq!(fatal.tripped().await, "peer server");
232 }
233
234 #[tokio::test]
236 async fn fatal_tripped_wakes_a_pending_waiter() {
237 let fatal = FatalSignal::new();
238 let waiter = tokio::spawn({
239 let fatal = fatal.clone();
240 async move { fatal.tripped().await }
241 });
242 tokio::task::yield_now().await;
244 fatal.trip("peer server");
245 assert_eq!(waiter.await.unwrap(), "peer server");
246 }
247
248 #[tokio::test]
251 async fn supervised_error_exit_trips_fatal() {
252 let fatal = FatalSignal::new();
253 let mut handle =
254 TransportHandle::spawn_supervised("test server", fatal.clone(), |_shutdown| async {
255 Err::<(), std::io::Error>(std::io::Error::other("listener torn down"))
256 });
257 assert_eq!(fatal.tripped().await, "test server");
258 handle.shutdown().await;
259 }
260
261 #[tokio::test]
264 async fn supervised_graceful_cancel_does_not_trip() {
265 let fatal = FatalSignal::new();
266 let mut handle = TransportHandle::spawn_supervised(
267 "test server",
268 fatal.clone(),
269 |shutdown| async move {
270 shutdown.await;
271 Ok::<(), std::io::Error>(())
272 },
273 );
274 handle.shutdown().await;
276 assert_eq!(fatal.check(), None);
277 }
278
279 #[tokio::test]
282 async fn supervised_premature_clean_exit_trips_fatal() {
283 let fatal = FatalSignal::new();
284 let mut handle =
285 TransportHandle::spawn_supervised("test server", fatal.clone(), |_shutdown| async {
286 Ok::<(), std::io::Error>(())
287 });
288 assert_eq!(fatal.tripped().await, "test server");
289 handle.shutdown().await;
290 }
291
292 #[tokio::test]
295 async fn supervised_panic_trips_fatal() {
296 let fatal = FatalSignal::new();
297 let mut handle =
298 TransportHandle::spawn_supervised("test server", fatal.clone(), |_shutdown| async {
299 panic!("server task blew up");
300 #[allow(unreachable_code)]
301 Ok::<(), std::io::Error>(())
302 });
303 assert_eq!(fatal.tripped().await, "test server");
304 handle.shutdown().await;
305 }
306
307 #[tokio::test]
311 async fn supervised_drop_without_shutdown_does_not_trip() {
312 let fatal = FatalSignal::new();
313 let handle = TransportHandle::spawn_supervised(
314 "test server",
315 fatal.clone(),
316 |shutdown| async move {
317 shutdown.await;
318 Ok::<(), std::io::Error>(())
319 },
320 );
321 drop(handle);
322 for _ in 0..32 {
323 tokio::task::yield_now().await;
324 assert_eq!(fatal.check(), None);
325 }
326 }
327
328 #[cfg(feature = "openraft")]
332 #[tokio::test]
333 async fn tonic_incoming_error_propagates_to_fatal() {
334 use std::sync::Arc;
335
336 use crate::admin::service::AdminServiceImpl;
337 use crate::admin::{MembershipAdmin, MembershipView, UnsupportedAdmin};
338 use crate::admin_proto::membership_admin_server::MembershipAdminServer;
339
340 let admin: Arc<dyn MembershipAdmin> = Arc::new(UnsupportedAdmin::new(MembershipView {
341 members: Vec::new(),
342 leader: None,
343 }));
344 let service = MembershipAdminServer::new(AdminServiceImpl::new(admin));
345 let incoming = futures::stream::iter(vec![Err::<tokio::net::TcpStream, std::io::Error>(
346 std::io::Error::other("accept failed"),
347 )]);
348 let fatal = FatalSignal::new();
349 let mut handle =
350 TransportHandle::spawn_supervised("admin server", fatal.clone(), move |shutdown| {
351 tonic::transport::Server::builder()
352 .add_service(service)
353 .serve_with_incoming_shutdown(incoming, shutdown)
354 });
355 assert_eq!(fatal.tripped().await, "admin server");
356 handle.shutdown().await;
357 }
358}