pb_mapper_client/sdk/
handle.rs1use std::time::Duration;
8
9use tokio::sync::{Mutex, watch};
10use tokio::task::JoinHandle;
11use tokio_util::sync::CancellationToken;
12
13use super::{Error, Result, TunnelStatus};
14
15pub(crate) struct LiveTunnel {
16 shutdown: CancellationToken,
17 join: Mutex<Option<JoinHandle<()>>>,
18 status: watch::Receiver<TunnelStatus>,
19}
20
21impl LiveTunnel {
22 pub(crate) fn new(
23 shutdown: CancellationToken,
24 join: JoinHandle<()>,
25 status: watch::Receiver<TunnelStatus>,
26 ) -> Self {
27 Self {
28 shutdown,
29 join: Mutex::new(Some(join)),
30 status,
31 }
32 }
33
34 pub(crate) fn status(&self) -> TunnelStatus {
35 self.status.borrow().clone()
36 }
37
38 pub(crate) fn subscribe(&self) -> watch::Receiver<TunnelStatus> {
39 self.status.clone()
40 }
41
42 pub(crate) async fn wait_ready(&self) -> Result<()> {
43 wait_for_connected(&mut self.status.clone()).await
44 }
45
46 pub(crate) async fn wait_ready_timeout(&self, timeout: Duration) -> Result<()> {
47 match tokio::time::timeout(timeout, self.wait_ready()).await {
48 Ok(result) => result,
49 Err(_) => Err(Error::ReadyTimeout { timeout }),
50 }
51 }
52
53 pub(crate) async fn stop(&self) -> Result<()> {
54 self.shutdown.cancel();
55 let mut slot = self.join.lock().await;
58 if let Some(handle) = slot.as_mut() {
59 match tokio::time::timeout(Duration::from_secs(5), &mut *handle).await {
60 Ok(Ok(())) => {}
61 Ok(Err(join_error)) if join_error.is_cancelled() => {}
62 Ok(Err(join_error)) => {
63 slot.take();
64 return Err(Error::protocol(format!("tunnel task failed: {join_error}")));
65 }
66 Err(_) => {
67 handle.abort();
68 let _ = handle.await;
69 }
70 }
71 slot.take();
72 }
73 Ok(())
74 }
75}
76
77impl Drop for LiveTunnel {
78 fn drop(&mut self) {
79 self.shutdown.cancel();
80 if let Some(handle) = self.join.get_mut().take() {
81 handle.abort();
82 }
83 }
84}
85
86async fn wait_for_connected(status: &mut watch::Receiver<TunnelStatus>) -> Result<()> {
87 loop {
88 match status.borrow().clone() {
89 TunnelStatus::Connected => return Ok(()),
90 TunnelStatus::Failed(reason) => return Err(Error::TunnelFailed { reason }),
91 TunnelStatus::Stopped => return Err(Error::Stopped),
92 TunnelStatus::Starting | TunnelStatus::Retrying => {}
93 }
94 status.changed().await.map_err(|_| Error::Stopped)?;
95 }
96}
97
98macro_rules! tunnel_handle {
105 ($(#[$doc:meta])* $name:ident) => {
106 $(#[$doc])*
107 pub struct $name {
108 inner: LiveTunnel,
109 key: String,
110 }
111
112 impl $name {
113 pub(crate) fn new(inner: LiveTunnel, key: String) -> Self {
114 Self { inner, key }
115 }
116
117 pub fn key(&self) -> &str {
119 &self.key
120 }
121
122 pub fn status(&self) -> TunnelStatus {
124 self.inner.status()
125 }
126
127 pub fn subscribe(&self) -> watch::Receiver<TunnelStatus> {
129 self.inner.subscribe()
130 }
131
132 pub async fn wait_ready(&self) -> Result<()> {
134 self.inner.wait_ready().await
135 }
136
137 pub async fn wait_ready_timeout(&self, timeout: Duration) -> Result<()> {
139 self.inner.wait_ready_timeout(timeout).await
140 }
141
142 pub async fn stop(&self) -> Result<()> {
144 self.inner.stop().await
145 }
146 }
147 };
148}
149
150tunnel_handle!(
151 Registration
153);
154
155tunnel_handle!(
156 Connection
158);
159
160#[cfg(test)]
161#[allow(clippy::unwrap_used)]
162mod tests {
163 use super::*;
164 use std::{future::Future, task::Poll};
165
166 async fn poll_pending(future: &mut std::pin::Pin<Box<impl Future>>) {
167 std::future::poll_fn(|cx| {
168 assert!(future.as_mut().poll(cx).is_pending());
169 Poll::Ready(())
170 })
171 .await;
172 }
173
174 #[tokio::test]
175 async fn cancelled_stop_keeps_worker_owned_until_handle_drop() {
176 let (tx, mut rx) = watch::channel(TunnelStatus::Starting);
177 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
178 let worker = tokio::spawn(async move {
179 let _tx = tx;
180 let _ = started_tx.send(());
181 std::future::pending::<()>().await;
182 });
183 let tunnel = LiveTunnel::new(CancellationToken::new(), worker, rx.clone());
184 started_rx.await.unwrap();
185 let mut stopping = Box::pin(tunnel.stop());
186 poll_pending(&mut stopping).await;
187 drop(stopping);
188 drop(tunnel);
189 assert!(
190 tokio::time::timeout(Duration::from_secs(1), rx.changed())
191 .await
192 .unwrap()
193 .is_err()
194 );
195 }
196
197 #[tokio::test]
198 async fn concurrent_stop_calls_both_wait_for_cleanup() {
199 let (tx, rx) = watch::channel(TunnelStatus::Starting);
200 let (release, released) = tokio::sync::oneshot::channel();
201 let worker = tokio::spawn(async move {
202 let _tx = tx;
203 let _ = released.await;
204 });
205 let tunnel = LiveTunnel::new(CancellationToken::new(), worker, rx);
206 let mut first = Box::pin(tunnel.stop());
207 let mut second = Box::pin(tunnel.stop());
208 poll_pending(&mut first).await;
209 poll_pending(&mut second).await;
210 release.send(()).unwrap();
211 first.await.unwrap();
212 second.await.unwrap();
213 }
214}