weida_core/limits.rs
1//! Resource limits.
2//!
3//! Master doc §50: no internal queue may be unbounded, and every allocation
4//! that a remote peer can influence must have a defensive ceiling. Each field
5//! below names the remote-controlled quantity it bounds.
6//!
7//! **`Limits` is a per-connection profile.** Every field applies to one
8//! connection — which is what a runtime holding one profile per connection
9//! tier will need
10//! (`docs/decisions/0002-control-and-bulk-separation.md` §6.4). Numbers that
11//! belong to a runtime rather than to a connection — how many connections a
12//! binding accepts, how many one peer may hold, how deep an endpoint's queue
13//! is, how many resolved addresses a dial tries — live on `RuntimeConfig`
14//! instead, so that no profile can carry a value nothing reads.
15
16use std::time::Duration;
17
18/// Resource limits applied to one connection.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct Limits {
21 /// Largest frame header this side will accept, in bytes. Checked against
22 /// the preamble length *before* any allocation.
23 pub max_header_bytes: u64,
24 /// QUIC concurrent inbound unidirectional streams. Bounds the number of
25 /// live per-stream parse tasks for one-way transfers.
26 pub max_concurrent_uni_streams: u32,
27 /// QUIC concurrent inbound bidirectional streams; bounds live request
28 /// exchanges per connection and, with `max_header_bytes`, worst-case
29 /// header memory.
30 pub max_concurrent_bidi_streams: u32,
31 /// QUIC per-stream receive window in bytes. Bounds buffered payload for one
32 /// transfer and provides backpressure to the sender.
33 pub stream_receive_window: u64,
34 /// QUIC per-connection receive window in bytes.
35 pub connection_receive_window: u64,
36 /// QUIC keep-alive interval. Sent by the dialling side only, so a binding's
37 /// value is not used.
38 pub keep_alive: Duration,
39 /// QUIC idle timeout, applied in both directions.
40 pub idle_timeout: Duration,
41 /// How long to wait for the peer HELLO before closing the connection with
42 /// `NEGOTIATION_FAILED`, in milliseconds.
43 pub hello_timeout_ms: u64,
44 /// Subscription filters one peer connection may hold at once, summed over
45 /// every path. Exceeding it closes the connection with `LIMIT_EXCEEDED`:
46 /// SUBSCRIBE has no stream to answer with an ERROR frame.
47 pub max_subscriptions: usize,
48 /// Payload bytes a publisher may hold queued for one subscriber. A message
49 /// that does not fit is dropped for that subscriber and counted; the
50 /// publisher never blocks on a slow consumer.
51 pub subscriber_buffer_bytes: usize,
52 /// Producer scopes — paths and topics — a receiver tracks per connection
53 /// for gap detection or reassembly, when `PerProducer` ordering is
54 /// negotiated. The peer chooses the scope names, so the table needs a
55 /// ceiling; at the cap a new scope is simply not tracked, no gap is
56 /// reported for it and nothing is held back for it.
57 pub max_sequence_scopes: usize,
58 /// Transfers a receiver may hold back at once, summed over scopes, when
59 /// `PerProducer(reassemble)` ordering is negotiated. A held transfer is
60 /// an unread stream, so the bytes it pins are quinn's — up to
61 /// `stream_receive_window` for it and `connection_receive_window` for the
62 /// hold as a whole. At the cap the oldest held transfer is released out
63 /// of order with its gap reported, never held in a growing buffer.
64 pub max_reorder_hold: usize,
65 /// Live transfers on one **local** connection, where the OS connection
66 /// *is* the stream and there is no multiplexing
67 /// ([decisions/0010](../../../docs/decisions/0010-local-transport.md)
68 /// §4.2). Windows caps named-pipe instances at 1-255, which is the
69 /// tightest platform limit and therefore the one the default respects;
70 /// an `open` at the cap waits for a live transfer to end, exactly as a
71 /// QUIC `open` waits on the peer's stream budget, rather than refusing.
72 /// A peer configured with zero can therefore open nothing and waits
73 /// until its caller's deadline, exactly as one granted no QUIC streams.
74 pub max_local_streams: usize,
75 /// Connections a subscriber parks toward a peer it dialled, so that peer
76 /// can open a stream back
77 /// ([decisions/0012](../../../docs/decisions/0012-local-connection-grouping.md)
78 /// §4.4). An accepted socket cannot be dialled, so fan-out over a local
79 /// socket transport rides connections the subscriber opened and left
80 /// waiting; each is a file descriptor held for a copy that may never
81 /// come, and each counts against `max_local_streams` on both sides. A
82 /// publisher that finds none parked drops that copy and counts it, the
83 /// same answer fan-out already gives an exhausted subscriber budget. Zero
84 /// disables the pool, which makes a subscription over such a transport an
85 /// error at subscribe time rather than a silence.
86 pub max_parked_reverse: usize,
87
88 /// Identities a receiver remembers per connection for `Bounded`
89 /// deduplication. The window bounds how long an identity is kept, not
90 /// how many arrive within it, so the count needs its own ceiling; at the
91 /// cap the oldest entry is evicted, which costs suppression rather than
92 /// memory.
93 pub max_dedup_entries: usize,
94}
95
96impl Limits {
97 /// Worst-case header memory a single hostile connection can pin, in bytes.
98 ///
99 /// Both stream budgets count: a peer may open its full uni *and* bidi
100 /// allowance, and every accepted stream starts with one header.
101 pub const fn worst_case_header_memory(&self) -> u64 {
102 self.max_header_bytes
103 * (self.max_concurrent_uni_streams as u64 + self.max_concurrent_bidi_streams as u64)
104 }
105}
106
107impl Default for Limits {
108 fn default() -> Self {
109 Limits {
110 max_header_bytes: 16 * 1024,
111 max_concurrent_uni_streams: 2048,
112 max_concurrent_bidi_streams: 1024,
113 stream_receive_window: 1024 * 1024,
114 connection_receive_window: 16 * 1024 * 1024,
115 keep_alive: Duration::from_secs(10),
116 idle_timeout: Duration::from_secs(30),
117 hello_timeout_ms: 10_000,
118 max_subscriptions: 256,
119 subscriber_buffer_bytes: 8 * 1024 * 1024,
120 max_sequence_scopes: 1024,
121 max_reorder_hold: 256,
122 max_local_streams: 255,
123 max_parked_reverse: 8,
124
125 max_dedup_entries: 4096,
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn defaults_match_the_protocol_document() {
136 let l = Limits::default();
137 assert_eq!(l.max_header_bytes, 16384);
138 assert_eq!(l.max_concurrent_uni_streams, 2048);
139 assert_eq!(l.max_concurrent_bidi_streams, 1024);
140 assert_eq!(l.stream_receive_window, 1 << 20);
141 assert_eq!(l.connection_receive_window, 16 << 20);
142 assert_eq!(l.keep_alive, Duration::from_secs(10));
143 assert_eq!(l.idle_timeout, Duration::from_secs(30));
144 assert_eq!(l.hello_timeout_ms, 10_000);
145 assert_eq!(l.max_subscriptions, 256);
146 assert_eq!(l.subscriber_buffer_bytes, 8 << 20);
147 assert_eq!(l.max_sequence_scopes, 1024);
148 assert_eq!(l.max_reorder_hold, 256);
149 assert_eq!(l.max_local_streams, 255);
150 assert_eq!(l.max_parked_reverse, 8);
151 assert_eq!(l.max_dedup_entries, 4096);
152 }
153
154 #[test]
155 fn worst_case_header_memory_is_48_mib() {
156 assert_eq!(Limits::default().worst_case_header_memory(), 48 << 20);
157 }
158}