1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
4
5use parking_lot::Mutex;
6#[cfg(test)]
7use tokio::sync::mpsc;
8use tokio::sync::watch;
9
10use super::diagnostics::{
11 EndpointFingerprint, InboundProgressSnapshot, PendingFailureFacts, PendingFailureKind,
12 PendingRegistrationIdentity, PendingResponseEntry, trace_pending_failure,
13};
14
15pub(super) type PendingResponses = Arc<Mutex<HashMap<u32, PendingResponseEntry>>>;
16
17pub(super) struct PendingRegistrationGuard {
18 pub(super) pending: PendingResponses,
19 pub(super) serial_no: u32,
20 pub(super) registration_identity: Arc<PendingRegistrationIdentity>,
21}
22
23impl Drop for PendingRegistrationGuard {
24 fn drop(&mut self) {
25 remove_pending_registration_if_current(
26 &self.pending,
27 self.serial_no,
28 &self.registration_identity,
29 );
30 }
31}
32
33fn remove_pending_registration_if_current(
34 pending: &PendingResponses,
35 serial_no: u32,
36 registration_identity: &Arc<PendingRegistrationIdentity>,
37) -> Option<PendingResponseEntry> {
38 let mut pending = pending.lock();
39 let is_current = pending
40 .get(&serial_no)
41 .is_some_and(|entry| Arc::ptr_eq(&entry.registration_identity, registration_identity));
42 if is_current {
43 pending.remove(&serial_no)
44 } else {
45 None
46 }
47}
48
49#[cfg(test)]
50pub(super) type DiagnosticSink = Option<mpsc::Sender<PendingFailureFacts>>;
51
52#[cfg(not(test))]
53#[derive(Clone)]
54pub(super) struct DiagnosticSink;
55
56#[derive(Clone)]
57pub(super) struct ConnectionTerminationFacts {
58 pub(super) kind: PendingFailureKind,
59 pub(super) endpoint_fingerprint: EndpointFingerprint,
60 pub(super) connection_generation: u64,
61 pub(super) progress: InboundProgressSnapshot,
62 pub(super) diagnostic_sink: DiagnosticSink,
63}
64
65pub(super) struct ConnectionLifecycle {
66 state: AtomicU8,
67 #[cfg(any(test, feature = "test-util"))]
68 pub(super) admission_publish_hook: Mutex<Option<LifecyclePauseHook>>,
69 #[cfg(test)]
70 pub(super) termination_drain_hook: Mutex<Option<LifecyclePauseHook>>,
71}
72
73#[cfg(any(test, feature = "test-util"))]
74#[derive(Clone, Default)]
75pub struct LifecyclePauseHook {
76 pub(super) entered: Arc<AtomicBool>,
77 pub(super) release: Arc<AtomicBool>,
78}
79
80#[cfg(any(test, feature = "test-util"))]
81impl LifecyclePauseHook {
82 pub fn entered(&self) -> bool {
83 self.entered.load(Ordering::Acquire)
84 }
85
86 pub fn release(&self) {
87 self.release.store(true, Ordering::Release);
88 }
89}
90
91impl ConnectionLifecycle {
92 const TERMINATED: u8 = 1 << 0;
93 const ADMISSION_ACTIVE: u8 = 1 << 1;
94 const WRITER_ACTIVE: u8 = 1 << 2;
95
96 pub(super) fn new() -> Self {
97 Self {
98 state: AtomicU8::new(0),
99 #[cfg(any(test, feature = "test-util"))]
100 admission_publish_hook: Mutex::new(None),
101 #[cfg(test)]
102 termination_drain_hook: Mutex::new(None),
103 }
104 }
105
106 pub(super) fn is_terminated(&self) -> bool {
107 self.state.load(Ordering::Acquire) & Self::TERMINATED != 0
108 }
109
110 fn claim_termination(&self) -> bool {
111 self.state.fetch_or(Self::TERMINATED, Ordering::AcqRel) & Self::TERMINATED == 0
112 }
113
114 pub(super) fn try_begin_writer(self: &Arc<Self>) -> Option<LifecycleActivityGuard> {
115 self.try_begin(Self::WRITER_ACTIVE)
116 }
117
118 pub(super) fn try_begin_admission(self: &Arc<Self>) -> Option<LifecycleActivityGuard> {
119 self.try_begin(Self::ADMISSION_ACTIVE)
120 }
121
122 #[cfg(any(test, feature = "test-util"))]
123 pub(super) fn pause_admission_publish_for_test(&self) {
124 Self::pause_for_test(self.admission_publish_hook.lock().clone());
125 }
126
127 #[cfg(test)]
128 fn pause_termination_drain_for_test(&self) {
129 Self::pause_for_test(self.termination_drain_hook.lock().clone());
130 }
131
132 #[cfg(any(test, feature = "test-util"))]
133 fn pause_for_test(hook: Option<LifecyclePauseHook>) {
134 if let Some(hook) = hook {
135 hook.entered.store(true, Ordering::Release);
136 while !hook.release.load(Ordering::Acquire) {
137 std::thread::yield_now();
138 }
139 }
140 }
141
142 fn try_begin(self: &Arc<Self>, activity: u8) -> Option<LifecycleActivityGuard> {
143 let mut current = self.state.load(Ordering::Acquire);
144 loop {
145 if current & Self::TERMINATED != 0 {
146 return None;
147 }
148 debug_assert_eq!(current & activity, 0);
149 match self.state.compare_exchange_weak(
150 current,
151 current | activity,
152 Ordering::AcqRel,
153 Ordering::Acquire,
154 ) {
155 Ok(_) => {
156 return Some(LifecycleActivityGuard {
157 lifecycle: Arc::clone(self),
158 activity,
159 });
160 }
161 Err(observed) => current = observed,
162 }
163 }
164 }
165}
166
167pub(super) struct LifecycleActivityGuard {
168 lifecycle: Arc<ConnectionLifecycle>,
169 activity: u8,
170}
171
172impl Drop for LifecycleActivityGuard {
173 fn drop(&mut self) {
174 self.lifecycle
175 .state
176 .fetch_and(!self.activity, Ordering::AcqRel);
177 }
178}
179
180pub(super) fn mark_disconnected(
181 connected: &Arc<std::sync::atomic::AtomicBool>,
182 connected_tx: &watch::Sender<bool>,
183) {
184 let was_connected = connected.swap(false, Ordering::AcqRel);
185 if was_connected {
186 let _ = connected_tx.send(false);
187 }
188}
189
190#[cfg(test)]
191pub(super) fn no_diagnostic_sink() -> DiagnosticSink {
192 None
193}
194
195#[cfg(not(test))]
196pub(super) fn no_diagnostic_sink() -> DiagnosticSink {
197 DiagnosticSink
198}
199
200pub(super) fn emit_diagnostic(diagnostic_sink: &DiagnosticSink, facts: &PendingFailureFacts) {
201 trace_pending_failure(facts);
202
203 #[cfg(test)]
204 if let Some(tx) = diagnostic_sink
205 && tx.try_send(facts.clone()).is_err()
206 {
207 tracing::debug!("bounded backend diagnostic test sink unavailable");
208 }
209
210 #[cfg(not(test))]
211 let _ = diagnostic_sink;
212}
213
214pub(super) fn pending_failure_facts(
215 kind: PendingFailureKind,
216 cmd_id: u16,
217 serial_no: u32,
218 writer_admitted: bool,
219 endpoint_fingerprint: &EndpointFingerprint,
220 connection_generation: u64,
221 progress: &InboundProgressSnapshot,
222) -> PendingFailureFacts {
223 PendingFailureFacts::new(
224 kind,
225 cmd_id,
226 serial_no,
227 writer_admitted,
228 endpoint_fingerprint.clone(),
229 connection_generation,
230 progress.received_bytes,
231 progress.frame_stage,
232 progress.codec_category,
233 )
234}
235
236fn fail_all_pending(pending: &PendingResponses, base: &ConnectionTerminationFacts) {
237 let entries: Vec<PendingResponseEntry> = pending
238 .lock()
239 .drain()
240 .map(|(serial_no, entry)| {
241 debug_assert_eq!(serial_no, entry.serial_no);
242 entry
243 })
244 .collect();
245
246 fail_pending_entries(entries, base);
247}
248
249fn fail_pending_entries(entries: Vec<PendingResponseEntry>, base: &ConnectionTerminationFacts) {
250 for entry in entries {
251 let facts = pending_failure_facts(
252 base.kind,
253 entry.cmd_id,
254 entry.serial_no,
255 entry.writer_admitted.load(Ordering::Acquire),
256 &base.endpoint_fingerprint,
257 base.connection_generation,
258 &base.progress,
259 );
260 emit_diagnostic(&base.diagnostic_sink, &facts);
261 if entry.tx.send(Err(facts)).is_err() {
262 tracing::debug!(
263 cmd_id = entry.cmd_id,
264 serial_no = entry.serial_no,
265 "pending response receiver dropped before transport failure delivery"
266 );
267 }
268 }
269}
270
271pub(super) fn claim_response_timeout_and_fail_other_pending(
272 termination_lifecycle: &ConnectionLifecycle,
273 connected: &Arc<AtomicBool>,
274 connected_tx: &watch::Sender<bool>,
275 shutdown_tx: &watch::Sender<bool>,
276 pending: &PendingResponses,
277 timed_out_serial_no: u32,
278 timed_out_registration_identity: &Arc<PendingRegistrationIdentity>,
279 base: &ConnectionTerminationFacts,
280) -> bool {
281 let entries = {
285 let mut pending = pending.lock();
286 if !termination_lifecycle.claim_termination() {
287 return false;
288 }
289 let timed_out_entry_is_current = pending.get(&timed_out_serial_no).is_some_and(|entry| {
290 Arc::ptr_eq(
291 &entry.registration_identity,
292 timed_out_registration_identity,
293 )
294 });
295 if timed_out_entry_is_current {
296 pending.remove(&timed_out_serial_no);
297 }
298 mark_disconnected(connected, connected_tx);
299 pending
300 .drain()
301 .map(|(serial_no, entry)| {
302 debug_assert_eq!(serial_no, entry.serial_no);
303 entry
304 })
305 .collect()
306 };
307
308 let _ = shutdown_tx.send(true);
309 fail_pending_entries(entries, base);
310 true
311}
312
313pub(super) fn claim_termination_and_fail_all_pending(
314 termination_lifecycle: &ConnectionLifecycle,
315 connected: &Arc<AtomicBool>,
316 connected_tx: &watch::Sender<bool>,
317 shutdown_tx: &watch::Sender<bool>,
318 pending: &PendingResponses,
319 base: &ConnectionTerminationFacts,
320) -> bool {
321 if !termination_lifecycle.claim_termination() {
322 return false;
323 }
324 #[cfg(test)]
325 termination_lifecycle.pause_termination_drain_for_test();
326
327 let _ = shutdown_tx.send(true);
332
333 {
338 let _pending_guard = pending.lock();
339 mark_disconnected(connected, connected_tx);
340 }
341 fail_all_pending(pending, base);
342 true
343}