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