Skip to main content

h2/proto/
connection.rs

1use crate::codec::UserError;
2use crate::frame::{Reason, StreamId};
3use crate::{client, server};
4
5use crate::frame::DEFAULT_INITIAL_WINDOW_SIZE;
6use crate::proto::*;
7
8use bytes::Bytes;
9use futures_core::Stream;
10use std::io;
11use std::marker::PhantomData;
12use std::pin::Pin;
13use std::task::{Context, Poll};
14use std::time::Duration;
15use tokio::io::AsyncRead;
16
17/// An H2 connection
18#[derive(Debug)]
19pub(crate) struct Connection<T, P, B: Buf = Bytes>
20where
21    P: Peer,
22{
23    /// Read / write frame values
24    codec: Codec<T, Prioritized<B>>,
25
26    inner: ConnectionInner<P, B>,
27}
28
29// Extracted part of `Connection` which does not depend on `T`. Reduces the amount of duplicated
30// method instantiations.
31#[derive(Debug)]
32struct ConnectionInner<P, B: Buf = Bytes>
33where
34    P: Peer,
35{
36    /// Tracks the connection level state transitions.
37    state: State,
38
39    /// An error to report back once complete.
40    ///
41    /// This exists separately from State in order to support
42    /// graceful shutdown.
43    error: Option<frame::GoAway>,
44
45    /// Pending GOAWAY frames to write.
46    go_away: GoAway,
47
48    /// A refused stream to reset before receiving another frame.
49    pending_refusal: Option<StreamId>,
50
51    /// Ping/pong handler
52    ping_pong: PingPong,
53
54    /// Connection settings
55    settings: Settings,
56
57    /// Stream state handler
58    streams: Streams<B, P>,
59
60    /// A `tracing` span tracking the lifetime of the connection.
61    span: tracing::Span,
62
63    /// Client or server
64    _phantom: PhantomData<P>,
65}
66
67struct DynConnection<'a, B: Buf = Bytes> {
68    state: &'a mut State,
69
70    go_away: &'a mut GoAway,
71
72    streams: DynStreams<'a, B>,
73
74    error: &'a mut Option<frame::GoAway>,
75
76    ping_pong: &'a mut PingPong,
77}
78
79#[derive(Debug, Clone)]
80pub(crate) struct Config {
81    pub next_stream_id: StreamId,
82    pub initial_max_send_streams: usize,
83    pub max_send_buffer_size: usize,
84    pub reset_stream_duration: Duration,
85    pub reset_stream_max: usize,
86    pub remote_reset_stream_max: usize,
87    pub local_error_reset_streams_max: Option<usize>,
88    pub settings: frame::Settings,
89    pub data_frame_budget: DataFrameBudget,
90}
91
92#[derive(Clone, Copy, Debug)]
93pub(crate) enum DataFrameBudget {
94    Auto,
95    Configured(usize),
96}
97
98impl DataFrameBudget {
99    pub(crate) fn resolve(self, connection_window: Option<WindowSize>) -> usize {
100        match self {
101            Self::Configured(budget) => budget,
102            Self::Auto => {
103                let window = connection_window.unwrap_or(DEFAULT_INITIAL_WINDOW_SIZE);
104                let budget = window as usize / 2;
105
106                budget.max(DEFAULT_DATA_FRAME_BUDGET)
107            }
108        }
109    }
110
111    pub(crate) fn is_auto(self) -> bool {
112        matches!(self, Self::Auto)
113    }
114}
115
116#[derive(Debug)]
117enum State {
118    /// Currently open in a sane state
119    Open,
120
121    /// The codec must be flushed
122    Closing(Reason, Initiator),
123
124    /// In a closed state
125    Closed(Reason, Initiator),
126}
127
128impl<T, P, B> Connection<T, P, B>
129where
130    T: AsyncRead + AsyncWrite + Unpin,
131    P: Peer,
132    B: Buf,
133{
134    pub fn new(codec: Codec<T, Prioritized<B>>, config: Config) -> Connection<T, P, B> {
135        fn streams_config(config: &Config) -> streams::Config {
136            streams::Config {
137                initial_max_send_streams: config.initial_max_send_streams,
138                local_max_buffer_size: config.max_send_buffer_size,
139                local_next_stream_id: config.next_stream_id,
140                local_push_enabled: config.settings.is_push_enabled().unwrap_or(true),
141                extended_connect_protocol_enabled: config
142                    .settings
143                    .is_extended_connect_protocol_enabled()
144                    .unwrap_or(false),
145                local_reset_duration: config.reset_stream_duration,
146                local_reset_max: config.reset_stream_max,
147                remote_reset_max: config.remote_reset_stream_max,
148                remote_init_window_sz: DEFAULT_INITIAL_WINDOW_SIZE,
149                remote_max_initiated: config
150                    .settings
151                    .max_concurrent_streams()
152                    .map(|max| max as usize),
153                local_max_error_reset_streams: config.local_error_reset_streams_max,
154                data_frame_budget: config.data_frame_budget,
155            }
156        }
157        let streams = Streams::new(streams_config(&config));
158        let span = tracing::debug_span!(parent: None, "Connection", peer = %P::NAME);
159        span.follows_from(tracing::Span::current());
160        Connection {
161            codec,
162            inner: ConnectionInner {
163                state: State::Open,
164                error: None,
165                go_away: GoAway::new(),
166                pending_refusal: None,
167                ping_pong: PingPong::new(),
168                settings: Settings::new(config.settings),
169                streams,
170                span,
171                _phantom: PhantomData,
172            },
173        }
174    }
175
176    /// connection flow control
177    pub(crate) fn set_target_window_size(&mut self, size: WindowSize) {
178        let _res = self.inner.streams.set_target_connection_window_size(size);
179        // TODO: proper error handling
180        debug_assert!(_res.is_ok());
181    }
182
183    /// Send a new SETTINGS frame with an updated initial window size.
184    pub(crate) fn set_initial_window_size(&mut self, size: WindowSize) -> Result<(), UserError> {
185        let mut settings = frame::Settings::default();
186        settings.set_initial_window_size(Some(size));
187        self.inner.settings.send_settings(settings)
188    }
189
190    /// Send a new SETTINGS frame with extended CONNECT protocol enabled.
191    pub(crate) fn set_enable_connect_protocol(&mut self) -> Result<(), UserError> {
192        let mut settings = frame::Settings::default();
193        settings.set_enable_connect_protocol(Some(1));
194        self.inner.settings.send_settings(settings)
195    }
196
197    /// Returns the maximum number of concurrent streams that may be initiated
198    /// by this peer.
199    pub(crate) fn max_send_streams(&self) -> usize {
200        self.inner.streams.max_send_streams()
201    }
202
203    /// Returns the maximum number of concurrent streams that may be initiated
204    /// by the remote peer.
205    pub(crate) fn max_recv_streams(&self) -> usize {
206        self.inner.streams.max_recv_streams()
207    }
208
209    #[cfg(feature = "unstable")]
210    pub fn num_wired_streams(&self) -> usize {
211        self.inner.streams.num_wired_streams()
212    }
213
214    /// Returns `Ready` when the connection is ready to receive a frame.
215    ///
216    /// Returns `Error` as this may raise errors that are caused by delayed
217    /// processing of received frames.
218    fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Error>> {
219        let _e = self.inner.span.enter();
220        let span = tracing::trace_span!("poll_ready");
221        let _e = span.enter();
222        // The order of these calls don't really matter too much
223        ready!(self.inner.ping_pong.send_pending_pong(cx, &mut self.codec))?;
224        ready!(self.inner.ping_pong.send_pending_ping(cx, &mut self.codec))?;
225        ready!(self
226            .inner
227            .settings
228            .poll_send(cx, &mut self.codec, &mut self.inner.streams))?;
229        if let Some(id) = self.inner.pending_refusal {
230            // Keep the refusal pending if making room requires socket progress.
231            ready!(self.codec.poll_ready(cx))?;
232            self.inner.streams.count_pending_refusal()?;
233            self.codec
234                .buffer(frame::Reset::new(id, Reason::REFUSED_STREAM).into())
235                .expect("invalid RST_STREAM frame");
236            self.inner.pending_refusal = None;
237        }
238
239        Poll::Ready(Ok(()))
240    }
241
242    /// Send any pending GOAWAY frames.
243    ///
244    /// This will return `Some(reason)` if the connection should be closed
245    /// afterwards. If this is a graceful shutdown, this returns `None`.
246    fn poll_go_away(&mut self, cx: &mut Context) -> Poll<Option<io::Result<Reason>>> {
247        self.inner.go_away.send_pending_go_away(cx, &mut self.codec)
248    }
249
250    pub fn go_away_from_user(&mut self, e: Reason) {
251        self.inner.as_dyn().go_away_from_user(e)
252    }
253
254    fn take_error(&mut self, ours: Reason, initiator: Initiator) -> Result<(), Error> {
255        let (debug_data, theirs) = self
256            .inner
257            .error
258            .take()
259            .as_ref()
260            .map_or((Bytes::new(), Reason::NO_ERROR), |frame| {
261                (frame.debug_data().clone(), frame.reason())
262            });
263
264        match (ours, theirs) {
265            (Reason::NO_ERROR, Reason::NO_ERROR) => Ok(()),
266            (ours, Reason::NO_ERROR) => Err(Error::GoAway(Bytes::new(), ours, initiator)),
267            // If both sides reported an error, give their
268            // error back to th user. We assume our error
269            // was a consequence of their error, and less
270            // important.
271            (_, theirs) => Err(Error::remote_go_away(debug_data, theirs)),
272        }
273    }
274
275    /// Closes the connection by transitioning to a GOAWAY state
276    /// iff there are no streams or references
277    pub fn maybe_close_connection_if_no_streams(&mut self) {
278        // If we poll() and realize that there are no streams or references
279        // then we can close the connection by transitioning to GOAWAY
280        if !self.inner.streams.has_streams_or_other_references() {
281            self.inner.as_dyn().go_away_now(Reason::NO_ERROR);
282        }
283    }
284
285    /// Checks if there are any streams
286    pub fn has_streams(&self) -> bool {
287        self.inner.streams.has_streams()
288    }
289
290    /// Checks if there are any streams or references left
291    pub fn has_streams_or_other_references(&self) -> bool {
292        // If we poll() and realize that there are no streams or references
293        // then we can close the connection by transitioning to GOAWAY
294        self.inner.streams.has_streams_or_other_references()
295    }
296
297    pub(crate) fn take_user_pings(&mut self) -> Option<UserPings> {
298        self.inner.ping_pong.take_user_pings()
299    }
300
301    /// Advances the internal state of the connection.
302    pub fn poll(&mut self, cx: &mut Context) -> Poll<Result<(), Error>> {
303        // XXX(eliza): cloning the span is unfortunately necessary here in
304        // order to placate the borrow checker — `self` is mutably borrowed by
305        // `poll2`, which means that we can't borrow `self.span` to enter it.
306        // The clone is just an atomic ref bump.
307        let span = self.inner.span.clone();
308        let _e = span.enter();
309        let span = tracing::trace_span!("poll");
310        let _e = span.enter();
311
312        loop {
313            tracing::trace!(connection.state = ?self.inner.state);
314            // TODO: probably clean up this glob of code
315            match self.inner.state {
316                // When open, continue to poll a frame
317                State::Open => {
318                    let result = match self.poll2(cx) {
319                        Poll::Ready(result) => result,
320                        // The connection is not ready to make progress
321                        Poll::Pending => {
322                            // Ensure all window updates have been sent.
323                            //
324                            // This will also handle flushing `self.codec`
325                            ready!(self.inner.streams.poll_complete(cx, &mut self.codec))?;
326
327                            if (self.inner.error.is_some()
328                                || self.inner.go_away.should_close_on_idle())
329                                && !self.inner.streams.has_streams()
330                            {
331                                self.inner.as_dyn().go_away_now(Reason::NO_ERROR);
332                                continue;
333                            }
334
335                            return Poll::Pending;
336                        }
337                    };
338
339                    self.inner.as_dyn().handle_poll2_result(result)?
340                }
341                State::Closing(reason, initiator) => {
342                    tracing::trace!("connection closing after flush");
343                    // Flush/shutdown the codec
344                    ready!(self.codec.shutdown(cx))?;
345
346                    // Transition the state to error
347                    self.inner.state = State::Closed(reason, initiator);
348                }
349                State::Closed(reason, initiator) => {
350                    return Poll::Ready(self.take_error(reason, initiator));
351                }
352            }
353        }
354    }
355
356    fn poll2(&mut self, cx: &mut Context) -> Poll<Result<(), Error>> {
357        // This happens outside of the loop to prevent needing to do a clock
358        // check and then comparison of the queue possibly multiple times a
359        // second (and thus, the clock wouldn't have changed enough to matter).
360        self.clear_expired_reset_streams();
361
362        loop {
363            // First, ensure that the `Connection` is able to receive a frame
364            //
365            // The order here matters:
366            // - poll_go_away may buffer a graceful shutdown GOAWAY frame
367            // - If it has, we've also added a PING to be sent in poll_ready
368            if let Some(reason) = ready!(self.poll_go_away(cx)?) {
369                if self.inner.go_away.should_close_now() {
370                    if self.inner.go_away.is_user_initiated() {
371                        // A user initiated abrupt shutdown shouldn't return
372                        // the same error back to the user.
373                        return Poll::Ready(Ok(()));
374                    } else {
375                        return Poll::Ready(Err(Error::library_go_away(reason)));
376                    }
377                }
378                // Only NO_ERROR should be waiting for idle
379                debug_assert_eq!(
380                    reason,
381                    Reason::NO_ERROR,
382                    "graceful GOAWAY should be NO_ERROR"
383                );
384            }
385            ready!(self.poll_ready(cx))?;
386
387            match self
388                .inner
389                .as_dyn()
390                .recv_frame(ready!(Pin::new(&mut self.codec).poll_next(cx)?))?
391            {
392                ReceivedFrame::Settings(frame) => {
393                    self.inner.settings.recv_settings(
394                        frame,
395                        &mut self.codec,
396                        &mut self.inner.streams,
397                    )?;
398                }
399                ReceivedFrame::Refused(id) => {
400                    debug_assert!(self.inner.pending_refusal.is_none());
401                    self.inner.pending_refusal = Some(id);
402                }
403                ReceivedFrame::Continue => (),
404                ReceivedFrame::Done => {
405                    return Poll::Ready(Ok(()));
406                }
407            }
408        }
409    }
410
411    fn clear_expired_reset_streams(&mut self) {
412        self.inner.streams.clear_expired_reset_streams();
413    }
414}
415
416impl<P, B> ConnectionInner<P, B>
417where
418    P: Peer,
419    B: Buf,
420{
421    fn as_dyn(&mut self) -> DynConnection<'_, B> {
422        let ConnectionInner {
423            state,
424            go_away,
425            streams,
426            error,
427            ping_pong,
428            ..
429        } = self;
430        let streams = streams.as_dyn();
431        DynConnection {
432            state,
433            go_away,
434            streams,
435            error,
436            ping_pong,
437        }
438    }
439}
440
441impl<B> DynConnection<'_, B>
442where
443    B: Buf,
444{
445    fn go_away(&mut self, id: StreamId, e: Reason) {
446        let frame = frame::GoAway::new(id, e);
447        self.streams.send_go_away(id);
448        self.go_away.go_away(frame);
449    }
450
451    fn go_away_now(&mut self, e: Reason) {
452        let last_processed_id = self.streams.last_processed_id();
453        let frame = frame::GoAway::new(last_processed_id, e);
454        self.go_away.go_away_now(frame);
455    }
456
457    fn go_away_now_data(&mut self, e: Reason, data: Bytes) {
458        let last_processed_id = self.streams.last_processed_id();
459        let frame = frame::GoAway::with_debug_data(last_processed_id, e, data);
460        self.go_away.go_away_now(frame);
461    }
462
463    fn go_away_from_user(&mut self, e: Reason) {
464        let last_processed_id = self.streams.last_processed_id();
465        let frame = frame::GoAway::new(last_processed_id, e);
466        self.go_away.go_away_from_user(frame);
467
468        // Notify all streams of reason we're abruptly closing.
469        self.streams.handle_error(Error::user_go_away(e));
470    }
471
472    fn handle_poll2_result(&mut self, result: Result<(), Error>) -> Result<(), Error> {
473        match result {
474            // The connection has shutdown normally
475            Ok(()) => {
476                *self.state = State::Closing(Reason::NO_ERROR, Initiator::Library);
477                Ok(())
478            }
479            // Attempting to read a frame resulted in a connection level
480            // error. This is handled by setting a GOAWAY frame followed by
481            // terminating the connection.
482            Err(Error::GoAway(debug_data, reason, initiator)) => {
483                self.handle_go_away(reason, debug_data, initiator);
484                Ok(())
485            }
486            // Attempting to read a frame resulted in a stream level error.
487            // Locally detected stream errors are reported to the peer with
488            // RST_STREAM. Remotely initiated resets have already been applied
489            // by the streams state machine and must not be echoed back.
490            Err(Error::Reset(id, reason, initiator)) => {
491                if initiator == Initiator::Remote {
492                    tracing::trace!(?id, ?reason, ?initiator, "stream reset");
493                    return Ok(());
494                }
495
496                debug_assert_eq!(initiator, Initiator::Library);
497                tracing::trace!(?id, ?reason, ?initiator, "stream error");
498                match self.streams.send_reset(id, reason) {
499                    Ok(()) => (),
500                    Err(crate::proto::error::GoAway { debug_data, reason }) => {
501                        self.handle_go_away(reason, debug_data, Initiator::Library);
502                    }
503                }
504                Ok(())
505            }
506            // Attempting to read a frame resulted in an I/O error. All
507            // active streams must be reset.
508            //
509            // TODO: Are I/O errors recoverable?
510            Err(Error::Io(kind, inner)) => {
511                tracing::debug!(error = ?kind, "Connection::poll; IO error");
512                let e = Error::Io(kind, inner);
513
514                // Reset all active streams
515                self.streams.handle_error(e.clone());
516
517                // Some client implementations drop the connections without notifying its peer
518                // Attempting to read after the client dropped the connection results in UnexpectedEof
519                // If as a server, we don't have anything more to send, just close the connection
520                // without error
521                //
522                // See https://github.com/hyperium/hyper/issues/3427
523                if self.streams.is_buffer_empty()
524                    && matches!(kind, io::ErrorKind::UnexpectedEof)
525                    && (self.streams.is_server()
526                        || self.error.as_ref().map(|f| f.reason() == Reason::NO_ERROR)
527                            == Some(true))
528                {
529                    *self.state = State::Closed(Reason::NO_ERROR, Initiator::Library);
530                    return Ok(());
531                }
532
533                // Return the error
534                Err(e)
535            }
536        }
537    }
538
539    fn handle_go_away(&mut self, reason: Reason, debug_data: Bytes, initiator: Initiator) {
540        let e = Error::GoAway(debug_data.clone(), reason, initiator);
541        tracing::debug!(error = ?e, "Connection::poll; connection error");
542
543        // We may have already sent a GOAWAY for this error,
544        // if so, don't send another, just flush and close up.
545        if self
546            .go_away
547            .going_away()
548            .map_or(false, |frame| frame.reason() == reason)
549        {
550            tracing::trace!("    -> already going away");
551            *self.state = State::Closing(reason, initiator);
552            return;
553        }
554
555        // Reset all active streams
556        self.streams.handle_error(e);
557        self.go_away_now_data(reason, debug_data);
558    }
559
560    fn recv_frame(&mut self, frame: Option<Frame>) -> Result<ReceivedFrame, Error> {
561        use crate::frame::Frame::*;
562        match frame {
563            Some(Headers(frame)) => {
564                tracing::trace!(?frame, "recv HEADERS");
565                match self.streams.recv_headers(frame)? {
566                    streams::RecvOutcome::Processed => (),
567                    streams::RecvOutcome::Refused(id) => return Ok(ReceivedFrame::Refused(id)),
568                }
569            }
570            Some(Data(frame)) => {
571                tracing::trace!(?frame, "recv DATA");
572                self.streams.recv_data(frame)?;
573            }
574            Some(Reset(frame)) => {
575                tracing::trace!(?frame, "recv RST_STREAM");
576                self.streams.recv_reset(frame)?;
577            }
578            Some(PushPromise(frame)) => {
579                tracing::trace!(?frame, "recv PUSH_PROMISE");
580                match self.streams.recv_push_promise(frame)? {
581                    streams::RecvOutcome::Processed => (),
582                    streams::RecvOutcome::Refused(id) => return Ok(ReceivedFrame::Refused(id)),
583                }
584            }
585            Some(Settings(frame)) => {
586                tracing::trace!(?frame, "recv SETTINGS");
587                return Ok(ReceivedFrame::Settings(frame));
588            }
589            Some(GoAway(frame)) => {
590                tracing::trace!(?frame, "recv GOAWAY");
591                // This should prevent starting new streams,
592                // but should allow continuing to process current streams
593                // until they are all EOS. Once they are, State should
594                // transition to GoAway.
595                self.streams.recv_go_away(&frame)?;
596                *self.error = Some(frame);
597            }
598            Some(Ping(frame)) => {
599                tracing::trace!(?frame, "recv PING");
600                let status = self.ping_pong.recv_ping(frame);
601                if status.is_shutdown() {
602                    assert!(
603                        self.go_away.is_going_away(),
604                        "received unexpected shutdown ping"
605                    );
606
607                    let last_processed_id = self.streams.last_processed_id();
608                    self.go_away(last_processed_id, Reason::NO_ERROR);
609                }
610            }
611            Some(WindowUpdate(frame)) => {
612                tracing::trace!(?frame, "recv WINDOW_UPDATE");
613                self.streams.recv_window_update(frame)?;
614            }
615            Some(Priority(frame)) => {
616                tracing::trace!(?frame, "recv PRIORITY");
617                // TODO: handle
618            }
619            None => {
620                tracing::trace!("codec closed");
621                self.streams.recv_eof(false).expect("mutex poisoned");
622                return Ok(ReceivedFrame::Done);
623            }
624        }
625        Ok(ReceivedFrame::Continue)
626    }
627}
628
629enum ReceivedFrame {
630    Refused(StreamId),
631    Settings(frame::Settings),
632    Continue,
633    Done,
634}
635
636impl<T, B> Connection<T, client::Peer, B>
637where
638    T: AsyncRead + AsyncWrite,
639    B: Buf,
640{
641    pub(crate) fn streams(&self) -> &Streams<B, client::Peer> {
642        &self.inner.streams
643    }
644}
645
646impl<T, B> Connection<T, server::Peer, B>
647where
648    T: AsyncRead + AsyncWrite + Unpin,
649    B: Buf,
650{
651    pub fn next_incoming(&mut self) -> Option<StreamRef<B>> {
652        self.inner.streams.next_incoming()
653    }
654
655    // Graceful shutdown only makes sense for server peers.
656    pub fn go_away_gracefully(&mut self) {
657        if self.inner.go_away.is_going_away() {
658            // No reason to start a new one.
659            return;
660        }
661
662        // According to http://httpwg.org/specs/rfc7540.html#GOAWAY:
663        //
664        // > A server that is attempting to gracefully shut down a connection
665        // > SHOULD send an initial GOAWAY frame with the last stream
666        // > identifier set to 2^31-1 and a NO_ERROR code. This signals to the
667        // > client that a shutdown is imminent and that initiating further
668        // > requests is prohibited. After allowing time for any in-flight
669        // > stream creation (at least one round-trip time), the server can
670        // > send another GOAWAY frame with an updated last stream identifier.
671        // > This ensures that a connection can be cleanly shut down without
672        // > losing requests.
673        self.inner.as_dyn().go_away(StreamId::MAX, Reason::NO_ERROR);
674
675        // We take the advice of waiting 1 RTT literally, and wait
676        // for a pong before proceeding.
677        self.inner.ping_pong.ping_shutdown();
678    }
679}
680
681impl<T, P, B> Drop for Connection<T, P, B>
682where
683    P: Peer,
684    B: Buf,
685{
686    fn drop(&mut self) {
687        // Ignore errors as this indicates that the mutex is poisoned.
688        let _ = self.inner.streams.recv_eof(true);
689    }
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn auto_data_frame_budget_scales_with_connection_window() {
698        assert_eq!(
699            DataFrameBudget::Auto.resolve(None),
700            DEFAULT_INITIAL_WINDOW_SIZE as usize / 2
701        );
702        assert_eq!(
703            DataFrameBudget::Auto.resolve(Some(DEFAULT_INITIAL_WINDOW_SIZE)),
704            DEFAULT_INITIAL_WINDOW_SIZE as usize / 2
705        );
706        assert_eq!(DataFrameBudget::Auto.resolve(Some(1024 * 1024)), 512 * 1024);
707    }
708
709    #[test]
710    fn auto_data_frame_budget_has_minimum() {
711        assert_eq!(
712            DataFrameBudget::Auto.resolve(Some(1)),
713            DEFAULT_DATA_FRAME_BUDGET
714        );
715        assert_eq!(
716            DataFrameBudget::Auto.resolve(Some(MAX_WINDOW_SIZE)),
717            MAX_WINDOW_SIZE as usize / 2
718        );
719    }
720
721    #[test]
722    fn configured_data_frame_budget_is_unchanged() {
723        assert_eq!(
724            DataFrameBudget::Configured(123).resolve(Some(MAX_WINDOW_SIZE)),
725            123
726        );
727    }
728}