Skip to main content

unb_server/
connection.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, Mutex, RwLock, Weak};
3
4use tokio::sync::watch;
5use unb_client::{EndpointSet, TransportKind};
6use unb_core::{NodeIdentity, RetirementReason};
7use unb_runtime::Wire;
8
9use crate::connect::EndpointDialer;
10use crate::node::Node;
11
12#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
13/// A stable, cloneable failure category for initial connect and reconnect.
14pub enum ConnectError {
15    #[error("no supported endpoint in set")]
16    NoSupportedEndpoint,
17    #[error("dial failed for {transport:?}: {message}")]
18    Dial {
19        transport: TransportKind,
20        message: String,
21    },
22    #[error("dial timed out for {transport:?}")]
23    DialTimedOut { transport: TransportKind },
24    #[error("peer establishment failed: {message}")]
25    Establishment { message: String },
26    #[error("peer identity mismatch: expected {expected:?}, got {actual:?}")]
27    IdentityMismatch {
28        expected: String,
29        actual: Option<String>,
30    },
31    #[error("the owning node is shut down")]
32    NodeShutdown,
33    #[error("the reconnect attempt was cancelled")]
34    Cancelled,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38/// Why a durable logical peer connection is currently disconnected.
39pub enum DisconnectReason {
40    ExplicitDisconnect,
41    NodeShutdown,
42    SessionRetired { reason: RetirementReason },
43    ReconnectFailed { error: ConnectError },
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47/// The current state of a durable logical peer connection.
48pub enum ConnectionStatus {
49    Connecting,
50    Connected,
51    Disconnected { reason: DisconnectReason },
52}
53
54#[derive(Clone)]
55struct SessionBinding {
56    session_id: String,
57    wire: Arc<Wire>,
58}
59
60struct ReconnectAttempt {
61    generation: u64,
62    cancellation: unb_runtime::CancellationToken,
63    completion: watch::Sender<Option<Result<(), ConnectError>>>,
64}
65
66pub(crate) struct ReconnectWait {
67    attempt: Arc<ReconnectAttempt>,
68}
69
70impl ReconnectWait {
71    pub(crate) async fn wait(self) -> Result<(), ConnectError> {
72        self.attempt.wait().await
73    }
74}
75
76impl ReconnectAttempt {
77    fn complete(&self, result: Result<(), ConnectError>) {
78        self.completion.send_if_modified(move |completion| {
79            if completion.is_some() {
80                false
81            } else {
82                *completion = Some(result);
83                true
84            }
85        });
86    }
87
88    fn wait(&self) -> impl std::future::Future<Output = Result<(), ConnectError>> + Send + 'static {
89        let mut completion = self.completion.subscribe();
90        async move {
91            loop {
92                if let Some(result) = completion.borrow().clone() {
93                    return result;
94                }
95                if completion.changed().await.is_err() {
96                    return Err(ConnectError::Cancelled);
97                }
98            }
99        }
100    }
101}
102
103#[derive(Clone)]
104/// A cloneable handle to one verified peer identity.
105///
106/// Initial [`Node::connect`](crate::Node::connect) eagerly establishes the
107/// session and synchronizes routes before returning this handle. Transport loss
108/// updates the handle but does not start background work. Call [`reconnect`](Self::reconnect)
109/// explicitly to perform one ordered endpoint sweep; concurrent calls share the
110/// same in-flight attempt and result.
111pub struct PeerConnection {
112    node: Weak<Node>,
113    peer: Arc<str>,
114    endpoints: Arc<RwLock<EndpointSet>>,
115    dialer: Arc<RwLock<Option<Arc<dyn EndpointDialer>>>>,
116    identity: Arc<RwLock<NodeIdentity>>,
117    session: Arc<Mutex<Option<SessionBinding>>>,
118    status: watch::Sender<ConnectionStatus>,
119    generation: Arc<AtomicU64>,
120    reconnect_attempt: Arc<Mutex<Option<Arc<ReconnectAttempt>>>>,
121}
122
123impl PeerConnection {
124    pub(crate) fn new(
125        node: Weak<Node>,
126        identity: NodeIdentity,
127        endpoints: EndpointSet,
128        session_id: String,
129        wire: Arc<Wire>,
130        dialer: Option<Arc<dyn EndpointDialer>>,
131    ) -> PeerConnection {
132        let (status, _) = watch::channel(ConnectionStatus::Connected);
133        PeerConnection {
134            node,
135            peer: Arc::from(identity.node_id.as_str()),
136            endpoints: Arc::new(RwLock::new(endpoints)),
137            dialer: Arc::new(RwLock::new(dialer)),
138            identity: Arc::new(RwLock::new(identity)),
139            session: Arc::new(Mutex::new(Some(SessionBinding { session_id, wire }))),
140            status,
141            generation: Arc::new(AtomicU64::new(0)),
142            reconnect_attempt: Arc::new(Mutex::new(None)),
143        }
144    }
145
146    pub fn peer(&self) -> &str {
147        &self.peer
148    }
149
150    pub fn status(&self) -> ConnectionStatus {
151        self.status.borrow().clone()
152    }
153
154    pub fn changed(&self) -> impl std::future::Future<Output = ConnectionStatus> + Send + 'static {
155        let mut receiver = self.status.subscribe();
156        async move {
157            if receiver.changed().await.is_err() {
158                return receiver.borrow().clone();
159            }
160            let status = receiver.borrow_and_update().clone();
161            status
162        }
163    }
164
165    pub fn disconnect(&self) {
166        let (attempt, binding) = {
167            let mut active = self
168                .reconnect_attempt
169                .lock()
170                .unwrap_or_else(|poisoned| poisoned.into_inner());
171            self.generation.fetch_add(1, Ordering::AcqRel);
172            let attempt = active.take();
173            self.publish(ConnectionStatus::Disconnected {
174                reason: DisconnectReason::ExplicitDisconnect,
175            });
176            let binding = self
177                .session
178                .lock()
179                .unwrap_or_else(|poisoned| poisoned.into_inner())
180                .as_ref()
181                .cloned();
182            (attempt, binding)
183        };
184        if let Some(attempt) = attempt {
185            attempt.cancellation.cancel();
186            attempt.complete(Err(ConnectError::Cancelled));
187        }
188        if let Some(binding) = binding {
189            binding.wire.shutdown();
190        }
191    }
192
193    pub async fn reconnect(&self) -> Result<(), ConnectError> {
194        if self.status() == ConnectionStatus::Connected {
195            return Ok(());
196        }
197        let (attempt, leader) = {
198            let mut active = self
199                .reconnect_attempt
200                .lock()
201                .unwrap_or_else(|poisoned| poisoned.into_inner());
202            if self.status() == ConnectionStatus::Connected {
203                return Ok(());
204            }
205            if let Some(attempt) = active.as_ref() {
206                (attempt.clone(), false)
207            } else {
208                let generation = self.generation.fetch_add(1, Ordering::AcqRel) + 1;
209                let (completion, _) = watch::channel(None);
210                let attempt = Arc::new(ReconnectAttempt {
211                    generation,
212                    cancellation: unb_runtime::CancellationToken::new(),
213                    completion,
214                });
215                *active = Some(attempt.clone());
216                self.publish(ConnectionStatus::Connecting);
217                (attempt, true)
218            }
219        };
220        if leader {
221            let connection = self.clone();
222            let running = attempt.clone();
223            unb_runtime::RuntimeHandle::current().spawn(async move {
224                connection.run_reconnect(running).await;
225            });
226        }
227        attempt.wait().await
228    }
229
230    pub(crate) fn owner(&self) -> Option<Arc<Node>> {
231        self.node.upgrade()
232    }
233
234    pub(crate) fn active_reconnect(&self) -> Option<ReconnectWait> {
235        self.reconnect_attempt
236            .lock()
237            .unwrap_or_else(|poisoned| poisoned.into_inner())
238            .as_ref()
239            .cloned()
240            .map(|attempt| ReconnectWait { attempt })
241    }
242
243    pub(crate) fn endpoints(&self) -> EndpointSet {
244        self.endpoints
245            .read()
246            .unwrap_or_else(|poisoned| poisoned.into_inner())
247            .clone()
248    }
249
250    pub(crate) fn replace_endpoints(&self, endpoints: EndpointSet) {
251        *self
252            .endpoints
253            .write()
254            .unwrap_or_else(|poisoned| poisoned.into_inner()) = endpoints;
255    }
256
257    pub(crate) fn replace_dialer(&self, dialer: Option<Arc<dyn EndpointDialer>>) {
258        *self
259            .dialer
260            .write()
261            .unwrap_or_else(|poisoned| poisoned.into_inner()) = dialer;
262    }
263
264    fn dialer(&self) -> Option<Arc<dyn EndpointDialer>> {
265        self.dialer
266            .read()
267            .unwrap_or_else(|poisoned| poisoned.into_inner())
268            .clone()
269    }
270
271    pub(crate) fn bind(&self, identity: NodeIdentity, session_id: String, wire: Arc<Wire>) {
272        let _active = self
273            .reconnect_attempt
274            .lock()
275            .unwrap_or_else(|poisoned| poisoned.into_inner());
276        self.install(identity, session_id, wire);
277    }
278
279    fn install(&self, identity: NodeIdentity, session_id: String, wire: Arc<Wire>) {
280        *self
281            .identity
282            .write()
283            .unwrap_or_else(|poisoned| poisoned.into_inner()) = identity;
284        *self
285            .session
286            .lock()
287            .unwrap_or_else(|poisoned| poisoned.into_inner()) =
288            Some(SessionBinding { session_id, wire });
289        self.publish(ConnectionStatus::Connected);
290    }
291
292    fn current_session(&self) -> Option<SessionBinding> {
293        self.session
294            .lock()
295            .unwrap_or_else(|poisoned| poisoned.into_inner())
296            .clone()
297    }
298
299    pub(crate) fn retire(&self, session_id: &str, reason: RetirementReason) {
300        let _active = self
301            .reconnect_attempt
302            .lock()
303            .unwrap_or_else(|poisoned| poisoned.into_inner());
304        let removed = {
305            let mut session = self
306                .session
307                .lock()
308                .unwrap_or_else(|poisoned| poisoned.into_inner());
309            if session
310                .as_ref()
311                .is_some_and(|binding| binding.session_id == session_id)
312            {
313                session.take()
314            } else {
315                None
316            }
317        };
318        if removed.is_some() && self.status() == ConnectionStatus::Connected {
319            self.publish(ConnectionStatus::Disconnected {
320                reason: DisconnectReason::SessionRetired { reason },
321            });
322        }
323    }
324
325    pub(crate) fn node_shutdown(&self) {
326        let (attempt, binding) = {
327            let mut active = self
328                .reconnect_attempt
329                .lock()
330                .unwrap_or_else(|poisoned| poisoned.into_inner());
331            self.generation.fetch_add(1, Ordering::AcqRel);
332            let attempt = active.take();
333            self.publish(ConnectionStatus::Disconnected {
334                reason: DisconnectReason::NodeShutdown,
335            });
336            let binding = self
337                .session
338                .lock()
339                .unwrap_or_else(|poisoned| poisoned.into_inner())
340                .take();
341            (attempt, binding)
342        };
343        if let Some(attempt) = attempt {
344            attempt.cancellation.cancel();
345            attempt.complete(Err(ConnectError::NodeShutdown));
346        }
347        if let Some(binding) = binding {
348            binding.wire.shutdown();
349        }
350    }
351
352    pub(crate) fn publish(&self, status: ConnectionStatus) {
353        if *self.status.borrow() != status {
354            self.status.send_replace(status);
355        }
356    }
357
358    async fn run_reconnect(&self, attempt: Arc<ReconnectAttempt>) {
359        let result = async {
360            let Some(node) = self.owner() else {
361                return Err(ConnectError::NodeShutdown);
362            };
363            if node.cancellation().is_cancelled() {
364                return Err(ConnectError::NodeShutdown);
365            }
366            if let Some(binding) = self.current_session() {
367                tokio::select! {
368                    biased;
369                    () = attempt.cancellation.cancelled() => return Err(ConnectError::Cancelled),
370                    () = binding.wire.closed() => {}
371                }
372                loop {
373                    if node.session(&binding.session_id).await.is_none() {
374                        break;
375                    }
376                    tokio::select! {
377                        biased;
378                        () = attempt.cancellation.cancelled() => return Err(ConnectError::Cancelled),
379                        () = tokio::task::yield_now() => {}
380                    }
381                }
382            }
383            node.reconnect_peer(self.peer(), &self.endpoints(), self.dialer())
384                .await
385        }
386        .await;
387        let completion = {
388            let mut active = self
389                .reconnect_attempt
390                .lock()
391                .unwrap_or_else(|poisoned| poisoned.into_inner());
392            let current = active
393                .as_ref()
394                .is_some_and(|current| Arc::ptr_eq(current, &attempt));
395            if !current
396                || attempt.cancellation.is_cancelled()
397                || self.generation.load(Ordering::Acquire) != attempt.generation
398            {
399                if let Ok(candidate) = result {
400                    candidate.candidate_wire.shutdown();
401                }
402                None
403            } else {
404                let result = match result {
405                    Ok(candidate) => {
406                        self.install(
407                            candidate.identity,
408                            candidate.selected.session_id,
409                            candidate.selected.wire,
410                        );
411                        Ok(())
412                    }
413                    Err(_error) if self.status() == ConnectionStatus::Connected => Ok(()),
414                    Err(error) => {
415                        self.publish(ConnectionStatus::Disconnected {
416                            reason: DisconnectReason::ReconnectFailed {
417                                error: error.clone(),
418                            },
419                        });
420                        Err(error)
421                    }
422                };
423                active.take();
424                Some(result)
425            }
426        };
427        if let Some(result) = completion {
428            attempt.complete(result);
429        }
430    }
431}