Skip to main content

spdy_mux/
stream.rs

1use std::future::Future;
2use std::io;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::sync::atomic::{
6    AtomicBool,
7    Ordering,
8};
9use std::task::{
10    Context,
11    Poll,
12};
13
14use bytes::{
15    Buf,
16    Bytes,
17    BytesMut,
18};
19use tokio::io::{
20    AsyncBufRead,
21    AsyncRead,
22    AsyncWrite,
23    ReadBuf,
24};
25use tokio::sync::mpsc;
26use tokio_util::sync::PollSender;
27
28use crate::error::Error;
29use crate::mux::{
30    MuxCommand,
31    MuxHandle,
32    SendWindow,
33    StreamRegistration,
34};
35
36/// Bidirectional SPDY/3.1 stream pair: a writable "data" stream plus an
37/// "error" stream half-closed at open time. The shape suits any peer that
38/// uses paired streams (Kubernetes port-forward is one such peer; the
39/// multiplexer treats the headers as opaque).
40///
41/// Streams are **lazily opened on the wire**: `MuxHandle::open_stream_pair`
42/// reserves a session slot and creates the per-stream channels, but no
43/// SPDY `SYN_STREAM` frame is sent until the consumer actually writes its
44/// first byte. This avoids the idle-upstream-close race for peers that
45/// dial an upstream connection eagerly on `SYN_STREAM` while preserving
46/// the pre-opened spare-stream throughput optimization.
47///
48/// Implements `AsyncRead + AsyncWrite` on the data half. The error half is
49/// available via `split()`.
50pub struct Stream {
51    state: StreamState,
52}
53
54enum StreamState {
55    /// Pair reserved, channels created, but no SPDY stream IDs allocated
56    /// and no `SYN_STREAM` on the wire yet. Transitions to `Opened` on the
57    /// first non-empty `poll_write`.
58    Unopened {
59        error_headers: Vec<(String, String)>,
60        data_headers: Vec<(String, String)>,
61        mux: MuxHandle,
62        data_rx: mpsc::Receiver<Bytes>,
63        error_rx: mpsc::Receiver<Bytes>,
64        /// Sender handed to the data worker at realize time. `Option` so it
65        /// can be moved out without re-creating the channel.
66        pending_data_tx: Option<mpsc::Sender<Bytes>>,
67        pending_error_tx: Option<mpsc::Sender<Bytes>>,
68        max_frame_size: u32,
69        read_buf: Option<Bytes>,
70        read_eof: bool,
71        /// In-flight lazy-open future. `Some` once `poll_write` started
72        /// a realize call and the first poll returned `Pending`. The
73        /// future owns a clone of the first payload so cancellation safety
74        /// is preserved across re-polls.
75        open_in_progress: Option<LazyOpenFuture>,
76        /// Guard that releases `active_pairs` on drop. Always present in
77        /// the Unopened state.
78        release_guard: Option<PairReleaseGuard>,
79    },
80    Opened {
81        data_id: u32,
82        data_rx: mpsc::Receiver<Bytes>,
83        error_rx: mpsc::Receiver<Bytes>,
84        mux: MuxHandle,
85        write_tx: PollSender<MuxCommand>,
86        send_window: Arc<SendWindow>,
87        max_frame_size: u32,
88        read_buf: Option<Bytes>,
89        read_eof: bool,
90        graceful_shutdown: Arc<AtomicBool>,
91        guard: StreamGuard,
92    },
93    /// Terminal state used while moving out of `Unopened` during realize.
94    /// Shouldn't be seen by a user.
95    Transitioning,
96}
97
98/// Boxed future driving a single lazy-open try.
99type LazyOpenFuture = Pin<Box<dyn Future<Output = Result<OpenedStreamParts, Error>> + Send>>;
100
101/// Guards the session's `active_pairs` counter for unopened streams.
102/// Once the stream realizes, the counter is owned by `StreamGuard` instead
103/// and this guard is disarmed so drop becomes a no-op.
104struct PairReleaseGuard {
105    mux: MuxHandle,
106    armed: bool,
107}
108
109impl PairReleaseGuard {
110    const fn new(mux: MuxHandle) -> Self {
111        Self { mux, armed: true }
112    }
113
114    /// Disarm without releasing. Use when ownership of the pair counter
115    /// transfers to a `StreamGuard`.
116    const fn disarm(&mut self) {
117        self.armed = false;
118    }
119}
120
121impl Drop for PairReleaseGuard {
122    fn drop(&mut self) {
123        if self.armed {
124            self.mux.release_pair();
125        }
126    }
127}
128
129/// Open-stream guard owning the IDs, drop permits, and graceful-shutdown
130/// flag. Replaces the previous `StreamGuard` and carries the same RST /
131/// worker-close contract.
132struct StreamGuard {
133    data_id: u32,
134    error_id: u32,
135    mux: MuxHandle,
136    ctrl_permit_error: Option<mpsc::OwnedPermit<MuxCommand>>,
137    ctrl_permit_data: Option<mpsc::OwnedPermit<MuxCommand>>,
138    close_reg_permit_error: Option<mpsc::OwnedPermit<StreamRegistration>>,
139    close_reg_permit_data: Option<mpsc::OwnedPermit<StreamRegistration>>,
140    /// Set to true when `poll_shutdown()` sends DATA+FIN (graceful half-close).
141    /// When true, `Drop` skips RST_STREAM for the data stream. The peer
142    /// already knows we're done writing and will close its end naturally.
143    /// This mirrors TCP semantics: shutdown(SHUT_WR) + close() sends FIN,
144    /// not RST.
145    graceful_shutdown: Arc<AtomicBool>,
146}
147
148/// RST_STREAM status code for CANCEL.
149const RST_STATUS_CANCEL: u32 = 5;
150
151impl Drop for StreamGuard {
152    fn drop(&mut self) {
153        // guaranteed path: use pre-reserved permits for infallible delivery.
154        // OwnedPermit::send() is synchronous, so no async is needed in Drop.
155        let graceful = self.graceful_shutdown.load(Ordering::Acquire);
156
157        // the error stream is already half-closed at open time: the
158        // `OpenPortForwardAndWrite` writer command emitted an empty
159        // DATA+FIN on `error_id` right after the two SYN_STREAM frames
160        // (matching kubectl's `errorStream.Close()` behavior). Sending
161        // RST_STREAM here would be wrong — we never use the error stream
162        // for writes after open, and the peer interprets RST_STREAM as
163        // an abnormal termination. Drop the permit unused.
164        //
165        // data stream: skip RST if poll_shutdown() already sent DATA+FIN
166        // (graceful half-close).
167        let _ = self.ctrl_permit_error.take();
168        if !graceful && let Some(permit) = self.ctrl_permit_data.take() {
169            permit.send(MuxCommand::CloseStream {
170                stream_id: self.data_id,
171                status: RST_STATUS_CANCEL,
172            });
173        }
174
175        // 2. notify workers via close-reg channels (stream entry cleanup and
176        //    send-window poisoning).
177        if let Some(permit) = self.close_reg_permit_error.take() {
178            permit.send(StreamRegistration::Close {
179                stream_id: self.error_id,
180            });
181        }
182        if let Some(permit) = self.close_reg_permit_data.take() {
183            permit.send(StreamRegistration::Close {
184                stream_id: self.data_id,
185            });
186        }
187
188        // 3. release the session slot.
189        self.mux.release_pair();
190    }
191}
192
193/// Runtime handles needed to construct a lazily-opened SPDY stream.
194pub(crate) struct UnopenedStreamParts {
195    pub error_headers: Vec<(String, String)>,
196    pub data_headers: Vec<(String, String)>,
197    pub mux: MuxHandle,
198    pub data_rx: mpsc::Receiver<Bytes>,
199    pub error_rx: mpsc::Receiver<Bytes>,
200    pub pending_data_tx: mpsc::Sender<Bytes>,
201    pub pending_error_tx: mpsc::Sender<Bytes>,
202    pub max_frame_size: u32,
203}
204
205/// Result of a successful realize call: the wire-visible bits a stream
206/// needs to switch into `Opened` state.
207pub(crate) struct OpenedStreamParts {
208    pub data_id: u32,
209    pub error_id: u32,
210    pub send_window: Arc<SendWindow>,
211    pub ctrl_permit_error: mpsc::OwnedPermit<MuxCommand>,
212    pub ctrl_permit_data: mpsc::OwnedPermit<MuxCommand>,
213    pub close_reg_permit_error: mpsc::OwnedPermit<StreamRegistration>,
214    pub close_reg_permit_data: mpsc::OwnedPermit<StreamRegistration>,
215}
216
217impl Stream {
218    pub(crate) fn new_unopened(parts: UnopenedStreamParts) -> Self {
219        let UnopenedStreamParts {
220            error_headers,
221            data_headers,
222            mux,
223            data_rx,
224            error_rx,
225            pending_data_tx,
226            pending_error_tx,
227            max_frame_size,
228        } = parts;
229        let release_guard = PairReleaseGuard::new(mux.clone());
230        Self {
231            state: StreamState::Unopened {
232                error_headers,
233                data_headers,
234                mux,
235                data_rx,
236                error_rx,
237                pending_data_tx: Some(pending_data_tx),
238                pending_error_tx: Some(pending_error_tx),
239                max_frame_size,
240                read_buf: None,
241                read_eof: false,
242                open_in_progress: None,
243                release_guard: Some(release_guard),
244            },
245        }
246    }
247
248    /// Returns true if the remote has already closed this stream's read
249    /// side (FIN or RST received while idle). Used by spare-stream checkout
250    /// to discard stale pre-opened streams.
251    ///
252    /// Unopened streams are never stale: no `SYN_STREAM` was sent yet, so
253    /// the apiserver hasn't created a backing pod TCP connection.
254    pub fn is_read_closed(&self) -> bool {
255        match &self.state {
256            StreamState::Unopened {
257                read_eof, data_rx, ..
258            } => *read_eof || data_rx.is_closed(),
259            StreamState::Opened {
260                read_eof, data_rx, ..
261            } => *read_eof || data_rx.is_closed(),
262            StreamState::Transitioning => false,
263        }
264    }
265
266    /// Split into data half (AsyncRead + AsyncWrite) and error half
267    /// (AsyncRead).
268    ///
269    /// Splitting an unopened stream is supported: both halves share a
270    /// single `LazyOpenSlot` driven by the data half's first write.
271    pub fn split(self) -> (DataStream, ErrorStream) {
272        match self.state {
273            StreamState::Unopened {
274                error_headers,
275                data_headers,
276                mux,
277                data_rx,
278                error_rx,
279                pending_data_tx,
280                pending_error_tx,
281                max_frame_size,
282                read_buf,
283                read_eof,
284                open_in_progress,
285                release_guard,
286            } => {
287                let shared = Arc::new(parking_lot::Mutex::new(SharedSplitState::Unopened(
288                    UnopenedShared {
289                        error_headers,
290                        data_headers,
291                        mux,
292                        pending_data_tx,
293                        pending_error_tx,
294                        open_in_progress,
295                        release_guard,
296                    },
297                )));
298                (
299                    DataStream {
300                        data_rx,
301                        max_frame_size,
302                        read_buf,
303                        read_eof,
304                        shared: Arc::clone(&shared),
305                    },
306                    ErrorStream {
307                        error_rx,
308                        error_buf: None,
309                        error_eof: false,
310                        shared,
311                    },
312                )
313            }
314            StreamState::Opened {
315                data_id,
316                data_rx,
317                error_rx,
318                mux,
319                write_tx,
320                send_window,
321                max_frame_size,
322                read_buf,
323                read_eof,
324                graceful_shutdown,
325                guard,
326            } => {
327                let opened = OpenedShared {
328                    data_id,
329                    mux,
330                    write_tx,
331                    send_window,
332                    graceful_shutdown,
333                    guard,
334                };
335                let shared = Arc::new(parking_lot::Mutex::new(SharedSplitState::Opened(opened)));
336                (
337                    DataStream {
338                        data_rx,
339                        max_frame_size,
340                        read_buf,
341                        read_eof,
342                        shared: Arc::clone(&shared),
343                    },
344                    ErrorStream {
345                        error_rx,
346                        error_buf: None,
347                        error_eof: false,
348                        shared,
349                    },
350                )
351            }
352            StreamState::Transitioning => {
353                unreachable!("split() called on transitioning stream")
354            }
355        }
356    }
357}
358
359impl Unpin for Stream {}
360
361/// Shared `poll_read` logic for channel-backed streams.
362fn poll_read_channel(
363    rx: &mut mpsc::Receiver<Bytes>, read_buf: &mut Option<Bytes>, read_eof: &mut bool,
364    cx: &mut Context<'_>, buf: &mut ReadBuf<'_>,
365) -> Poll<io::Result<()>> {
366    if *read_eof {
367        return Poll::Ready(Ok(()));
368    }
369
370    // drain buffered data first
371    if let Some(ref mut remaining) = *read_buf {
372        let to_copy = remaining.len().min(buf.remaining());
373        buf.put_slice(&remaining[..to_copy]);
374        if to_copy >= remaining.len() {
375            *read_buf = None;
376        } else {
377            *remaining = remaining.slice(to_copy..);
378        }
379        return Poll::Ready(Ok(()));
380    }
381
382    // poll channel for more data
383    match rx.poll_recv(cx) {
384        Poll::Ready(Some(data)) => {
385            let to_copy = data.len().min(buf.remaining());
386            buf.put_slice(&data[..to_copy]);
387            if to_copy < data.len() {
388                *read_buf = Some(data.slice(to_copy..));
389            }
390            Poll::Ready(Ok(()))
391        }
392        Poll::Ready(None) => {
393            *read_eof = true;
394            Poll::Ready(Ok(()))
395        }
396        Poll::Pending => Poll::Pending,
397    }
398}
399
400/// Shared `consume` logic for `AsyncBufRead`.
401fn consume_channel_buf(read_buf: &mut Option<Bytes>, amt: usize) {
402    if let Some(ref mut bytes) = *read_buf {
403        let consumed = amt.min(bytes.len());
404        bytes.advance(consumed);
405        if bytes.is_empty() {
406            *read_buf = None;
407        }
408    }
409}
410
411/// Shared `poll_fill_buf` logic for channel-backed streams.
412fn poll_fill_buf_channel<'a>(
413    rx: &'a mut mpsc::Receiver<Bytes>, read_buf: &'a mut Option<Bytes>, read_eof: &'a mut bool,
414    cx: &mut Context<'_>,
415) -> Poll<io::Result<&'a [u8]>> {
416    loop {
417        if read_buf.as_ref().is_some_and(|b| !b.is_empty()) {
418            return Poll::Ready(Ok(read_buf.as_deref().unwrap()));
419        }
420        if read_buf.is_some() {
421            *read_buf = None;
422        }
423        if *read_eof {
424            return Poll::Ready(Ok(&[]));
425        }
426        match rx.poll_recv(cx) {
427            Poll::Pending => return Poll::Pending,
428            Poll::Ready(None) => {
429                *read_eof = true;
430                return Poll::Ready(Ok(&[]));
431            }
432            Poll::Ready(Some(b)) => {
433                *read_buf = Some(b);
434            }
435        }
436    }
437}
438
439/// Send DATA+FIN on a fully opened stream and mark the guard graceful so
440/// `Drop` skips RST_STREAM for the data half.
441fn poll_shutdown_opened(
442    graceful_shutdown: &AtomicBool, mux: &MuxHandle, data_id: u32,
443) -> Poll<io::Result<()>> {
444    graceful_shutdown.store(true, Ordering::Release);
445    let _ = mux.send_data_nonblocking(data_id, Bytes::new(), true);
446    Poll::Ready(Ok(()))
447}
448
449fn broken_pipe() -> io::Error {
450    io::Error::new(io::ErrorKind::BrokenPipe, "mux closed")
451}
452
453/// Build a complete SPDY DATA frame in a single allocation and send it as a
454/// pre-encoded raw frame, enforcing per-stream send window flow control.
455///
456/// Session-level send window isn't enforced on purpose. The peer (kubelet
457/// apiserver) never sends session-level WINDOW_UPDATE with stream_id=0, so
458/// enforcing it would deadlock once the initial window drains. Per-stream
459/// windows still provide proper backpressure.
460///
461/// Clamps write size to max_frame_size - 8 (the 8-byte SPDY DATA header).
462///
463/// Ordering invariant (prevents window leak on Pending):
464///   1. `poll_reserve` cmd_tx permit (may return Pending; no side effects)
465///   2. Read send window, compute n = min(buf.len(), stream_window,
466///      max_payload)
467///   3. If n == 0: register waker, return Pending
468///   4. `stream_window.consume(n)`: debit committed
469///   5. `send_item(frame)`: infallible after successful reserve
470///   6. return Ready(Ok(n))
471fn poll_write_via_sender(
472    write_tx: &mut PollSender<MuxCommand>, stream_id: u32, send_window: &SendWindow,
473    max_frame_size: u32, cx: &mut Context<'_>, buf: &[u8],
474) -> Poll<io::Result<usize>> {
475    // early check: stream was closed (window poisoned by reader)
476    if send_window.is_closed() {
477        return Poll::Ready(Err(broken_pipe()));
478    }
479
480    // acquire cmd_tx permit. No side effects on Pending.
481    match write_tx.poll_reserve(cx) {
482        Poll::Ready(Ok(())) => {}
483        Poll::Ready(Err(_)) => return Poll::Ready(Err(broken_pipe())),
484        Poll::Pending => return Poll::Pending,
485    }
486
487    // maximum DATA payload is max_frame_size - 8 (8-byte SPDY DATA header).
488    let max_payload = (max_frame_size as usize).saturating_sub(8);
489    let max_payload = if max_payload == 0 {
490        buf.len()
491    } else {
492        max_payload
493    };
494
495    // compute write size, clamped to per-stream window AND max_frame_size.
496    let stream_avail = send_window.available().max(0) as usize;
497    let mut n = buf.len().min(stream_avail).min(max_payload);
498
499    if n == 0 {
500        // per-stream window exhausted. Register waker.
501        send_window.register_waker(cx.waker());
502
503        // re-check for poisoning
504        if send_window.is_closed() {
505            return Poll::Ready(Err(broken_pipe()));
506        }
507        // re-check window after registering waker (lost wake guard)
508        let stream_avail = send_window.available().max(0) as usize;
509        n = buf.len().min(stream_avail).min(max_payload);
510        if n == 0 {
511            return Poll::Pending;
512        }
513    }
514
515    // debit per-stream window via CAS.
516    if !send_window.consume(n) {
517        return Poll::Ready(Err(broken_pipe()));
518    }
519
520    // build DATA frame and send via the reserved permit.
521    let write_buf = &buf[..n];
522    let mut frame = BytesMut::with_capacity(8 + n);
523    frame.extend_from_slice(&(stream_id & 0x7FFF_FFFF).to_be_bytes());
524    let flags_len = (n as u32) & 0x00FF_FFFF;
525    frame.extend_from_slice(&flags_len.to_be_bytes());
526    frame.extend_from_slice(write_buf);
527
528    let cmd = MuxCommand::SendRawFrame {
529        frame: frame.freeze(),
530    };
531    match write_tx.send_item(cmd) {
532        Ok(()) => Poll::Ready(Ok(n)),
533        Err(_) => Poll::Ready(Err(broken_pipe())),
534    }
535}
536
537/// Borrowed arguments for [`poll_lazy_open`]. Bundles the per-stream lazy
538/// state into one borrow so the function signature stays under the
539/// `clippy::too_many_arguments` threshold and the call sites read as one
540/// logical unit instead of six positional arguments.
541struct LazyOpenArgs<'a> {
542    error_headers: Vec<(String, String)>,
543    data_headers: Vec<(String, String)>,
544    max_frame_size: u32,
545    mux: &'a MuxHandle,
546    pending_data_tx: &'a mut Option<mpsc::Sender<Bytes>>,
547    pending_error_tx: &'a mut Option<mpsc::Sender<Bytes>>,
548    open_in_progress: &'a mut Option<LazyOpenFuture>,
549}
550
551/// Drive the lazy-open path for the data half of a stream. On success the
552/// caller transitions `Stream` (or the split `DataStream`'s shared slot)
553/// into `Opened` state and returns the number of bytes accepted from `buf`.
554///
555/// Returns:
556/// - `Ready(Ok(n))` if open completed and `n` bytes were committed to the
557///   atomic open+write batch (the bytes are owned by the writer now).
558/// - `Pending` if the realize future hasn't completed yet.
559/// - `Ready(Err(_))` on fatal mux error.
560///
561/// The first payload is capped to one SPDY DATA frame (`max_frame_size - 8`)
562/// because `SpdyCodec::encode_data` doesn't split. Anything beyond that in
563/// the caller's first `write_all()` lands in subsequent normal `poll_write`
564/// calls on the `Opened` state.
565fn poll_lazy_open(
566    args: LazyOpenArgs<'_>, cx: &mut Context<'_>, buf: &[u8],
567) -> Poll<io::Result<(OpenedStreamParts, usize)>> {
568    let LazyOpenArgs {
569        error_headers,
570        data_headers,
571        max_frame_size,
572        mux,
573        pending_data_tx,
574        pending_error_tx,
575        open_in_progress,
576    } = args;
577    if open_in_progress.is_none() {
578        // if pending senders have been consumed by a previous failed open
579        // try, the stream is permanently broken: nothing left to
580        // register with the workers.
581        let (Some(data_tx), Some(error_tx)) = (pending_data_tx.take(), pending_error_tx.take())
582        else {
583            return Poll::Ready(Err(broken_pipe()));
584        };
585        let max_payload = (max_frame_size as usize).saturating_sub(8).max(1);
586        let n = buf.len().min(max_payload);
587        // clone the first chunk so the future is cancellation-safe: if this
588        // poll returns Pending the next poll re-uses the same payload, and
589        // if the future is dropped the caller never sees the bytes as
590        // committed.
591        let first_payload = Bytes::copy_from_slice(&buf[..n]);
592        let mux_clone = mux.clone();
593        let fut = async move {
594            mux_clone
595                .realize_stream_pair(
596                    error_headers,
597                    data_headers,
598                    first_payload,
599                    data_tx,
600                    error_tx,
601                )
602                .await
603        };
604        *open_in_progress = Some(Box::pin(fut));
605    }
606
607    let fut = open_in_progress.as_mut().expect("future just inserted");
608    match fut.as_mut().poll(cx) {
609        Poll::Pending => Poll::Pending,
610        Poll::Ready(Ok(parts)) => {
611            *open_in_progress = None;
612            let max_payload = (max_frame_size as usize).saturating_sub(8).max(1);
613            let n = buf.len().min(max_payload);
614            Poll::Ready(Ok((parts, n)))
615        }
616        Poll::Ready(Err(_)) => {
617            *open_in_progress = None;
618            Poll::Ready(Err(broken_pipe()))
619        }
620    }
621}
622
623impl AsyncRead for Stream {
624    fn poll_read(
625        self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>,
626    ) -> Poll<io::Result<()>> {
627        let this = self.get_mut();
628        match &mut this.state {
629            StreamState::Unopened {
630                data_rx,
631                read_buf,
632                read_eof,
633                ..
634            } => poll_read_channel(data_rx, read_buf, read_eof, cx, buf),
635            StreamState::Opened {
636                data_rx,
637                read_buf,
638                read_eof,
639                ..
640            } => poll_read_channel(data_rx, read_buf, read_eof, cx, buf),
641            StreamState::Transitioning => unreachable!(),
642        }
643    }
644}
645
646impl AsyncBufRead for Stream {
647    fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
648        let this = self.get_mut();
649        match &mut this.state {
650            StreamState::Unopened {
651                data_rx,
652                read_buf,
653                read_eof,
654                ..
655            } => poll_fill_buf_channel(data_rx, read_buf, read_eof, cx),
656            StreamState::Opened {
657                data_rx,
658                read_buf,
659                read_eof,
660                ..
661            } => poll_fill_buf_channel(data_rx, read_buf, read_eof, cx),
662            StreamState::Transitioning => unreachable!(),
663        }
664    }
665
666    fn consume(self: Pin<&mut Self>, amt: usize) {
667        let this = self.get_mut();
668        match &mut this.state {
669            StreamState::Unopened { read_buf, .. } => consume_channel_buf(read_buf, amt),
670            StreamState::Opened { read_buf, .. } => consume_channel_buf(read_buf, amt),
671            StreamState::Transitioning => unreachable!(),
672        }
673    }
674}
675
676impl AsyncWrite for Stream {
677    fn poll_write(
678        self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8],
679    ) -> Poll<io::Result<usize>> {
680        let this = self.get_mut();
681
682        // empty writes are a no-op; never trigger lazy open.
683        if buf.is_empty() {
684            return Poll::Ready(Ok(0));
685        }
686
687        // drive lazy open if needed. We borrow the Unopened state directly,
688        // then transition by replacing `state` with the new `Opened` value.
689        if matches!(this.state, StreamState::Unopened { .. }) {
690            // extract the fields we need to drive the future without moving
691            // the receivers (they stay borrowed by the state).
692            let (parts, n_consumed) = match &mut this.state {
693                StreamState::Unopened {
694                    error_headers,
695                    data_headers,
696                    mux,
697                    pending_data_tx,
698                    pending_error_tx,
699                    open_in_progress,
700                    max_frame_size,
701                    ..
702                } => match poll_lazy_open(
703                    LazyOpenArgs {
704                        error_headers: std::mem::take(error_headers),
705                        data_headers: std::mem::take(data_headers),
706                        max_frame_size: *max_frame_size,
707                        mux,
708                        pending_data_tx,
709                        pending_error_tx,
710                        open_in_progress,
711                    },
712                    cx,
713                    buf,
714                ) {
715                    Poll::Ready(Ok(v)) => v,
716                    Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
717                    Poll::Pending => return Poll::Pending,
718                },
719                _ => unreachable!(),
720            };
721
722            // transition Unopened -> Opened, transferring ownership of the
723            // active_pairs slot from the release guard into the StreamGuard.
724            let old = std::mem::replace(&mut this.state, StreamState::Transitioning);
725            let StreamState::Unopened {
726                mux,
727                data_rx,
728                error_rx,
729                max_frame_size,
730                read_buf,
731                read_eof,
732                mut release_guard,
733                ..
734            } = old
735            else {
736                unreachable!()
737            };
738            if let Some(g) = release_guard.as_mut() {
739                g.disarm();
740            }
741
742            let graceful_shutdown = Arc::new(AtomicBool::new(false));
743            let guard = StreamGuard {
744                data_id: parts.data_id,
745                error_id: parts.error_id,
746                mux: mux.clone(),
747                ctrl_permit_error: Some(parts.ctrl_permit_error),
748                ctrl_permit_data: Some(parts.ctrl_permit_data),
749                close_reg_permit_error: Some(parts.close_reg_permit_error),
750                close_reg_permit_data: Some(parts.close_reg_permit_data),
751                graceful_shutdown: Arc::clone(&graceful_shutdown),
752            };
753            let write_tx = PollSender::new(mux.cmd_sender());
754            this.state = StreamState::Opened {
755                data_id: parts.data_id,
756                data_rx,
757                error_rx,
758                mux,
759                write_tx,
760                send_window: parts.send_window,
761                max_frame_size,
762                read_buf,
763                read_eof,
764                graceful_shutdown,
765                guard,
766            };
767            // drop the disarmed release guard explicitly.
768            drop(release_guard);
769            return Poll::Ready(Ok(n_consumed));
770        }
771
772        match &mut this.state {
773            StreamState::Opened {
774                data_id,
775                write_tx,
776                send_window,
777                max_frame_size,
778                ..
779            } => poll_write_via_sender(write_tx, *data_id, send_window, *max_frame_size, cx, buf),
780            StreamState::Unopened { .. } => unreachable!("handled above"),
781            StreamState::Transitioning => unreachable!(),
782        }
783    }
784
785    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
786        Poll::Ready(Ok(()))
787    }
788
789    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
790        let this = self.get_mut();
791        match &mut this.state {
792            // Unopened: no SPDY stream exists yet. There is nothing on the
793            // wire to half-close. Drop will release the local slot when the
794            // Stream goes out of scope.
795            StreamState::Unopened { .. } => Poll::Ready(Ok(())),
796            StreamState::Opened {
797                graceful_shutdown,
798                mux,
799                data_id,
800                ..
801            } => poll_shutdown_opened(graceful_shutdown, mux, *data_id),
802            StreamState::Transitioning => unreachable!(),
803        }
804    }
805}
806
807/// State shared between `DataStream` and `ErrorStream` after `split()`.
808/// The data half drives lazy open; the error half participates via a single
809/// guard reference once the stream is realized.
810enum SharedSplitState {
811    Unopened(UnopenedShared),
812    Opened(OpenedShared),
813    /// Used while transferring fields out during the Unopened -> Opened
814    /// transition. Shouldn't be seen by user code because the
815    /// transition is performed under the parking_lot guard.
816    Transitioning,
817}
818
819struct UnopenedShared {
820    error_headers: Vec<(String, String)>,
821    data_headers: Vec<(String, String)>,
822    mux: MuxHandle,
823    pending_data_tx: Option<mpsc::Sender<Bytes>>,
824    pending_error_tx: Option<mpsc::Sender<Bytes>>,
825    open_in_progress: Option<LazyOpenFuture>,
826    release_guard: Option<PairReleaseGuard>,
827}
828
829struct OpenedShared {
830    data_id: u32,
831    mux: MuxHandle,
832    write_tx: PollSender<MuxCommand>,
833    send_window: Arc<SendWindow>,
834    graceful_shutdown: Arc<AtomicBool>,
835    /// Kept alive for its `Drop` impl, which sends RST_STREAM (or skips on
836    /// graceful shutdown) and notifies the workers. Never read directly.
837    #[allow(dead_code)]
838    guard: StreamGuard,
839}
840
841/// Data half of a split SPDY stream: AsyncRead (from pod) + AsyncWrite (to
842/// pod). Lazy open fires on the first non-empty write through this half.
843pub struct DataStream {
844    data_rx: mpsc::Receiver<Bytes>,
845    max_frame_size: u32,
846    read_buf: Option<Bytes>,
847    read_eof: bool,
848    shared: Arc<parking_lot::Mutex<SharedSplitState>>,
849}
850
851impl Unpin for DataStream {}
852
853impl AsyncRead for DataStream {
854    fn poll_read(
855        self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>,
856    ) -> Poll<io::Result<()>> {
857        let this = self.get_mut();
858        poll_read_channel(
859            &mut this.data_rx,
860            &mut this.read_buf,
861            &mut this.read_eof,
862            cx,
863            buf,
864        )
865    }
866}
867
868impl AsyncBufRead for DataStream {
869    fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
870        let this = self.get_mut();
871        poll_fill_buf_channel(
872            &mut this.data_rx,
873            &mut this.read_buf,
874            &mut this.read_eof,
875            cx,
876        )
877    }
878
879    fn consume(self: Pin<&mut Self>, amt: usize) {
880        consume_channel_buf(&mut self.get_mut().read_buf, amt);
881    }
882}
883
884impl AsyncWrite for DataStream {
885    fn poll_write(
886        self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8],
887    ) -> Poll<io::Result<usize>> {
888        let this = self.get_mut();
889        if buf.is_empty() {
890            return Poll::Ready(Ok(0));
891        }
892        let mut guard = this.shared.lock();
893        if let SharedSplitState::Unopened(u) = &mut *guard {
894            let res = poll_lazy_open(
895                LazyOpenArgs {
896                    error_headers: std::mem::take(&mut u.error_headers),
897                    data_headers: std::mem::take(&mut u.data_headers),
898                    max_frame_size: this.max_frame_size,
899                    mux: &u.mux,
900                    pending_data_tx: &mut u.pending_data_tx,
901                    pending_error_tx: &mut u.pending_error_tx,
902                    open_in_progress: &mut u.open_in_progress,
903                },
904                cx,
905                buf,
906            );
907            match res {
908                Poll::Pending => return Poll::Pending,
909                Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
910                Poll::Ready(Ok((parts, n_consumed))) => {
911                    let old = std::mem::replace(&mut *guard, SharedSplitState::Transitioning);
912                    let SharedSplitState::Unopened(mut u) = old else {
913                        unreachable!()
914                    };
915                    if let Some(g) = u.release_guard.as_mut() {
916                        g.disarm();
917                    }
918                    let graceful_shutdown = Arc::new(AtomicBool::new(false));
919                    let stream_guard = StreamGuard {
920                        data_id: parts.data_id,
921                        error_id: parts.error_id,
922                        mux: u.mux.clone(),
923                        ctrl_permit_error: Some(parts.ctrl_permit_error),
924                        ctrl_permit_data: Some(parts.ctrl_permit_data),
925                        close_reg_permit_error: Some(parts.close_reg_permit_error),
926                        close_reg_permit_data: Some(parts.close_reg_permit_data),
927                        graceful_shutdown: Arc::clone(&graceful_shutdown),
928                    };
929                    let write_tx = PollSender::new(u.mux.cmd_sender());
930                    *guard = SharedSplitState::Opened(OpenedShared {
931                        data_id: parts.data_id,
932                        mux: u.mux,
933                        write_tx,
934                        send_window: parts.send_window,
935                        graceful_shutdown,
936                        guard: stream_guard,
937                    });
938                    drop(u.release_guard);
939                    return Poll::Ready(Ok(n_consumed));
940                }
941            }
942        }
943        match &mut *guard {
944            SharedSplitState::Opened(o) => poll_write_via_sender(
945                &mut o.write_tx,
946                o.data_id,
947                &o.send_window,
948                this.max_frame_size,
949                cx,
950                buf,
951            ),
952            SharedSplitState::Unopened(_) => unreachable!("handled above"),
953            SharedSplitState::Transitioning => unreachable!(),
954        }
955    }
956
957    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
958        Poll::Ready(Ok(()))
959    }
960
961    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
962        let this = self.get_mut();
963        let guard = this.shared.lock();
964        match &*guard {
965            SharedSplitState::Unopened(_) => Poll::Ready(Ok(())),
966            SharedSplitState::Opened(o) => {
967                poll_shutdown_opened(&o.graceful_shutdown, &o.mux, o.data_id)
968            }
969            SharedSplitState::Transitioning => unreachable!(),
970        }
971    }
972}
973
974/// Error half of a split SPDY stream: AsyncRead only (pod error messages).
975pub struct ErrorStream {
976    error_rx: mpsc::Receiver<Bytes>,
977    error_buf: Option<Bytes>,
978    error_eof: bool,
979    #[allow(dead_code)] // kept alive so the shared open-state and guard outlive both halves
980    shared: Arc<parking_lot::Mutex<SharedSplitState>>,
981}
982
983impl Unpin for ErrorStream {}
984
985impl AsyncRead for ErrorStream {
986    fn poll_read(
987        self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>,
988    ) -> Poll<io::Result<()>> {
989        let this = self.get_mut();
990        poll_read_channel(
991            &mut this.error_rx,
992            &mut this.error_buf,
993            &mut this.error_eof,
994            cx,
995            buf,
996        )
997    }
998}