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)]
13pub 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)]
38pub enum DisconnectReason {
40 ExplicitDisconnect,
41 NodeShutdown,
42 SessionRetired { reason: RetirementReason },
43 ReconnectFailed { error: ConnectError },
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub 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)]
104pub 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}