Skip to main content

pb_mapper_client/sdk/
handle.rs

1//! Handles for a running tunnel: [`Registration`] and [`Connection`].
2//!
3//! Both wrap the same `LiveTunnel` — a cancellation token, the worker's join
4//! handle, and the status channel it publishes to. A handle observes and stops
5//! its tunnel; it never drives the traffic itself.
6
7use crate::diagnostics::{Diagnostics, TunnelDiagnostics, TunnelEvents};
8use crate::endpoint::RelayEndpoint;
9use std::time::Duration;
10
11use tokio::sync::{Mutex, watch};
12use tokio::task::JoinHandle;
13use tokio_util::sync::CancellationToken;
14
15use super::{Error, Result, TunnelStatus};
16
17pub(crate) struct LiveTunnel {
18    shutdown: CancellationToken,
19    join: Mutex<Option<JoinHandle<()>>>,
20    status: watch::Receiver<TunnelStatus>,
21    endpoint: RelayEndpoint,
22    diagnostics: Diagnostics,
23}
24
25impl LiveTunnel {
26    pub(crate) fn new(
27        shutdown: CancellationToken,
28        join: JoinHandle<()>,
29        status: watch::Receiver<TunnelStatus>,
30        endpoint: RelayEndpoint,
31        diagnostics: Diagnostics,
32    ) -> Self {
33        Self {
34            shutdown,
35            join: Mutex::new(Some(join)),
36            status,
37            endpoint,
38            diagnostics,
39        }
40    }
41
42    pub(crate) fn status(&self) -> TunnelStatus {
43        self.status.borrow().clone()
44    }
45
46    pub(crate) fn subscribe(&self) -> watch::Receiver<TunnelStatus> {
47        self.status.clone()
48    }
49
50    pub(crate) async fn wait_ready(&self) -> Result<()> {
51        wait_for_connected(&mut self.status.clone()).await
52    }
53
54    pub(crate) async fn wait_ready_timeout(&self, timeout: Duration) -> Result<()> {
55        match tokio::time::timeout(timeout, self.wait_ready()).await {
56            Ok(result) => result,
57            Err(_) => Err(Error::ReadyTimeout { timeout }),
58        }
59    }
60
61    pub(crate) async fn stop(&self) -> Result<()> {
62        self.shutdown.cancel();
63        // Retain ownership while awaiting: cancellation of stop must not detach
64        // the worker, and concurrent stop calls must observe the same cleanup.
65        let mut slot = self.join.lock().await;
66        if let Some(handle) = slot.as_mut() {
67            match tokio::time::timeout(Duration::from_secs(5), &mut *handle).await {
68                Ok(Ok(())) => {}
69                Ok(Err(join_error)) if join_error.is_cancelled() => {}
70                Ok(Err(join_error)) => {
71                    slot.take();
72                    self.diagnostics.stop();
73                    return Err(Error::protocol(format!("tunnel task failed: {join_error}")));
74                }
75                Err(_) => {
76                    handle.abort();
77                    let _ = handle.await;
78                }
79            }
80            slot.take();
81        }
82        self.diagnostics.stop();
83        Ok(())
84    }
85}
86
87impl Drop for LiveTunnel {
88    fn drop(&mut self) {
89        self.shutdown.cancel();
90        if let Some(handle) = self.join.get_mut().take() {
91            handle.abort();
92        }
93        self.diagnostics.stop();
94    }
95}
96
97async fn wait_for_connected(status: &mut watch::Receiver<TunnelStatus>) -> Result<()> {
98    loop {
99        match status.borrow().clone() {
100            TunnelStatus::Connected => return Ok(()),
101            TunnelStatus::Failed(reason) => return Err(Error::TunnelFailed { reason }),
102            TunnelStatus::Stopped => return Err(Error::Stopped),
103            TunnelStatus::Starting | TunnelStatus::Retrying => {}
104        }
105        status.changed().await.map_err(|_| Error::Stopped)?;
106    }
107}
108
109/// Declares one end of a live tunnel as a public handle.
110///
111/// [`Registration`] and [`Connection`] share a lifecycle down to the last method:
112/// the same status, the same readiness wait, the same stop. They stay distinct
113/// types so neither can be passed where the other is meant, and this macro is
114/// what keeps the two from drifting apart.
115macro_rules! tunnel_handle {
116    ($(#[$doc:meta])* $name:ident) => {
117        $(#[$doc])*
118        pub struct $name {
119            inner: LiveTunnel,
120            key: String,
121        }
122
123        impl $name {
124            pub(crate) fn new(inner: LiveTunnel, key: String) -> Self {
125                Self { inner, key }
126            }
127
128            /// The service key this tunnel is bound to.
129            pub fn key(&self) -> &str {
130                &self.key
131            }
132
133            /// The latest status its worker reported.
134            pub fn status(&self) -> TunnelStatus {
135                self.inner.status()
136            }
137
138            /// Credential-free recovery counters, running SDK version and shared relay load.
139            pub fn diagnostics(&self) -> TunnelDiagnostics {
140                self.inner.diagnostics.snapshot(&self.inner.endpoint)
141            }
142
143            /// The events kept in [`TunnelDiagnostics::events`], then every
144            /// event as it happens, until the tunnel stops.
145            pub fn subscribe_events(&self) -> TunnelEvents {
146                self.inner.diagnostics.subscribe()
147            }
148
149            /// Subscribe to status changes. Useful for N-API event bridges.
150            pub fn subscribe(&self) -> watch::Receiver<TunnelStatus> {
151                self.inner.subscribe()
152            }
153
154            /// Resolve once the tunnel is connected, or fails, or stops.
155            pub async fn wait_ready(&self) -> Result<()> {
156                self.inner.wait_ready().await
157            }
158
159            /// [`Self::wait_ready`], bounded by `timeout`.
160            pub async fn wait_ready_timeout(&self, timeout: Duration) -> Result<()> {
161                self.inner.wait_ready_timeout(timeout).await
162            }
163
164            /// Cancel the worker and wait for it to unwind.
165            pub async fn stop(&self) -> Result<()> {
166                self.inner.stop().await
167            }
168        }
169    };
170}
171
172tunnel_handle!(
173    /// A live `register` tunnel: a local service published on the relay.
174    Registration
175);
176
177tunnel_handle!(
178    /// A live `connect` tunnel: a local listener forwarding to a registered service.
179    Connection
180);
181
182#[cfg(test)]
183#[allow(clippy::unwrap_used)]
184mod tests {
185    use super::*;
186    use std::{future::Future, task::Poll};
187
188    async fn poll_pending(future: &mut std::pin::Pin<Box<impl Future>>) {
189        std::future::poll_fn(|cx| {
190            assert!(future.as_mut().poll(cx).is_pending());
191            Poll::Ready(())
192        })
193        .await;
194    }
195
196    #[tokio::test]
197    async fn cancelled_stop_keeps_worker_owned_until_handle_drop() {
198        let (tx, mut rx) = watch::channel(TunnelStatus::Starting);
199        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
200        let worker = tokio::spawn(async move {
201            let _tx = tx;
202            let _ = started_tx.send(());
203            std::future::pending::<()>().await;
204        });
205        let tunnel = LiveTunnel::new(
206            CancellationToken::new(),
207            worker,
208            rx.clone(),
209            RelayEndpoint::shared("127.0.0.1:1"),
210            Diagnostics::default(),
211        );
212        started_rx.await.unwrap();
213        let mut stopping = Box::pin(tunnel.stop());
214        poll_pending(&mut stopping).await;
215        drop(stopping);
216        drop(tunnel);
217        assert!(
218            tokio::time::timeout(Duration::from_secs(1), rx.changed())
219                .await
220                .unwrap()
221                .is_err()
222        );
223    }
224
225    #[tokio::test]
226    async fn concurrent_stop_calls_both_wait_for_cleanup() {
227        let (tx, rx) = watch::channel(TunnelStatus::Starting);
228        let (release, released) = tokio::sync::oneshot::channel();
229        let worker = tokio::spawn(async move {
230            let _tx = tx;
231            let _ = released.await;
232        });
233        let tunnel = LiveTunnel::new(
234            CancellationToken::new(),
235            worker,
236            rx,
237            RelayEndpoint::shared("127.0.0.1:1"),
238            Diagnostics::default(),
239        );
240        let mut first = Box::pin(tunnel.stop());
241        let mut second = Box::pin(tunnel.stop());
242        poll_pending(&mut first).await;
243        poll_pending(&mut second).await;
244        release.send(()).unwrap();
245        first.await.unwrap();
246        second.await.unwrap();
247    }
248}