Skip to main content

TransportProfile

Struct TransportProfile 

Source
#[non_exhaustive]
pub struct TransportProfile {
Show 18 fields pub congestion: Option<Congestion>, pub initial_rtt: Option<Duration>, pub receive_window: Option<u64>, pub stream_receive_window: Option<u64>, pub send_window: Option<u64>, pub max_concurrent_uni_streams: Option<u64>, pub max_concurrent_bidi_streams: Option<u64>, pub max_idle_timeout: Option<Duration>, pub keep_alive_interval: Option<Duration>, pub packet_threshold: Option<u32>, pub time_threshold: Option<f32>, pub persistent_congestion_threshold: Option<u32>, pub ack_frequency: Option<AckFrequency>, pub initial_mtu: Option<u16>, pub min_mtu: Option<u16>, pub mtu_discovery: Option<MtuDiscovery>, pub send_fairness: Option<bool>, pub datagram_receive_buffer: Option<DatagramBuffer>,
}
Expand description

A per-leg description of QUIC transport parameters.

Every field is optional and every None means leave this alone. There is no field whose None is a value: a profile that sets three knobs is a profile about three knobs, and the other thirteen belong to whoever built the quinn::TransportConfig it is applied to.

#[non_exhaustive] with a Default, as the shaping configs are: the attribute lets a later release add a seventeenth knob without a break, and outside this crate it makes both struct-expression and ..Default::default() syntax illegal, so the Default is what leaves a construction path open at all. The documented way to build one is therefore TransportProfile::default() followed by field assignment, which is what the example does — it is the only path the attribute leaves, so it is the one worth proving.

use std::time::Duration;

use moqtap_proxy::transport::{Congestion, MtuDiscovery, TransportProfile};

let mut profile = TransportProfile::default();
profile.congestion = Some(Congestion::Bbr);
profile.initial_rtt = Some(Duration::from_millis(40));
profile.receive_window = Some(8 * 1024 * 1024);
profile.initial_mtu = Some(1350);
profile.mtu_discovery = Some(MtuDiscovery::Off);

profile.validate()?;
let config = profile.into_config()?;

Fields (Non-exhaustive)§

This struct is marked as non-exhaustive
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§congestion: Option<Congestion>

Which congestion controller to install.

Each variant installs that controller’s own default configuration. The controllers behave very differently on a lossy path — BBR keeps sending through loss that collapses a Cubic sender — so a run comparing two of them wants this named explicitly rather than inherited from whatever quinn’s default happens to be that release.

§initial_rtt: Option<Duration>

The RTT to assume before a measurement exists.

It decides the first retransmission timeout, so on a long path a default that is far too low spends the opening exchange retransmitting packets that were merely in flight.

§receive_window: Option<u64>

Connection-wide flow-control window, in bytes.

The cap on unacknowledged data across all streams. Too small for the bandwidth-delay product and the sender stalls on flow control at a throughput that has nothing to do with the congestion controller under test.

§stream_receive_window: Option<u64>

Per-stream flow-control window, in bytes.

Held below TransportProfile::receive_window so that one slow reader cannot monopolise the connection’s receive buffers.

§send_window: Option<u64>

Cap on unacknowledged outgoing data, in bytes.

The send-side counterpart of TransportProfile::receive_window, and the one window quinn takes as a plain u64 rather than a QUIC varint, so no range check applies to it.

§max_concurrent_uni_streams: Option<u64>

How many unidirectional streams the peer may have open at once.

MoQT carries media on unidirectional streams, so this is the ceiling on concurrent subgroups; a value below what a subscription needs shows up as senders blocked waiting for a stream credit rather than as anything resembling congestion.

§max_concurrent_bidi_streams: Option<u64>

How many bidirectional streams the peer may have open at once.

What this starves depends on the draft, and a profile is installed on a leg before any version has been negotiated, so it cannot depend on which. Three answers over the range, each taken from that draft’s own Section 3.3:

  • Drafts 07 through 15 specify a single use of bidirectional streams, the control stream (draft-15 Section 3.3). A cap of zero there does not impair a session, it prevents one.
  • Draft-16 specifies two, the control stream and SUBSCRIBE_NAMESPACE (draft-16 Section 3.3). It is the one draft on which a cap can starve something and leave the session running.
  • Drafts 17 through 19 moved the control plane onto a pair of unidirectional streams and give bidirectional streams to requests alone — six message types on draft-17, seven on drafts 18 and 19 (draft-17 Section 3.3). A cap there is the request-side counterpart of TransportProfile::max_concurrent_uni_streams on the media side, and it is the case this knob is carried for.

So a value written without knowing which draft the run will negotiate is a foot-gun rather than a setting, and that is a reason to say so here rather than a reason to leave the field out.

§max_idle_timeout: Option<Duration>

How long a connection may sit idle before it is closed.

The effective timeout is the smaller of this and the peer’s own, so setting it here only ever shortens the wait.

§keep_alive_interval: Option<Duration>

How often to send a packet purely to keep the connection alive.

Must be strictly below TransportProfile::max_idle_timeout when both are set — see TransportProfileError::KeepAliveNotBelowIdle.

§packet_threshold: Option<u32>

How many packets may be acknowledged after a packet before it is declared lost.

The reordering tolerance of loss detection. Lowering it makes a reordering path look like a lossy one, which is occasionally the point and is otherwise a way to misread a run.

§time_threshold: Option<f32>

Loss-detection time threshold, as a multiple of the round-trip estimate.

