Skip to main content

moqtap_client/draft16/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-16 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, REQUEST_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}