moqtap_client/draft13/session/request_id.rs
1use moqtap_codec::varint::VarInt;
2
3/// Which end of the session this endpoint is, which fixes the parity of the
4/// request IDs it may allocate.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Role {
7 /// Client: even request IDs, 0, 2, 4, ...
8 Client,
9 /// Server: odd request IDs, 1, 3, 5, ...
10 Server,
11}
12
13/// Errors from request ID allocation or validation.
14#[derive(Debug, thiserror::Error, PartialEq, Eq)]
15pub enum RequestIdError {
16 /// The request ID is not below the current MAX_REQUEST_ID.
17 #[error("request ID {0} exceeds max {1}")]
18 ExceedsMax(u64, u64),
19 /// The request ID has the wrong parity for the endpoint that sent it.
20 #[error("request ID {0} has wrong parity for {1:?}")]
21 WrongParity(u64, Role),
22 /// MAX_REQUEST_ID must only increase; it decreased.
23 #[error("max request ID can only increase: was {0}, got {1}")]
24 Decreased(u64, u64),
25 /// No request IDs are available (max is 0 or exhausted).
26 #[error("no request IDs available (blocked)")]
27 Blocked,
28 /// A new request carried an ID other than the next one in the peer's own
29 /// sequence.
30 #[error("request ID {got} is not the peer's next in sequence, which is {expected}")]
31 OutOfSequence {
32 /// The ID the request carried.
33 got: u64,
34 /// The ID the peer's next new request had to carry.
35 expected: u64,
36 },
37}
38
39/// Allocates this endpoint's request IDs and checks the parity of the peer's.
40///
41/// Draft-13 Section 8.1: "The client's Request ID starts at 0 and are even and
42/// the server's Request ID starts at 1 and are odd. The Request ID increments
43/// by 2 with ANNOUNCE, FETCH, SUBSCRIBE, SUBSCRIBE_NAMESPACE or TRACK_STATUS
44/// request." The step of two is what keeps the two endpoints' ID spaces
45/// disjoint, so it is not a spacing convention an implementation may tighten:
46/// one that steps by one starts issuing the IDs reserved for its peer on its
47/// second request, and the same section makes that a session close with
48/// Invalid Request ID.
49///
50/// The ceiling is exclusive. Section 8.5 describes the MAX_REQUEST_ID
51/// message's Request ID field as "The new Maximum Request ID for the session
52/// plus one", and closes the session with 'Too Many Requests' on a request ID
53/// "equal or larger than this". Section 8.3.2.2 gives the setup parameter of
54/// the same name a default of 0 and reads that as "the peer MUST NOT send
55/// requests", which only holds if a ceiling of 0 forbids the ID 0 as well.
56pub struct RequestIdAllocator {
57 role: Role,
58 next_id: u64,
59 max_id: u64,
60 peer_next_id: u64,
61}
62
63impl RequestIdAllocator {
64 /// Create an allocator for the given role, starting at ID 0 or 1 with a
65 /// ceiling of 0 - blocked until the peer raises it.
66 pub fn new(role: Role) -> Self {
67 let next_id = match role {
68 Role::Client => 0,
69 Role::Server => 1,
70 };
71 // The peer's sequence is the other half of the space, and starts at
72 // the other end for the same reason this one does.
73 let peer_next_id = match role {
74 Role::Client => 1,
75 Role::Server => 0,
76 };
77 Self { role, next_id, max_id: 0, peer_next_id }
78 }
79
80 /// The role this allocator allocates for.
81 pub fn role(&self) -> Role {
82 self.role
83 }
84
85 /// Allocate the next request ID.
86 ///
87 /// # Errors
88 ///
89 /// [`RequestIdError::Blocked`] when the next ID would reach the ceiling.
90 pub fn allocate(&mut self) -> Result<VarInt, RequestIdError> {
91 if self.is_blocked() {
92 return Err(RequestIdError::Blocked);
93 }
94 let id = VarInt::from_u64(self.next_id).unwrap();
95 self.next_id += 2;
96 Ok(id)
97 }
98
99 /// Update the maximum allowed request ID (can only increase).
100 ///
101 /// # Errors
102 ///
103 /// [`RequestIdError::Decreased`] if the new value is not strictly greater
104 /// than the current one, which Section 8.5 calls a protocol
105 /// violation.
106 pub fn update_max(&mut self, new_max: u64) -> Result<(), RequestIdError> {
107 if new_max <= self.max_id {
108 return Err(RequestIdError::Decreased(self.max_id, new_max));
109 }
110 self.max_id = new_max;
111 Ok(())
112 }
113
114 /// Check the parity of a request ID the peer put on the wire.
115 ///
116 /// The parity checked here is the **peer's**, the opposite of this
117 /// allocator's own, so a client accepts only odd IDs.
118 ///
119 /// This deliberately does not check the ID against a ceiling. The ceiling
120 /// that applies to a peer's request ID is the MAX_REQUEST_ID *this*
121 /// endpoint advertised, which the allocator does not hold - `max_id` here
122 /// is the budget the peer granted us, a different number that may be
123 /// larger or smaller. The endpoint owns that check.
124 ///
125 /// # Errors
126 ///
127 /// [`RequestIdError::WrongParity`] carrying the ID and the **peer's**
128 /// role, so the message names the endpoint that broke the rule rather than
129 /// the one that caught it.
130 pub fn validate_peer_id(&self, id: u64) -> Result<(), RequestIdError> {
131 let peer_role = match self.role {
132 Role::Client => Role::Server,
133 Role::Server => Role::Client,
134 };
135 let peer_sends_even = peer_role == Role::Client;
136 if id.is_multiple_of(2) != peer_sends_even {
137 return Err(RequestIdError::WrongParity(id, peer_role));
138 }
139 Ok(())
140 }
141
142 /// Whether the next allocation would reach the ceiling.
143 ///
144 /// A ceiling of 0 needs no special case: the client's first ID is 0 and
145 /// the server's is 1, and neither is below 0.
146 pub fn is_blocked(&self) -> bool {
147 self.next_id >= self.max_id
148 }
149
150 /// The Request ID the peer's next **new** request must carry.
151 ///
152 /// Section 8.1 starts each endpoint's sequence at 0 or 1 by role and
153 /// steps it by 2 per request, so the whole sequence is fixed from the
154 /// outset and the next value is a number rather than a guess.
155 pub fn peer_next_id(&self) -> u64 {
156 self.peer_next_id
157 }
158
159 /// Take the Request ID of a new request the peer sent, holding it to the
160 /// sequence Section 8.1 fixes.
161 ///
162 /// "If an endpoint receives a Request ID that is not valid for the peer,
163 /// or a new request with a Request ID that is not expected, it MUST
164 /// close the session with Invalid Request ID."
165 ///
166 /// One counter answers both halves of that. A repeat is below the next
167 /// value and a skip is above it, and neither is the value the peer's own
168 /// step of two produces, so nothing further has to be remembered: the set
169 /// of IDs already spent is every value of this parity below the counter.
170 ///
171 /// Only a **new** request advances this. A message that names a request
172 /// already open - a response, a cancellation, an update that carries the
173 /// original's ID rather than one of its own - reuses an ID on purpose, and
174 /// passing one here would refuse it.
175 ///
176 /// # Errors
177 ///
178 /// [`RequestIdError::OutOfSequence`] carrying the ID that arrived and the
179 /// one the sequence called for.
180 pub fn record_peer_id(&mut self, id: u64) -> Result<(), RequestIdError> {
181 if id != self.peer_next_id {
182 return Err(RequestIdError::OutOfSequence { got: id, expected: self.peer_next_id });
183 }
184 self.peer_next_id += 2;
185 Ok(())
186 }
187
188 /// Get the current maximum request ID.
189 pub fn max_id(&self) -> u64 {
190 self.max_id
191 }
192}