1use bytes::{Bytes, BytesMut};
32
33use super::{FetchResponse, FetchableTopicResponse, PartitionData};
34use crate::{
35 ProtocolError,
36 primitives::{
37 array::{array_len_prefix_len, put_array_len, put_nullable_array_len},
38 fixed::{put_i16, put_i32, put_i64},
39 string_bytes::{put_compact_string, put_string},
40 uuid::put_uuid,
41 },
42 records::RecordsPayload,
43 tagged_fields::{WriteTaggedFields, encode_to_bytes},
44};
45
46#[derive(Debug, Clone)]
60pub enum FetchWriteOp {
61 Inline(Bytes),
64 Records(RecordsPayload),
67}
68
69impl FetchWriteOp {
70 #[must_use]
72 pub fn len(&self) -> usize {
73 match self {
74 Self::Inline(b) => b.len(),
75 Self::Records(p) => p.payload_len(),
76 }
77 }
78
79 #[must_use]
81 pub fn is_empty(&self) -> bool {
82 self.len() == 0
83 }
84}
85
86impl FetchResponse {
87 pub fn write_plan(&self, version: i16) -> Result<Vec<FetchWriteOp>, ProtocolError> {
99 debug_assert!(
100 version >= 4,
101 "write_plan is only valid for the canonical Fetch codec (v4+)"
102 );
103 let flex = version >= 12;
104 let mut ops: Vec<FetchWriteOp> = Vec::new();
105 let mut buf = BytesMut::new();
108
109 if version >= 1 {
111 put_i32(&mut buf, self.throttle_time_ms);
112 }
113 if version >= 7 {
114 put_i16(&mut buf, self.error_code);
115 }
116 if version >= 7 {
117 put_i32(&mut buf, self.session_id);
118 }
119 put_array_len(&mut buf, self.responses.len(), flex);
121 for topic in &self.responses {
122 encode_topic(&mut buf, &mut ops, topic, version, flex)?;
123 }
124
125 if flex {
129 let mut tagged = WriteTaggedFields::new();
130 if !crate::codegen_helpers::is_default(&self.node_endpoints) {
131 let node_endpoints = &self.node_endpoints;
132 let payload = encode_to_bytes(
133 {
134 let prefix = array_len_prefix_len(node_endpoints.len(), flex);
135 let body: usize = node_endpoints
136 .iter()
137 .map(|it| crate::Encode::encoded_len(it, version))
138 .sum();
139 prefix + body
140 },
141 |b| {
142 put_array_len(b, node_endpoints.len(), flex);
143 for it in node_endpoints {
144 crate::Encode::encode(it, b, version)?;
145 }
146 Ok(())
147 },
148 );
149 tagged.add(0, payload);
150 }
151 tagged.write(&mut buf, &self.unknown_tagged_fields);
152 }
153
154 if !buf.is_empty() {
155 ops.push(FetchWriteOp::Inline(buf.freeze()));
156 }
157 Ok(ops)
158 }
159}
160
161fn encode_topic(
165 buf: &mut BytesMut,
166 ops: &mut Vec<FetchWriteOp>,
167 topic: &FetchableTopicResponse,
168 version: i16,
169 flex: bool,
170) -> Result<(), ProtocolError> {
171 if (0..=12).contains(&version) {
172 if flex {
173 put_compact_string(buf, &topic.topic);
174 } else {
175 put_string(buf, &topic.topic);
176 }
177 }
178 if version >= 13 {
179 put_uuid(buf, topic.topic_id);
180 }
181 put_array_len(buf, topic.partitions.len(), flex);
183 for partition in &topic.partitions {
184 encode_partition(buf, ops, partition, version, flex)?;
185 }
186 if flex {
187 let tagged = WriteTaggedFields::new();
189 tagged.write(buf, &topic.unknown_tagged_fields);
190 }
191 Ok(())
192}
193
194fn encode_partition(
198 buf: &mut BytesMut,
199 ops: &mut Vec<FetchWriteOp>,
200 p: &PartitionData,
201 version: i16,
202 flex: bool,
203) -> Result<(), ProtocolError> {
204 put_i32(buf, p.partition_index);
205 put_i16(buf, p.error_code);
206 put_i64(buf, p.high_watermark);
207 if version >= 4 {
208 put_i64(buf, p.last_stable_offset);
209 }
210 if version >= 5 {
211 put_i64(buf, p.log_start_offset);
212 }
213 if version >= 4 {
214 let len = p.aborted_transactions.as_ref().map(Vec::len);
215 put_nullable_array_len(buf, len, flex);
216 if let Some(v) = &p.aborted_transactions {
217 for it in v {
218 crate::Encode::encode(it, buf, version)?;
219 }
220 }
221 }
222 if version >= 11 {
223 put_i32(buf, p.preferred_read_replica);
224 }
225 match &p.records {
228 None => {
229 if flex {
230 crate::primitives::varint::put_uvarint(buf, 0);
232 } else {
233 put_i32(buf, -1);
234 }
235 }
236 Some(payload) => {
237 let payload_len = payload.payload_len();
238 if flex {
239 let prefixed = u32::try_from(payload_len + 1).map_err(|_| {
241 ProtocolError::InvalidValue("records too large for compact len")
242 })?;
243 crate::primitives::varint::put_uvarint(buf, prefixed);
244 } else {
245 let len = i32::try_from(payload_len)
246 .map_err(|_| ProtocolError::InvalidValue("records too large for i32 len"))?;
247 put_i32(buf, len);
248 }
249 flush_inline(buf, ops);
252 ops.push(FetchWriteOp::Records(payload.clone()));
253 }
254 }
255 if flex {
256 let mut tagged = WriteTaggedFields::new();
260 if !crate::codegen_helpers::is_default(&p.diverging_epoch) {
261 let payload = encode_to_bytes(
262 crate::Encode::encoded_len(&p.diverging_epoch, version),
263 |b| {
264 crate::Encode::encode(&p.diverging_epoch, b, version)?;
265 Ok(())
266 },
267 );
268 tagged.add(0, payload);
269 }
270 if !crate::codegen_helpers::is_default(&p.current_leader) {
271 let payload = encode_to_bytes(
272 crate::Encode::encoded_len(&p.current_leader, version),
273 |b| {
274 crate::Encode::encode(&p.current_leader, b, version)?;
275 Ok(())
276 },
277 );
278 tagged.add(1, payload);
279 }
280 if !crate::codegen_helpers::is_default(&p.snapshot_id) {
281 let payload =
282 encode_to_bytes(crate::Encode::encoded_len(&p.snapshot_id, version), |b| {
283 crate::Encode::encode(&p.snapshot_id, b, version)?;
284 Ok(())
285 });
286 tagged.add(2, payload);
287 }
288 tagged.write(buf, &p.unknown_tagged_fields);
289 }
290 Ok(())
291}
292
293#[inline]
298fn flush_inline(buf: &mut BytesMut, ops: &mut Vec<FetchWriteOp>) {
299 if !buf.is_empty() {
300 ops.push(FetchWriteOp::Inline(buf.split().freeze()));
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use assert2::assert;
307 use bytes::BytesMut;
308
309 use super::{
310 super::{AbortedTransaction, FetchableTopicResponse, NodeEndpoint, PartitionData},
311 *,
312 };
313 use crate::{Encode, records::RecordsPayload};
314
315 fn concat_plan(ops: &[FetchWriteOp]) -> Vec<u8> {
319 let mut out = BytesMut::new();
320 for op in ops {
321 match op {
322 FetchWriteOp::Inline(b) => out.extend_from_slice(b),
323 FetchWriteOp::Records(p) => p.encode_to(&mut out).unwrap(),
324 }
325 }
326 out.to_vec()
327 }
328
329 fn raw_batch_bytes(base_offset: i64) -> Bytes {
330 use crate::records::{Record, RecordBatch};
333 let rb = RecordBatch {
334 base_offset,
335 records: vec![Record {
336 key: Some(Bytes::from_static(b"k")),
337 value: Some(Bytes::from_static(b"val")),
338 ..Default::default()
339 }],
340 ..RecordBatch::default()
341 };
342 let mut buf = BytesMut::new();
343 rb.encode(&mut buf).unwrap();
344 buf.freeze()
345 }
346
347 fn assert_plan_matches_encode(resp: &FetchResponse, version: i16) {
351 let mut canonical = BytesMut::new();
352 resp.encode(&mut canonical, version).unwrap();
353 let plan = resp.write_plan(version).unwrap();
354 let assembled = concat_plan(&plan);
355 assert!(
356 &assembled[..] == &canonical[..],
357 "write_plan != encode at version {version}: plan_len={} encode_len={}",
358 assembled.len(),
359 canonical.len()
360 );
361 let plan_total: usize = plan.iter().map(FetchWriteOp::len).sum();
364 assert!(plan_total == resp.encoded_len(version));
365 assert!(plan_total == canonical.len());
366 }
367
368 fn multi_partition_response(version: i16) -> FetchResponse {
369 let mut p0_records = BytesMut::new();
373 p0_records.extend_from_slice(&raw_batch_bytes(0));
374 p0_records.extend_from_slice(&raw_batch_bytes(1));
375 let p0 = PartitionData {
376 partition_index: 0,
377 high_watermark: 2,
378 last_stable_offset: 2,
379 log_start_offset: 0,
380 records: Some(RecordsPayload::Raw(p0_records.freeze())),
381 ..PartitionData::default()
382 };
383 let p1 = PartitionData {
384 partition_index: 1,
385 high_watermark: 1,
386 last_stable_offset: 1,
387 log_start_offset: 0,
388 aborted_transactions: if version >= 4 {
391 Some(vec![AbortedTransaction {
392 producer_id: 7,
393 first_offset: 0,
394 ..AbortedTransaction::default()
395 }])
396 } else {
397 None
398 },
399 records: Some(RecordsPayload::Raw(raw_batch_bytes(0))),
400 ..PartitionData::default()
401 };
402 let p2 = PartitionData {
403 partition_index: 2,
404 error_code: 0,
405 high_watermark: 0,
406 records: None,
407 ..PartitionData::default()
408 };
409 let topic0 = FetchableTopicResponse {
410 topic: if version <= 12 {
411 "topic-a".to_string()
412 } else {
413 String::new()
414 },
415 topic_id: if version >= 13 {
416 crate::primitives::uuid::Uuid([9u8; 16])
417 } else {
418 crate::primitives::uuid::Uuid([0u8; 16])
419 },
420 partitions: vec![p0, p1, p2],
421 ..FetchableTopicResponse::default()
422 };
423 let topic1 = FetchableTopicResponse {
424 topic: if version <= 12 {
425 "topic-b".to_string()
426 } else {
427 String::new()
428 },
429 topic_id: if version >= 13 {
430 crate::primitives::uuid::Uuid([3u8; 16])
431 } else {
432 crate::primitives::uuid::Uuid([0u8; 16])
433 },
434 partitions: vec![PartitionData {
435 partition_index: 0,
436 high_watermark: 1,
437 last_stable_offset: 1,
438 log_start_offset: 0,
439 records: Some(RecordsPayload::Raw(raw_batch_bytes(5))),
440 ..PartitionData::default()
441 }],
442 ..FetchableTopicResponse::default()
443 };
444 FetchResponse {
445 throttle_time_ms: 3,
446 error_code: 0,
447 session_id: 42,
448 responses: vec![topic0, topic1],
449 node_endpoints: if version >= 16 {
450 vec![NodeEndpoint {
451 node_id: 1,
452 host: "h".to_string(),
453 port: 9092,
454 rack: None,
455 ..NodeEndpoint::default()
456 }]
457 } else {
458 Vec::new()
459 },
460 ..FetchResponse::default()
461 }
462 }
463
464 #[test]
465 fn fetch_response_plan_matches_encode_all_versions() {
466 for version in 4..=18 {
469 assert_plan_matches_encode(&multi_partition_response(version), version);
470 }
471 }
472
473 #[test]
474 fn fetch_response_plan_matches_encode_populated() {
475 for version in 4..=18 {
479 assert_plan_matches_encode(&FetchResponse::populated(version), version);
480 }
481 }
482
483 #[test]
484 fn fetch_response_plan_matches_encode_default() {
485 for version in 4..=18 {
486 assert_plan_matches_encode(&FetchResponse::default(), version);
487 }
488 }
489
490 #[test]
491 fn truncated_trailing_batch_rides_through_verbatim() {
492 for version in [4i16, 11, 12, 18] {
497 let mut records = BytesMut::new();
498 records.extend_from_slice(&raw_batch_bytes(0));
499 records.extend_from_slice(&raw_batch_bytes(1));
500 records.extend_from_slice(&[0u8; 9]);
502 let resp = FetchResponse {
503 throttle_time_ms: 0,
504 session_id: 1,
505 responses: vec![FetchableTopicResponse {
506 topic: if version <= 12 {
507 "t".to_string()
508 } else {
509 String::new()
510 },
511 topic_id: if version >= 13 {
512 crate::primitives::uuid::Uuid([1u8; 16])
513 } else {
514 crate::primitives::uuid::Uuid([0u8; 16])
515 },
516 partitions: vec![PartitionData {
517 partition_index: 0,
518 high_watermark: 2,
519 last_stable_offset: 2,
520 log_start_offset: 0,
521 records: Some(RecordsPayload::Raw(records.freeze())),
522 ..PartitionData::default()
523 }],
524 ..FetchableTopicResponse::default()
525 }],
526 ..FetchResponse::default()
527 };
528 assert_plan_matches_encode(&resp, version);
529 }
530 }
531
532 #[test]
533 fn empty_responses_plan_matches_encode() {
534 for version in 4..=18 {
535 let resp = FetchResponse {
536 throttle_time_ms: 0,
537 session_id: 0,
538 responses: Vec::new(),
539 ..FetchResponse::default()
540 };
541 assert_plan_matches_encode(&resp, version);
542 }
543 }
544}