Skip to main content

barnabas_core/
conn.rs

1//! Request/response correlation over one broker connection.
2//!
3//! Kafka answers a connection's requests **in the order they were sent**, which
4//! is what makes pipelining cheap: the client can have several requests in
5//! flight and still match responses by position. The correlation id is then a
6//! check rather than a lookup, and this type treats it that way — a mismatch is
7//! fatal, because a stream that has desynchronised cannot be resynchronised.
8//!
9//! Two things this type exists to stop a caller getting wrong, both found in
10//! P0 against a real broker:
11//!
12//! - **The header version is not the API version.** Flexible versions added a
13//!   tagged-field section to the header, so each API and version pair has its
14//!   own header version. Guess it and the response decodes into plausible
15//!   garbage rather than failing.
16//! - **The response header version differs from the request's** for several
17//!   APIs, so it is derived separately.
18
19use std::collections::VecDeque;
20
21use bytes::{Bytes, BytesMut};
22use kafka_protocol::messages::{ApiKey, RequestHeader, ResponseHeader};
23use kafka_protocol::protocol::{encode_request_header_into_buffer, Decodable, Encodable, StrBytes};
24
25use crate::frame::{self, FrameDecoder};
26use crate::{Error, Result};
27
28/// A request awaiting its response.
29#[derive(Debug, Clone, Copy)]
30struct Pending {
31    api_key: ApiKey,
32    version: i16,
33    correlation_id: i32,
34}
35
36/// A response frame, matched to the request that asked for it.
37///
38/// The body is handed back undecoded so the caller decodes into the concrete
39/// response type it expects. That keeps this type free of the 185 message types
40/// and keeps the decode where the caller's error handling already is.
41#[derive(Debug)]
42pub struct PendingResponse {
43    pub api_key: ApiKey,
44    pub version: i16,
45    pub correlation_id: i32,
46    /// The response body, header already consumed.
47    pub body: Bytes,
48}
49
50/// One broker connection's protocol state. Owns no socket.
51#[derive(Debug)]
52pub struct Connection {
53    client_id: StrBytes,
54    next_correlation_id: i32,
55    in_flight: VecDeque<Pending>,
56    decoder: FrameDecoder,
57}
58
59impl Connection {
60    #[must_use]
61    pub fn new(client_id: impl Into<StrBytes>) -> Self {
62        Self {
63            client_id: client_id.into(),
64            next_correlation_id: 0,
65            in_flight: VecDeque::new(),
66            decoder: FrameDecoder::default(),
67        }
68    }
69
70    /// How many requests are awaiting responses.
71    ///
72    /// The producer's in-flight limit is enforced against this: Kafka retains
73    /// idempotence with up to five in flight, and only because sequence numbers
74    /// let the broker order them.
75    #[must_use]
76    pub fn in_flight(&self) -> usize {
77        self.in_flight.len()
78    }
79
80    /// Encode `req` into a framed, ready-to-write buffer and record it as in
81    /// flight.
82    ///
83    /// # Errors
84    /// [`Error::Codec`] if encoding fails.
85    pub fn request<R: Encodable>(
86        &mut self,
87        api_key: ApiKey,
88        version: i16,
89        req: &R,
90    ) -> Result<Bytes> {
91        self.next_correlation_id = self.next_correlation_id.wrapping_add(1);
92        let correlation_id = self.next_correlation_id;
93
94        let mut header = RequestHeader::default();
95        header.request_api_key = api_key as i16;
96        header.request_api_version = version;
97        header.correlation_id = correlation_id;
98        header.client_id = Some(self.client_id.clone());
99
100        let mut body = BytesMut::new();
101        // The helper derives the header version from the key and version, which
102        // is exactly the thing not to hand-pick.
103        encode_request_header_into_buffer(&mut body, &header)
104            .map_err(|e| Error::Codec(format!("encode header: {e}")))?;
105        req.encode(&mut body, version)
106            .map_err(|e| Error::Codec(format!("encode {api_key:?} v{version}: {e}")))?;
107
108        self.in_flight.push_back(Pending {
109            api_key,
110            version,
111            correlation_id,
112        });
113        frame::frame(&body)
114    }
115
116    /// Feed bytes from the socket.
117    pub fn push_bytes(&mut self, bytes: &[u8]) {
118        self.decoder.push(bytes);
119    }
120
121    /// How many more bytes the next response needs. See
122    /// [`FrameDecoder::needed`](crate::frame::FrameDecoder::needed).
123    #[must_use]
124    pub fn needed(&self) -> usize {
125        self.decoder.needed()
126    }
127
128    /// Take the next complete response, matched to its request.
129    ///
130    /// # Errors
131    /// [`Error::Unsolicited`] if nothing was in flight, [`Error::Correlation`]
132    /// if the id does not match the oldest in-flight request. Both are fatal
133    /// for the connection.
134    pub fn next_response(&mut self) -> Result<Option<PendingResponse>> {
135        let Some(mut frame) = self.decoder.next_frame()? else {
136            return Ok(None);
137        };
138        let pending = self.in_flight.front().copied().ok_or(Error::Unsolicited)?;
139
140        let header = ResponseHeader::decode(
141            &mut frame,
142            pending.api_key.response_header_version(pending.version),
143        )
144        .map_err(|e| Error::Codec(format!("decode response header: {e}")))?;
145
146        if header.correlation_id != pending.correlation_id {
147            // Left in flight deliberately: the caller's only correct move is to
148            // drop the connection, and popping would imply recovery.
149            return Err(Error::Correlation {
150                got: header.correlation_id,
151                expected: pending.correlation_id,
152            });
153        }
154        self.in_flight.pop_front();
155
156        Ok(Some(PendingResponse {
157            api_key: pending.api_key,
158            version: pending.version,
159            correlation_id: header.correlation_id,
160            body: frame,
161        }))
162    }
163
164    /// Decode a response body into its concrete type.
165    ///
166    /// # Errors
167    /// [`Error::Codec`] if decoding fails.
168    pub fn decode<R: Decodable>(resp: &PendingResponse) -> Result<R> {
169        let mut body = resp.body.clone();
170        R::decode(&mut body, resp.version).map_err(|e| {
171            Error::Codec(format!(
172                "decode {:?} v{} response: {e}",
173                resp.api_key, resp.version
174            ))
175        })
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use kafka_protocol::messages::{ApiVersionsRequest, ApiVersionsResponse};
183
184    /// Encode a response the way a broker would, so the tests drive the real
185    /// decode path rather than a mock of it.
186    fn broker_response<R: Encodable>(
187        api_key: ApiKey,
188        version: i16,
189        correlation_id: i32,
190        resp: &R,
191    ) -> Bytes {
192        let mut header = ResponseHeader::default();
193        header.correlation_id = correlation_id;
194        let mut body = BytesMut::new();
195        header
196            .encode(&mut body, api_key.response_header_version(version))
197            .unwrap();
198        resp.encode(&mut body, version).unwrap();
199        frame::frame(&body).unwrap()
200    }
201
202    fn api_versions_response(api_count: usize) -> ApiVersionsResponse {
203        let mut resp = ApiVersionsResponse::default();
204        resp.api_keys = (0..api_count)
205            .map(|i| {
206                let mut k = kafka_protocol::messages::api_versions_response::ApiVersion::default();
207                k.api_key = i as i16;
208                k.max_version = 1;
209                k
210            })
211            .collect();
212        resp
213    }
214
215    #[test]
216    fn a_request_and_its_response_are_matched() {
217        let mut conn = Connection::new(StrBytes::from_static_str("test"));
218        let req = ApiVersionsRequest::default();
219        let _wire = conn.request(ApiKey::ApiVersions, 3, &req).unwrap();
220        assert_eq!(conn.in_flight(), 1);
221
222        conn.push_bytes(&broker_response(
223            ApiKey::ApiVersions,
224            3,
225            1,
226            &api_versions_response(2),
227        ));
228        let resp = conn.next_response().unwrap().expect("a response");
229        assert_eq!(resp.correlation_id, 1);
230        assert_eq!(conn.in_flight(), 0);
231
232        let decoded: ApiVersionsResponse = Connection::decode(&resp).unwrap();
233        assert_eq!(decoded.api_keys.len(), 2);
234    }
235
236    /// Pipelining is the point of correlation ids: three requests out, three
237    /// responses back in order, all matched.
238    #[test]
239    fn pipelined_requests_match_in_order() {
240        let mut conn = Connection::new(StrBytes::from_static_str("test"));
241        for _ in 0..3 {
242            conn.request(ApiKey::ApiVersions, 3, &ApiVersionsRequest::default())
243                .unwrap();
244        }
245        assert_eq!(conn.in_flight(), 3);
246
247        for id in 1..=3 {
248            conn.push_bytes(&broker_response(
249                ApiKey::ApiVersions,
250                3,
251                id,
252                &api_versions_response(id as usize),
253            ));
254        }
255        for id in 1..=3 {
256            let resp = conn.next_response().unwrap().expect("a response");
257            assert_eq!(resp.correlation_id, id);
258            let decoded: ApiVersionsResponse = Connection::decode(&resp).unwrap();
259            assert_eq!(decoded.api_keys.len(), id as usize);
260        }
261        assert_eq!(conn.in_flight(), 0);
262    }
263
264    /// A desynchronised stream is fatal, and must not silently pop the pending
265    /// request — otherwise the *next* response would be matched to the wrong
266    /// request and the corruption would spread instead of stopping.
267    #[test]
268    fn a_correlation_mismatch_is_fatal_and_does_not_advance() {
269        let mut conn = Connection::new(StrBytes::from_static_str("test"));
270        conn.request(ApiKey::ApiVersions, 3, &ApiVersionsRequest::default())
271            .unwrap();
272        conn.push_bytes(&broker_response(
273            ApiKey::ApiVersions,
274            3,
275            99,
276            &api_versions_response(1),
277        ));
278        assert!(matches!(
279            conn.next_response(),
280            Err(Error::Correlation {
281                got: 99,
282                expected: 1
283            })
284        ));
285        assert_eq!(conn.in_flight(), 1);
286    }
287
288    #[test]
289    fn a_response_with_nothing_in_flight_is_an_error() {
290        let mut conn = Connection::new(StrBytes::from_static_str("test"));
291        conn.push_bytes(&broker_response(
292            ApiKey::ApiVersions,
293            3,
294            1,
295            &api_versions_response(1),
296        ));
297        assert!(matches!(conn.next_response(), Err(Error::Unsolicited)));
298    }
299
300    /// Half a response is not a response.
301    #[test]
302    fn a_partial_response_yields_nothing_yet() {
303        let mut conn = Connection::new(StrBytes::from_static_str("test"));
304        conn.request(ApiKey::ApiVersions, 3, &ApiVersionsRequest::default())
305            .unwrap();
306        let wire = broker_response(ApiKey::ApiVersions, 3, 1, &api_versions_response(4));
307        conn.push_bytes(&wire[..wire.len() - 1]);
308        assert!(conn.next_response().unwrap().is_none());
309        conn.push_bytes(&wire[wire.len() - 1..]);
310        assert!(conn.next_response().unwrap().is_some());
311    }
312
313    /// Correlation ids must be distinct per request; a duplicate would make the
314    /// mismatch check useless.
315    #[test]
316    fn correlation_ids_advance() {
317        let mut conn = Connection::new(StrBytes::from_static_str("test"));
318        conn.request(ApiKey::ApiVersions, 3, &ApiVersionsRequest::default())
319            .unwrap();
320        conn.request(ApiKey::ApiVersions, 3, &ApiVersionsRequest::default())
321            .unwrap();
322        conn.push_bytes(&broker_response(
323            ApiKey::ApiVersions,
324            3,
325            1,
326            &api_versions_response(1),
327        ));
328        conn.push_bytes(&broker_response(
329            ApiKey::ApiVersions,
330            3,
331            2,
332            &api_versions_response(1),
333        ));
334        assert_eq!(conn.next_response().unwrap().unwrap().correlation_id, 1);
335        assert_eq!(conn.next_response().unwrap().unwrap().correlation_id, 2);
336    }
337}