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