Must be finite and greater than 1.0: it is a multiplier on the RTT, so a value at or below one declares packets lost before an acknowledgement could have arrived.

§persistent_congestion_threshold: Option<u32>

How many consecutive probe timeouts amount to persistent congestion.

quinn multiplies the probe timeout by this to get the window it looks for entirely-lost packets in, and a path judged persistently congested has its congestion window collapsed to the minimum. Lowering it makes a sender give up on a bad path sooner.

Nothing in this crate demonstrates its effect. Persistent congestion is entered on a duration of losses, and no test here asserts a duration, so a gate for this field could assert only that a setter accepted the value. It ships under that stated limit, which is a different thing from a knob accepted and ignored — the same footing as TransportProfile::ack_frequency.

§ack_frequency: Option<AckFrequency>

Acknowledgement frequency to request of the peer.

None leaves quinn’s default, which is not to negotiate the extension at all. Some asks for it, with the knobs in AckFrequency.

§initial_mtu: Option<u16>

The packet size to start with, in bytes.

Must be at least 1200 — see TransportProfileError::MtuBelowFloor.

§min_mtu: Option<u16>

The packet size never to go below, in bytes, after black-hole detection has lowered the discovered MTU.

Must be at least 1200, and no larger than TransportProfile::initial_mtu.

§mtu_discovery: Option<MtuDiscovery>

Whether to search for a larger path MTU, and how far.

quinn’s default is to search, so None here means keep searching and MtuDiscovery::Off is the only way to stop it. The two are deliberately distinguishable.

§send_fairness: Option<bool>

Whether to share send capacity fairly between streams rather than draining them in priority order.

It changes which subgroup arrives first when several are ready at once, which is visible in delivery order and not in any counter.

§datagram_receive_buffer: Option<DatagramBuffer>

How much room to give incoming QUIC datagrams, in bytes.

DatagramBuffer::Disabled refuses datagrams outright, which is a different thing from leaving the field unset — see DatagramBuffer for why that distinction has a type rather than a nested Option.

Implementations§

Source§

impl TransportProfile

Source

pub fn validate(&self) -> Result<(), TransportProfileError>

Everything wrong with this profile, before any connection exists.

Returns the first failure, checked in field-declaration order, as the other validators in this workspace do: a profile with two mistakes reports the earlier field, and fixing it reveals the second. The order is fixed so the answer is repeatable rather than dependent on which check happened to be written first.

Two deliberate departures from strict declaration order, both because the more useful thing to be told comes first:

  • TransportProfileError::KeepAliveNotBelowIdle is checked at keep_alive_interval, the later of the two fields it compares, so that the earlier field has already had its own range check.
  • Both MTU floors are checked before TransportProfileError::MtuInverted. A value below the floor is a single-field fault with a single-field fix, and once both values are legal the inversion may not exist any more.
§What is not checked, and why

The idle timeout has no error variant of its own. IdleTimeout converts from a Duration through as_millis against the same varint ceiling as the windows, which puts the limit around 146 million years — no configuration file reaches it, and an error a reader can never meet is worse than no error at all. The conversion is nevertheless fallible in Rust, because Duration::from_secs(u64::MAX) exists, so it is folded into TransportProfileError::VarIntRange under the field name max_idle_timeout. That keeps the path free of a panic without adding a rule to the list an author has to read.

Source

pub fn apply_to( &self, tc: &mut TransportConfig, ) -> Result<(), TransportProfileError>

Write this profile’s fields into tc, leaving every field it does not set untouched.

§Why this takes &mut and returns nothing

quinn::TransportConfig has exactly three impls — the inherent setters, Default and Debug. There is no Clone, and every field is private with no getter. So there is no way to write fn apply(&self, base: &TransportConfig) -> TransportConfig: the function cannot copy base and cannot read a single value out of it, so the only thing it could return is a fresh default with the caller’s configuration silently thrown away. Mutating in place is the one shape in which leaves the rest untouched is true rather than merely claimed.

The trap this leaves for whoever maintains it: base.clone() compiles. &TransportConfig is Clone even though TransportConfig is not, so the call clones the reference and the mistake only surfaces at the return, as “TransportConfig does not implement Clone, so &TransportConfig was cloned instead”. Anyone who reaches for the by-value signature will meet that message and should read it as the reason this signature is what it is.

§All or nothing

The first statement is self.validate()?, and that is the whole of how this method is kept from installing something validate would have rejected — there is no second list of rules to drift out of step with the first, and no field-by-field reading to do to check it. It also means tc is either fully written or not written at all: a profile that fails returns before the first setter runs, so a caller who ignores the error is not left with a half-applied config.

The conversions below re-run the fallible steps with ? rather than unwrapping them. They cannot fail after validate has passed, but expressing that as a panic would make a future divergence between the two lists into a crash instead of an error.

Source

pub fn into_config(&self) -> Result<TransportConfig, TransportProfileError>

A fresh config carrying only this profile.

Equivalent to TransportProfile::apply_to over a quinn::TransportConfig::default(), and defined that way rather than duplicated, so the two can only ever accept and refuse the same profiles. Use it for a leg with no configuration of its own; use apply_to for a leg that already has one.

Trait Implementations§

Source§

impl Clone for TransportProfile

Source§

fn clone(&self) -> TransportProfile

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for TransportProfile

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for TransportProfile

Source§

fn default() -> TransportProfile

Returns the “default value” for a type. Read more
Source§

impl PartialEq for TransportProfile

Source§

fn eq(&self, other: &TransportProfile) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for TransportProfile

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more