1use {
6 crate::{
7 error::{MetadataReplayError, RaftError},
8 kraft::types::{Epoch, NodeId},
9 },
10 krabka_ids::Offset,
11 krabka_metadata::{MetadataImage, MetadataRecord, TranslateError, VoterSet, from_kraft_value},
12 krabka_protocol::{
13 Decode, Encode,
14 owned::no_op_record::NoOpRecord,
15 records::{
16 self, Record, RecordBatch,
17 metadata::{KraftMetadataRecord, control::ControlRecord, decode_value_header},
18 },
19 },
20};
21
22pub fn decode_committed_value(
62 value: &[u8],
63 image: &MetadataImage,
64 offset: i64,
65) -> Result<Option<MetadataRecord>, MetadataReplayError> {
66 if is_kip835_noop(value) {
68 return Ok(None);
69 }
70 match from_kraft_value(value, image) {
71 Ok(record) => Ok(Some(record)),
72 Err(
73 error @ (TranslateError::UnknownTopicId(_)
74 | TranslateError::UnknownTopicName(_)
75 | TranslateError::UnknownAclId(_)
76 | TranslateError::InvalidReference { .. }),
77 ) => {
78 tracing::warn!(
79 offset,
80 %error,
81 "kraft: skipped a committed record that names state the image does not hold"
82 );
83 Ok(None)
84 }
85 Err(error) => Err(MetadataReplayError::UndecodableRecord { offset, error }),
86 }
87}
88
89const NO_OP_RECORD_API_KEY: u32 = 20;
91
92pub fn noop_record_value() -> Result<bytes::Bytes, RaftError> {
98 Ok(KraftMetadataRecord::NoOp(NoOpRecord::default()).encode_value(0)?)
99}
100
101#[must_use]
105pub fn is_kip835_noop(value: &[u8]) -> bool {
106 let mut header = value;
107 if decode_value_header(&mut header).ok().map(|h| h.api_key) != Some(NO_OP_RECORD_API_KEY) {
108 return false;
109 }
110 matches!(
111 KraftMetadataRecord::decode_value(value),
112 Ok((KraftMetadataRecord::NoOp(record), _)) if record.unknown_tagged_fields.0.is_empty()
113 )
114}
115
116pub fn metadata_record_batch(
117 leader_epoch: Epoch,
118 blobs: &[bytes::Bytes],
119) -> Result<RecordBatch, RaftError> {
120 if blobs.is_empty() {
121 return Ok(RecordBatch {
122 partition_leader_epoch: i32::try_from(leader_epoch).unwrap_or(i32::MAX),
123 ..Default::default()
124 });
125 }
126
127 let deltas = krabka_verified::metadata_record_offset_deltas(blobs.len()).ok_or_else(|| {
128 RaftError::ChangeRejected("metadata batch offset deltas exceed i32".to_owned())
129 })?;
130 let last_offset_delta = deltas.last().copied().unwrap_or_default();
132 let records: Vec<Record> = blobs
133 .iter()
134 .zip(deltas)
135 .map(|(blob, offset_delta)| Record {
136 offset_delta,
137 value: Some(blob.clone()),
138 ..Default::default()
139 })
140 .collect();
141
142 Ok(RecordBatch {
143 partition_leader_epoch: i32::try_from(leader_epoch).unwrap_or(i32::MAX),
144 last_offset_delta,
145 records,
146 ..Default::default()
147 })
148}
149
150pub fn typed_control_batch(
151 leader_epoch: Epoch,
152 controls: &[ControlRecord],
153) -> Result<RecordBatch, RaftError> {
154 let records = controls
155 .iter()
156 .enumerate()
157 .map(|(index, control)| {
158 let (key, value) = control.encode_key_value()?;
159 Ok(Record {
160 offset_delta: i32::try_from(index).unwrap_or(i32::MAX),
161 key: Some(key),
162 value: Some(value),
163 ..Default::default()
164 })
165 })
166 .collect::<Result<Vec<_>, krabka_protocol::ProtocolError>>()?;
167 Ok(RecordBatch {
168 partition_leader_epoch: i32::try_from(leader_epoch).unwrap_or(i32::MAX),
169 attributes: records::Attributes::default().with_control(true),
170 last_offset_delta: i32::try_from(controls.len().saturating_sub(1)).unwrap_or(i32::MAX),
171 records,
172 ..Default::default()
173 })
174}
175
176pub fn decode_control_record(record: &Record) -> Result<ControlRecord, RaftError> {
183 let key = match &record.key {
184 None => {
185 return Err(RaftError::MalformedControlRecord(
186 "Missing key in the record when a key was expected",
187 ));
188 }
189 Some(key) if key.is_empty() => {
190 return Err(RaftError::MalformedControlRecord(
191 "Got an unexpected empty key in the record",
192 ));
193 }
194 Some(key) => key,
195 };
196 let value = match &record.value {
197 None => {
198 return Err(RaftError::MalformedControlRecord(
199 "Missing value in the record when a value was expected",
200 ));
201 }
202 Some(value) if value.is_empty() => {
203 return Err(RaftError::MalformedControlRecord(
204 "Got an unexpected empty value in the record",
205 ));
206 }
207 Some(value) => value,
208 };
209 Ok(ControlRecord::decode(key, value)?)
210}
211
212#[cfg_attr(test, mutants::skip)]
229pub fn leader_change_batch(epoch: Epoch, leader_id: NodeId, voter_set: &VoterSet) -> RecordBatch {
230 use krabka_protocol::{
231 Encode,
232 owned::{
233 common::leader_change_message::voter::Voter, leader_change_message::LeaderChangeMessage,
234 },
235 records::{
236 header::Attributes,
237 metadata::control::{ControlRecordType, control_record_key},
238 },
239 };
240
241 let voters: Vec<Voter> = voter_set
242 .iter()
243 .map(|voter| Voter {
244 voter_id: i32::try_from(voter.id.0).unwrap_or(i32::MAX),
245 ..Default::default()
246 })
247 .collect();
248 let msg = LeaderChangeMessage {
249 version: 0,
250 leader_id: i32::try_from(leader_id.0).unwrap_or(i32::MAX),
251 voters: voters.clone(),
252 granting_voters: voters,
253 ..Default::default()
254 };
255 let mut value = bytes::BytesMut::new();
256 let _ = msg.encode(&mut value, 0);
258 let key = control_record_key(ControlRecordType::LeaderChange);
259 RecordBatch {
260 partition_leader_epoch: i32::try_from(epoch).unwrap_or(i32::MAX),
261 attributes: Attributes::default().with_control(true),
262 last_offset_delta: 0,
263 records: vec![Record {
264 offset_delta: 0,
265 key: Some(key),
266 value: Some(value.freeze()),
267 ..Default::default()
268 }],
269 ..Default::default()
270 }
271}
272
273pub fn start_of_epoch_batch(
279 epoch: Epoch,
280 leader_id: NodeId,
281 voter_set: &VoterSet,
282 controls: &[ControlRecord],
283) -> Result<RecordBatch, RaftError> {
284 let mut batch = leader_change_batch(epoch, leader_id, voter_set);
285 for (offset_delta, control) in (1_i32..).zip(controls) {
286 let (key, value) = control.encode_key_value()?;
287 batch.records.push(Record {
288 offset_delta,
289 key: Some(key),
290 value: Some(value),
291 ..Default::default()
292 });
293 batch.last_offset_delta = offset_delta;
294 }
295 Ok(batch)
296}
297
298pub fn encode_batches(batches: &[RecordBatch]) -> bytes::Bytes {
303 let mut out = bytes::BytesMut::new();
304 for batch in batches {
305 if let Err(e) = batch.encode(&mut out) {
306 tracing::error!(?e, "kraft: encode batch for fetch serve failed");
307 }
308 }
309 out.freeze()
310}
311
312pub fn decode_batches(mut buf: &[u8]) -> Result<Vec<RecordBatch>, RaftError> {
316 let mut out = Vec::new();
317 while !buf.is_empty() {
318 match RecordBatch::decode(&mut buf) {
319 Ok(batch) => out.push(batch),
320 Err(e) => {
321 return Err(RaftError::ChangeRejected(format!(
322 "decode replicated batch: {e}"
323 )));
324 }
325 }
326 }
327 Ok(out)
328}
329
330#[must_use]
333pub fn batch_last_offset(batch: &RecordBatch) -> i64 {
334 batch
335 .base_offset
336 .saturating_add(i64::from(batch.last_offset_delta))
337}
338
339#[must_use]
343pub fn is_features_delta(record: &MetadataRecord) -> bool {
344 matches!(record, MetadataRecord::V1FeatureLevel(_))
345}
346
347#[must_use]
352pub fn features_epoch_after_snapshot(
353 records: &[MetadataRecord],
354 end_offset: i64,
355 previous: i64,
356) -> i64 {
357 if records.iter().any(is_features_delta) {
358 end_offset.saturating_sub(1)
359 } else {
360 previous
361 }
362}
363
364pub fn next_batch_offset(batches: &[RecordBatch]) -> Option<Offset> {
365 batches.last().map(|batch| {
366 Offset(
367 batch
368 .base_offset
369 .saturating_add(i64::from(batch.last_offset_delta))
370 .saturating_add(1),
371 )
372 })
373}
374
375#[cfg(test)]
376mod metadata_record_batch_tests {
377 use {
378 super::*, assert2::check, krabka_protocol::owned::k_raft_version_record::KRaftVersionRecord,
379 };
380
381 #[yare::parameterized(
382 empty = { Vec::new(), Vec::new() },
383 single = { vec![bytes::Bytes::from_static(b"a")], vec![0] },
384 multiple = {
385 vec![
386 bytes::Bytes::from_static(b"a"),
387 bytes::Bytes::from_static(b"b"),
388 bytes::Bytes::from_static(b"c"),
389 ],
390 vec![0, 1, 2],
391 },
392 )]
393 fn metadata_record_offset_deltas_cover_empty_single_and_multiple_batches(
394 blobs: Vec<bytes::Bytes>,
395 expected_deltas: Vec<i32>,
396 ) {
397 let batch = metadata_record_batch(5, &blobs).expect("metadata batch");
398 check!(batch.partition_leader_epoch == 5);
399 check!(
400 batch
401 .records
402 .iter()
403 .map(|record| record.offset_delta)
404 .collect::<Vec<_>>()
405 == expected_deltas
406 );
407 check!(batch.last_offset_delta == expected_deltas.last().copied().unwrap_or(0));
408 }
409
410 #[test]
411 fn metadata_control_batch_keeps_the_leader_epoch() {
412 let control = typed_control_batch(7, &[ControlRecord::KRaftVersion(
413 KRaftVersionRecord::default(),
414 )])
415 .expect("control batch");
416 check!(control.partition_leader_epoch == 7);
417 }
418}
419
420#[cfg(test)]
421mod features_epoch_tests {
422 use {
423 super::*,
424 crate::test_support::single_partition_topic,
425 assert2::check,
426 krabka_metadata::{FeatureLevelRecord, metadata_version::METADATA_VERSION_FEATURE},
427 };
428
429 fn feature() -> MetadataRecord {
430 MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
431 name: METADATA_VERSION_FEATURE.into(),
432 level: 25,
433 })
434 }
435
436 fn topic() -> MetadataRecord {
437 MetadataRecord::V1Topic(single_partition_topic("a", uuid::Uuid::from_u128(1)))
438 }
439
440 #[yare::parameterized(
444 features_from_initial = { vec![feature(), topic()], 10, -1, 9 },
445 features_replace_earlier = { vec![topic(), feature()], 10, 4, 9 },
446 no_features_keep_initial = { vec![topic()], 10, -1, -1 },
447 no_features_keep_earlier = { vec![topic()], 10, 4, 4 },
448 empty_keeps_earlier = { Vec::new(), 10, 4, 4 },
449 )]
450 fn snapshot_load_moves_the_epoch_only_with_feature_records(
451 records: Vec<MetadataRecord>,
452 end_offset: i64,
453 previous: i64,
454 want: i64,
455 ) {
456 check!(features_epoch_after_snapshot(&records, end_offset, previous) == want);
457 }
458}