Skip to main content

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}