1use std::collections::HashMap;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
10use std::time::SystemTime;
11
12use arc_swap::ArcSwapOption;
13use bytes::Bytes;
14use futures::{SinkExt, StreamExt};
15use parking_lot::Mutex;
16use tokio::net::TcpStream;
17#[cfg(test)]
18use tokio::sync::oneshot;
19use tokio::sync::{Mutex as AsyncMutex, mpsc, watch};
20use tokio_util::codec::Framed;
21
22use futu_core::error::{FutuError, Result};
23use futu_core::log_redact::endpoint_log_fingerprint;
24
25#[cfg(test)]
26use crate::nn_codec::NNCodec;
27use crate::nn_codec::NNFrame;
28
29mod diagnostics;
30mod drop_impl;
31mod inbound;
32mod lifecycle;
33mod outbound;
34mod protocol_identity;
35mod request;
36#[cfg(feature = "test-util")]
37mod test_util;
38
39#[cfg(test)]
40use diagnostics::InboundFrameStage;
41use diagnostics::{
42 EndpointFingerprint, InboundProgress, ObservedNNCodec, PendingFailureKind,
43 TcpLoginTransportStage, trace_tcp_login_transport_stage,
44};
45#[cfg(test)]
46use diagnostics::{PendingFailureFacts, PendingRegistrationIdentity, PendingResponseEntry};
47
48use inbound::{InboundFrameDecision, decode_inbound_frame_body, is_ex_head_error};
49#[cfg(any(test, feature = "test-util"))]
50pub use lifecycle::LifecyclePauseHook;
51#[cfg(test)]
52use lifecycle::PendingRegistrationGuard;
53use lifecycle::{
54 ConnectionLifecycle, ConnectionTerminationFacts, DiagnosticSink, PendingResponses,
55 claim_termination_and_fail_all_pending, emit_diagnostic, mark_disconnected, no_diagnostic_sink,
56 pending_failure_facts,
57};
58pub use protocol_identity::BackendProtocolIdentity;
59pub(crate) use request::{BackendWriterAdmission, RequestTimeoutPolicy, WriterAdmissionObserver};
60
61static NEXT_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1);
62
63pub struct BackendConn {
65 serial_no: AtomicU32,
66 sec_data: AtomicU32,
67 outbound_order: AsyncMutex<()>,
78 connected: Arc<std::sync::atomic::AtomicBool>,
79 connected_tx: watch::Sender<bool>,
80 session_key: SharedSessionKey,
86 previous_session_key: SharedSessionKey,
89 cmd_tx: mpsc::Sender<BackendCmd>,
90 pending: PendingResponses,
91 termination_lifecycle: Arc<ConnectionLifecycle>,
92 endpoint_fingerprint: EndpointFingerprint,
93 connection_generation: u64,
94 inbound_progress: Arc<InboundProgress>,
95 diagnostic_sink: DiagnosticSink,
96 shutdown_tx: watch::Sender<bool>,
97 pub user_id: AtomicU32,
98 client_ip: Mutex<String>,
103 protocol_identity: BackendProtocolIdentity,
104 pub client_ver: u16,
105}
106
107enum BackendCmd {
108 Send {
109 frame: NNFrame,
110 writer_admitted: Option<Arc<AtomicBool>>,
111 },
112}
113type SharedSessionKey = Arc<ArcSwapOption<Vec<u8>>>;
114
115pub type PushCallback = Arc<dyn Fn(u64, u16, Bytes, SystemTime) + Send + Sync + 'static>;
122
123impl BackendConn {
124 pub const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
131
132 pub const CLIENT_VER_FTGTW: u16 = 1031;
143
144 async fn establish_stream(addr: &str, timeout: std::time::Duration) -> Result<TcpStream> {
146 let addr_fp = endpoint_log_fingerprint(addr);
147 let stream = match tokio::time::timeout(timeout, TcpStream::connect(addr)).await {
148 Ok(Ok(s)) => s,
149 Ok(Err(error)) => {
150 return Err(FutuError::Network(std::io::Error::new(
151 error.kind(),
152 format!("backend_connect_failed endpoint_fingerprint={addr_fp}"),
153 )));
154 }
155 Err(_elapsed) => {
156 return Err(FutuError::Network(std::io::Error::new(
157 std::io::ErrorKind::TimedOut,
158 format!("backend_connect_timeout endpoint_fingerprint={addr_fp}"),
159 )));
160 }
161 };
162 stream.set_nodelay(true).map_err(|error| {
163 FutuError::Network(std::io::Error::new(
164 error.kind(),
165 format!("backend_socket_config_failed endpoint_fingerprint={addr_fp}"),
166 ))
167 })?;
168 Ok(stream)
169 }
170
171 pub async fn connect(
173 addr: &str,
174 push_callback: PushCallback,
175 protocol_identity: BackendProtocolIdentity,
176 ) -> Result<Self> {
177 let stream = Self::establish_stream(addr, Self::CONNECT_TIMEOUT).await?;
178 tracing::info!(
179 addr_fp = %endpoint_log_fingerprint(addr),
180 "connected to backend"
181 );
182 Ok(Self::from_stream(
183 stream,
184 push_callback,
185 EndpointFingerprint::from_endpoint(addr),
186 protocol_identity,
187 ))
188 }
189
190 pub async fn connect_race(
200 addrs: &[String],
201 push_callback: PushCallback,
202 protocol_identity: BackendProtocolIdentity,
203 ) -> Result<(Self, String)> {
204 use futures::stream::{FuturesUnordered, StreamExt};
205
206 if addrs.is_empty() {
207 return Err(FutuError::Network(std::io::Error::new(
208 std::io::ErrorKind::InvalidInput,
209 "connect_race: empty address list",
210 )));
211 }
212
213 tracing::info!(
214 candidates = addrs.len(),
215 candidate_fps = ?addrs
216 .iter()
217 .map(|addr| endpoint_log_fingerprint(addr))
218 .collect::<Vec<_>>(),
219 "racing parallel connects"
220 );
221
222 let mut attempts: FuturesUnordered<_> = addrs
223 .iter()
224 .cloned()
225 .map(|addr| async move {
226 let result = Self::establish_stream(&addr, Self::CONNECT_TIMEOUT).await;
227 (addr, result)
228 })
229 .collect();
230
231 let mut last_err: Option<FutuError> = None;
232 while let Some((addr, result)) = attempts.next().await {
233 match result {
234 Ok(stream) => {
235 tracing::info!(
236 addr_fp = %endpoint_log_fingerprint(&addr),
237 remaining_losers = attempts.len(),
238 "connect race winner"
239 );
240 drop(attempts); let conn = Self::from_stream(
242 stream,
243 push_callback,
244 EndpointFingerprint::from_endpoint(&addr),
245 protocol_identity,
246 );
247 return Ok((conn, addr));
248 }
249 Err(e) => {
250 tracing::debug!(
251 addr_fp = %endpoint_log_fingerprint(&addr),
252 error = %e,
253 "candidate failed"
254 );
255 last_err = Some(e);
256 }
257 }
258 }
259
260 Err(last_err.unwrap_or_else(|| {
261 FutuError::Network(std::io::Error::other("connect_race: all candidates failed"))
262 }))
263 }
264
265 #[cfg(feature = "test-util")]
270 pub fn from_duplex(
271 stream: tokio::io::DuplexStream,
272 push_callback: PushCallback,
273 protocol_identity: BackendProtocolIdentity,
274 ) -> Self {
275 Self::from_stream_inner(stream, push_callback, protocol_identity)
276 }
277
278 fn from_stream(
280 stream: TcpStream,
281 push_callback: PushCallback,
282 endpoint_fingerprint: EndpointFingerprint,
283 protocol_identity: BackendProtocolIdentity,
284 ) -> Self {
285 Self::from_stream_with_connection_diagnostics(
286 stream,
287 push_callback,
288 endpoint_fingerprint,
289 protocol_identity,
290 no_diagnostic_sink(),
291 )
292 }
293
294 #[cfg(any(test, feature = "test-util"))]
297 pub(crate) fn from_stream_inner<S>(
298 stream: S,
299 push_callback: PushCallback,
300 protocol_identity: BackendProtocolIdentity,
301 ) -> Self
302 where
303 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
304 {
305 Self::from_stream_with_connection_diagnostics(
306 stream,
307 push_callback,
308 EndpointFingerprint::from_endpoint("test-peer"),
309 protocol_identity,
310 no_diagnostic_sink(),
311 )
312 }
313
314 #[cfg(test)]
315 pub(crate) fn from_stream_inner_default_futunn_chs_for_test<S>(
316 stream: S,
317 push_callback: PushCallback,
318 ) -> Self
319 where
320 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
321 {
322 Self::from_stream_inner(stream, push_callback, BackendProtocolIdentity::new(40, 0))
323 }
324
325 pub(crate) fn from_stream_with_endpoint<S>(
329 stream: S,
330 push_callback: PushCallback,
331 endpoint: &str,
332 protocol_identity: BackendProtocolIdentity,
333 ) -> Self
334 where
335 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
336 {
337 Self::from_stream_with_connection_diagnostics(
338 stream,
339 push_callback,
340 EndpointFingerprint::from_endpoint(endpoint),
341 protocol_identity,
342 no_diagnostic_sink(),
343 )
344 }
345
346 #[cfg(test)]
347 pub(crate) fn from_stream_inner_with_diagnostics_default_futunn_chs_for_test<S>(
348 stream: S,
349 push_callback: PushCallback,
350 ) -> (Self, mpsc::Receiver<PendingFailureFacts>)
351 where
352 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
353 {
354 const DIAGNOSTIC_CAPACITY: usize = 16;
355 let (diagnostic_tx, diagnostic_rx) = mpsc::channel(DIAGNOSTIC_CAPACITY);
356 let conn = Self::from_stream_with_connection_diagnostics(
357 stream,
358 push_callback,
359 EndpointFingerprint::from_endpoint("test-peer"),
360 BackendProtocolIdentity::new(40, 0),
361 Some(diagnostic_tx),
362 );
363 (conn, diagnostic_rx)
364 }
365
366 fn from_stream_with_connection_diagnostics<S>(
367 stream: S,
368 push_callback: PushCallback,
369 endpoint_fingerprint: EndpointFingerprint,
370 protocol_identity: BackendProtocolIdentity,
371 diagnostic_sink: DiagnosticSink,
372 ) -> Self
373 where
374 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
375 {
376 let connection_generation = NEXT_CONNECTION_GENERATION.fetch_add(1, Ordering::Relaxed);
377 let codec = ObservedNNCodec::new_with_connection_identity(
378 endpoint_fingerprint.clone(),
379 connection_generation,
380 );
381 let inbound_progress = codec.inbound_progress();
382 let framed = Framed::new(stream, codec);
383 let (mut sink, mut stream_rx) = framed.split();
384
385 let (cmd_tx, mut cmd_rx) = mpsc::channel::<BackendCmd>(256);
386
387 let pending: PendingResponses = Arc::new(Mutex::new(HashMap::new()));
388 let pending_recv = pending.clone();
389 let pending_send = pending.clone();
390 let termination_lifecycle = Arc::new(ConnectionLifecycle::new());
391 let termination_lifecycle_recv = Arc::clone(&termination_lifecycle);
392 let termination_lifecycle_send = Arc::clone(&termination_lifecycle);
393 let endpoint_fingerprint_recv = endpoint_fingerprint.clone();
394 let endpoint_fingerprint_send = endpoint_fingerprint.clone();
395 let inbound_progress_recv = Arc::clone(&inbound_progress);
396 let inbound_progress_send = Arc::clone(&inbound_progress);
397 let diagnostic_sink_recv = diagnostic_sink.clone();
398 let diagnostic_sink_send = diagnostic_sink.clone();
399 let connected = Arc::new(std::sync::atomic::AtomicBool::new(true));
400 let (connected_tx, _) = watch::channel(true);
401 let (shutdown_tx, mut shutdown_rx_recv) = watch::channel(false);
402 let mut shutdown_rx_send = shutdown_tx.subscribe();
403 let connected_recv = Arc::clone(&connected);
404 let connected_send = Arc::clone(&connected);
405 let connected_tx_recv = connected_tx.clone();
406 let connected_tx_send = connected_tx.clone();
407 let shutdown_tx_recv = shutdown_tx.clone();
408 let shutdown_tx_send = shutdown_tx.clone();
409 let session_key: SharedSessionKey = Arc::new(ArcSwapOption::empty());
410 let session_key_for_recv = session_key.clone();
411 let previous_session_key: SharedSessionKey = Arc::new(ArcSwapOption::empty());
412 let previous_session_key_for_recv = previous_session_key.clone();
413
414 tokio::spawn(async move {
416 let termination = loop {
417 let next_frame = tokio::select! {
418 _ = shutdown_rx_recv.changed() => {
419 break ConnectionTerminationFacts {
420 kind: PendingFailureKind::Shutdown,
421 endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
422 connection_generation,
423 progress: inbound_progress_recv.snapshot(),
424 diagnostic_sink: diagnostic_sink_recv.clone(),
425 };
426 }
427 next = stream_rx.next() => next,
428 };
429 let mut frame = match next_frame {
430 Some(Ok(frame)) => frame,
431 Some(Err(_error)) => {
432 break ConnectionTerminationFacts {
433 kind: PendingFailureKind::CodecError,
434 endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
435 connection_generation,
436 progress: inbound_progress_recv.snapshot(),
437 diagnostic_sink: diagnostic_sink_recv.clone(),
438 };
439 }
440 None => {
441 break ConnectionTerminationFacts {
442 kind: PendingFailureKind::PeerEof,
443 endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
444 connection_generation,
445 progress: inbound_progress_recv.snapshot(),
446 diagnostic_sink: diagnostic_sink_recv.clone(),
447 };
448 }
449 };
450
451 trace_tcp_login_transport_stage(
452 frame.header.cmd_id,
453 frame.header.serial_no,
454 TcpLoginTransportStage::FrameComplete,
455 );
456
457 tracing::debug!(
459 cmd_id = frame.header.cmd_id,
460 serial_no = frame.header.serial_no,
461 is_push = frame.header.is_push,
462 is_compressed = frame.header.is_compressed,
463 ex_head_len = frame.header.ex_head_len,
464 body_len = frame.header.body_len,
465 actual_body_len = frame.body.len(),
466 "recv frame"
467 );
468
469 if let Some(err) = frame.parse_ex_head_error()
477 && is_ex_head_error(&err)
478 {
479 let is_soft_fail = err.code == -102;
485 if is_soft_fail {
486 tracing::debug!(
487 cmd_id = frame.header.cmd_id,
488 code = err.code,
489 source = %err.source,
490 message = %err.message,
491 "server: cmd not available on this channel (soft-fail)"
492 );
493 } else {
494 tracing::warn!(
495 cmd_id = frame.header.cmd_id,
496 cmd_result = err.cmd_result,
497 code = err.code,
498 source = %err.source,
499 message = %err.message,
500 "server returned err_info in ex_head"
501 );
502 }
503 }
504
505 if !frame.header.is_push {
512 let mismatched_entry = {
513 let mut pending = pending_recv.lock();
514 match pending.get(&frame.header.serial_no) {
515 Some(entry) if entry.cmd_id != frame.header.cmd_id => {
516 pending.remove(&frame.header.serial_no)
517 }
518 _ => None,
519 }
520 };
521 if let Some(entry) = mismatched_entry {
522 let mut progress = inbound_progress_recv.snapshot();
523 progress.codec_category = Some("response_cmd_mismatch");
524 let facts = pending_failure_facts(
525 PendingFailureKind::CodecError,
526 entry.cmd_id,
527 entry.serial_no,
528 entry.writer_admitted.load(Ordering::Acquire),
529 &endpoint_fingerprint_recv,
530 connection_generation,
531 &progress,
532 )
533 .with_response_cmd_id(frame.header.cmd_id);
534 emit_diagnostic(&diagnostic_sink_recv, &facts);
535 if entry.tx.send(Err(facts)).is_err() {
536 tracing::debug!(
537 cmd_id = entry.cmd_id,
538 response_cmd_id = frame.header.cmd_id,
539 serial_no = entry.serial_no,
540 "pending response receiver dropped before cmd mismatch delivery"
541 );
542 }
543 continue;
544 }
545 }
546
547 let session_key = session_key_for_recv.load_full();
548 let previous_session_key = previous_session_key_for_recv.load_full();
549 if decode_inbound_frame_body(
550 &mut frame,
551 session_key.as_deref().map(Vec::as_slice),
552 previous_session_key.as_deref().map(Vec::as_slice),
553 ) == InboundFrameDecision::Drop
554 {
555 if !frame.header.is_push {
556 let mut progress = inbound_progress_recv.snapshot();
557 progress.codec_category = Some("decode_error");
558 if let Some(entry) = pending_recv.lock().remove(&frame.header.serial_no) {
559 trace_tcp_login_transport_stage(
560 entry.cmd_id,
561 entry.serial_no,
562 TcpLoginTransportStage::SerialMatched,
563 );
564 let facts = pending_failure_facts(
565 PendingFailureKind::CodecError,
566 entry.cmd_id,
567 entry.serial_no,
568 entry.writer_admitted.load(Ordering::Acquire),
569 &endpoint_fingerprint_recv,
570 connection_generation,
571 &progress,
572 );
573 emit_diagnostic(&diagnostic_sink_recv, &facts);
574 if entry.tx.send(Err(facts)).is_err() {
575 tracing::debug!(
576 cmd_id = entry.cmd_id,
577 serial_no = entry.serial_no,
578 "pending response receiver dropped before inbound decode failure delivery"
579 );
580 }
581 } else {
582 let facts = pending_failure_facts(
583 PendingFailureKind::UnmatchedSerial,
584 frame.header.cmd_id,
585 frame.header.serial_no,
586 false,
587 &endpoint_fingerprint_recv,
588 connection_generation,
589 &progress,
590 );
591 emit_diagnostic(&diagnostic_sink_recv, &facts);
592 }
593 }
594 continue;
595 }
596
597 let is_push = frame.header.is_push
602 || (frame.header.serial_no == 0 && !pending_recv.lock().contains_key(&0));
603 if is_push {
604 tracing::debug!(
605 cmd_id = frame.header.cmd_id,
606 body_len = frame.body.len(),
607 is_push = frame.header.is_push,
608 is_compressed = frame.header.is_compressed,
609 reserved = ?frame.header.reserved,
610 "backend push received"
611 );
612 push_callback(
613 connection_generation,
614 frame.header.cmd_id,
615 frame.body,
616 SystemTime::now(),
617 );
618 } else {
619 let entry = pending_recv.lock().remove(&frame.header.serial_no);
620 if let Some(entry) = entry {
621 trace_tcp_login_transport_stage(
622 entry.cmd_id,
623 entry.serial_no,
624 TcpLoginTransportStage::SerialMatched,
625 );
626 if let Err(Ok(_frame)) = entry.tx.send(Ok(frame)) {
627 tracing::debug!(
628 cmd_id = entry.cmd_id,
629 serial_no = entry.serial_no,
630 "backend response receiver dropped before frame delivery"
631 );
632 }
633 } else {
634 let progress = inbound_progress_recv.snapshot();
635 let facts = pending_failure_facts(
636 PendingFailureKind::UnmatchedSerial,
637 frame.header.cmd_id,
638 frame.header.serial_no,
639 false,
640 &endpoint_fingerprint_recv,
641 connection_generation,
642 &progress,
643 );
644 emit_diagnostic(&diagnostic_sink_recv, &facts);
645 }
646 }
647 };
648 claim_termination_and_fail_all_pending(
650 &termination_lifecycle_recv,
651 &connected_recv,
652 &connected_tx_recv,
653 &shutdown_tx_recv,
654 &pending_recv,
655 &termination,
656 );
657 });
658
659 tokio::spawn(async move {
661 loop {
662 if termination_lifecycle_send.is_terminated() {
663 break;
664 }
665 let next_cmd = tokio::select! {
666 biased;
667 _ = shutdown_rx_send.changed() => {
668 break;
669 }
670 cmd = cmd_rx.recv() => cmd,
671 };
672 let Some(cmd) = next_cmd else {
673 break;
674 };
675 let Some(writer_activity) = termination_lifecycle_send.try_begin_writer() else {
676 break;
677 };
678
679 match cmd {
680 BackendCmd::Send {
681 frame,
682 writer_admitted,
683 } => {
684 if let Some(writer_admitted) = writer_admitted {
688 writer_admitted.store(true, Ordering::Release);
689 trace_tcp_login_transport_stage(
690 frame.header.cmd_id,
691 frame.header.serial_no,
692 TcpLoginTransportStage::WriterAdmitted,
693 );
694 }
695 if sink.send(frame).await.is_err() {
696 drop(writer_activity);
697 let termination = ConnectionTerminationFacts {
698 kind: PendingFailureKind::SendError,
699 endpoint_fingerprint: endpoint_fingerprint_send.clone(),
700 connection_generation,
701 progress: inbound_progress_send.snapshot(),
702 diagnostic_sink: diagnostic_sink_send.clone(),
703 };
704 claim_termination_and_fail_all_pending(
705 &termination_lifecycle_send,
706 &connected_send,
707 &connected_tx_send,
708 &shutdown_tx_send,
709 &pending_send,
710 &termination,
711 );
712 break;
713 }
714 }
715 }
716 }
717 });
718
719 Self {
720 serial_no: AtomicU32::new(0),
721 sec_data: AtomicU32::new(1),
722 outbound_order: AsyncMutex::new(()),
723 connected,
724 connected_tx,
725 session_key, previous_session_key,
727 cmd_tx,
728 pending,
729 termination_lifecycle,
730 endpoint_fingerprint,
731 connection_generation,
732 inbound_progress,
733 diagnostic_sink,
734 shutdown_tx,
735 user_id: AtomicU32::new(0),
736 client_ip: Mutex::new(String::new()),
737 protocol_identity,
738 client_ver: Self::CLIENT_VER_FTGTW,
739 }
740 }
741
742 pub const fn protocol_identity(&self) -> BackendProtocolIdentity {
743 self.protocol_identity
744 }
745
746 pub fn set_session_key(&self, key: Vec<u8>) {
750 let previous = self.session_key.swap(Some(Arc::new(key)));
751 self.previous_session_key.store(previous);
752 }
753
754 pub fn set_sec_data(&self, val: u32) {
756 self.sec_data.store(val, Ordering::Relaxed);
757 }
758
759 pub fn apply_login_sec_data(&self, val: Option<u32>) {
766 if let Some(val) = val {
767 self.set_sec_data(val);
768 }
769 }
770
771 pub fn set_client_ip(&self, ip: String) {
773 *self.client_ip.lock() = ip;
774 }
775
776 pub fn client_ip(&self) -> String {
778 self.client_ip.lock().clone()
779 }
780
781 pub fn connection_generation(&self) -> u64 {
784 self.connection_generation
785 }
786
787 #[cfg(any(test, feature = "test-util"))]
788 pub fn pause_writer_admission_for_test(&self) -> LifecyclePauseHook {
789 let hook = LifecyclePauseHook::default();
790 *self.termination_lifecycle.admission_publish_hook.lock() = Some(hook.clone());
791 hook
792 }
793
794 pub fn is_connected(&self) -> bool {
795 self.connected.load(Ordering::Acquire)
796 }
797
798 pub(crate) fn has_session_key(&self) -> bool {
799 self.session_key
800 .load_full()
801 .is_some_and(|key| !key.is_empty())
802 }
803
804 pub fn subscribe_connection_state(&self) -> watch::Receiver<bool> {
812 self.connected_tx.subscribe()
813 }
814
815 pub fn mark_disconnected_for_reconnect(&self, reason: &'static str) {
823 tracing::warn!(
824 reason,
825 "marking backend connection disconnected to trigger reconnect"
826 );
827 let termination = ConnectionTerminationFacts {
828 kind: PendingFailureKind::Shutdown,
832 endpoint_fingerprint: self.endpoint_fingerprint.clone(),
833 connection_generation: self.connection_generation,
834 progress: self.inbound_progress.snapshot(),
835 diagnostic_sink: self.diagnostic_sink.clone(),
836 };
837 claim_termination_and_fail_all_pending(
838 &self.termination_lifecycle,
839 &self.connected,
840 &self.connected_tx,
841 &self.shutdown_tx,
842 &self.pending,
843 &termination,
844 );
845 }
846}
847
848#[cfg(test)]
849mod tests;