1use {
10 crate::{
11 error::RemoteStorageError,
12 metadata::{RemoteLogSegmentId, RemoteLogSegmentState},
13 },
14 krabka_ids::LeaderEpoch,
15 std::collections::BTreeMap,
16};
17
18#[derive(derive_more::From, derive_more::Into)]
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct CustomMetadata(pub Vec<u8>);
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RemoteLogSegmentMetadata {
30 remote_log_segment_id: RemoteLogSegmentId,
31 start_offset: i64,
32 end_offset: i64,
33 max_timestamp_ms: i64,
34 broker_id: i32,
35 event_timestamp_ms: i64,
36 segment_size_in_bytes: i32,
37 custom_metadata: Option<CustomMetadata>,
38 state: RemoteLogSegmentState,
39 segment_leader_epochs: BTreeMap<LeaderEpoch, i64>,
40 txn_index_empty: bool,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct RemoteLogSegmentDetails {
49 segment_size_in_bytes: i32,
50 state: RemoteLogSegmentState,
51 segment_leader_epochs: BTreeMap<LeaderEpoch, i64>,
52}
53
54impl RemoteLogSegmentDetails {
55 #[must_use]
56 pub fn new(
57 segment_size_in_bytes: i32,
58 state: RemoteLogSegmentState,
59 segment_leader_epochs: BTreeMap<LeaderEpoch, i64>,
60 ) -> Self {
61 Self {
62 segment_size_in_bytes,
63 state,
64 segment_leader_epochs,
65 }
66 }
67}
68
69impl RemoteLogSegmentMetadata {
70 pub fn new(
78 remote_log_segment_id: RemoteLogSegmentId,
79 start_offset: i64,
80 end_offset: i64,
81 max_timestamp_ms: i64,
82 broker_id: i32,
83 event_timestamp_ms: i64,
84 details: RemoteLogSegmentDetails,
85 ) -> Result<Self, RemoteStorageError> {
86 let RemoteLogSegmentDetails {
87 segment_size_in_bytes,
88 state,
89 segment_leader_epochs,
90 } = details;
91 if segment_leader_epochs.is_empty() {
92 return Err(RemoteStorageError::InvalidArgument(
93 "segment_leader_epochs must not be empty".into(),
94 ));
95 }
96 if end_offset < start_offset {
97 return Err(RemoteStorageError::InvalidArgument(format!(
98 "end_offset ({end_offset}) < start_offset ({start_offset})"
99 )));
100 }
101 if segment_size_in_bytes < 0 {
102 return Err(RemoteStorageError::InvalidArgument(format!(
103 "segment_size_in_bytes ({segment_size_in_bytes}) must be >= 0"
104 )));
105 }
106 Ok(Self {
107 remote_log_segment_id,
108 start_offset,
109 end_offset,
110 max_timestamp_ms,
111 broker_id,
112 event_timestamp_ms,
113 segment_size_in_bytes,
114 custom_metadata: None,
115 state,
116 segment_leader_epochs,
117 txn_index_empty: false,
118 })
119 }
120
121 pub fn with_update(
133 &self,
134 update: &RemoteLogSegmentMetadataUpdate,
135 ) -> Result<Self, RemoteStorageError> {
136 if update.remote_log_segment_id != self.remote_log_segment_id {
137 return Err(RemoteStorageError::InvalidArgument(
138 "update segment id does not match metadata segment id".into(),
139 ));
140 }
141 if !self.state.is_valid_transition(update.state) {
142 return Err(RemoteStorageError::InvalidSegmentTransition {
143 id: self.remote_log_segment_id.clone(),
144 from: self.state,
145 to: update.state,
146 });
147 }
148 let mut next = self.clone();
149 next.state = update.state;
150 next.event_timestamp_ms = update.event_timestamp_ms;
151 next.broker_id = update.broker_id;
152 if update.custom_metadata.is_some() {
153 next.custom_metadata.clone_from(&update.custom_metadata);
154 }
155 Ok(next)
156 }
157
158 #[must_use]
160 pub fn remote_log_segment_id(&self) -> &RemoteLogSegmentId { &self.remote_log_segment_id }
161
162 #[must_use]
164 pub fn start_offset(&self) -> i64 { self.start_offset }
165
166 #[must_use]
168 pub fn end_offset(&self) -> i64 { self.end_offset }
169
170 #[must_use]
172 pub fn max_timestamp_ms(&self) -> i64 { self.max_timestamp_ms }
173
174 #[must_use]
176 pub fn broker_id(&self) -> i32 { self.broker_id }
177
178 #[must_use]
180 pub fn event_timestamp_ms(&self) -> i64 { self.event_timestamp_ms }
181
182 #[must_use]
184 pub fn segment_size_in_bytes(&self) -> i32 { self.segment_size_in_bytes }
185
186 #[must_use]
189 pub fn custom_metadata(&self) -> Option<&CustomMetadata> { self.custom_metadata.as_ref() }
190
191 #[must_use]
193 pub fn state(&self) -> RemoteLogSegmentState { self.state }
194
195 #[must_use]
198 pub fn segment_leader_epochs(&self) -> &BTreeMap<LeaderEpoch, i64> {
199 &self.segment_leader_epochs
200 }
201
202 #[must_use]
204 pub fn epoch_bounds(&self, epoch: LeaderEpoch) -> (Option<i64>, Option<i64>) {
205 let start = self.segment_leader_epochs.get(&epoch).copied();
206 let next = self
207 .segment_leader_epochs
208 .iter()
209 .filter(|(candidate, _)| **candidate > epoch)
210 .map(|(_, start)| *start)
211 .min();
212 (start, next)
213 }
214
215 #[must_use]
218 pub fn with_custom_metadata(mut self, custom: CustomMetadata) -> Self {
219 self.custom_metadata = Some(custom);
220 self
221 }
222
223 #[must_use]
226 pub fn txn_index_empty(&self) -> bool { self.txn_index_empty }
227
228 #[must_use]
230 pub fn with_txn_index_empty(mut self, empty: bool) -> Self {
231 self.txn_index_empty = empty;
232 self
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct RemoteLogSegmentMetadataUpdate {
239 pub remote_log_segment_id: RemoteLogSegmentId,
241 pub event_timestamp_ms: i64,
243 pub custom_metadata: Option<CustomMetadata>,
245 pub state: RemoteLogSegmentState,
247 pub broker_id: i32,
249}
250
251#[cfg(test)]
252mod tests {
253 use {
254 super::*,
255 crate::metadata::{TopicIdPartition, test_topic_partition as tp},
256 assert2::{assert, check},
257 uuid::Uuid,
258 };
259
260 fn seg_id() -> RemoteLogSegmentId { RemoteLogSegmentId::new(tp(), Uuid::from_u128(99)) }
261
262 fn epochs() -> BTreeMap<LeaderEpoch, i64> {
263 maplit::btreemap! {LeaderEpoch(0) => 0}
264 }
265
266 fn metadata_range(
267 start: i64,
268 end: i64,
269 leader_epochs: BTreeMap<LeaderEpoch, i64>,
270 ) -> Result<RemoteLogSegmentMetadata, RemoteStorageError> {
271 RemoteLogSegmentMetadata::new(
272 seg_id(),
273 start,
274 end,
275 123,
276 1,
277 456,
278 RemoteLogSegmentDetails::new(
279 1024,
280 RemoteLogSegmentState::CopySegmentStarted,
281 leader_epochs,
282 ),
283 )
284 }
285
286 fn started_metadata() -> RemoteLogSegmentMetadata { metadata_range(0, 10, epochs()).unwrap() }
287
288 #[test]
289 fn accessors_return_constructed_values() {
290 let md = RemoteLogSegmentMetadata::new(
293 seg_id(),
294 0,
295 100,
296 777, 5,
298 888,
299 RemoteLogSegmentDetails::new(
300 4096, RemoteLogSegmentState::CopySegmentStarted,
302 epochs(),
303 ),
304 )
305 .unwrap();
306 assert!(md.max_timestamp_ms() == 777);
307 assert!(md.segment_size_in_bytes() == 4096);
308 }
309
310 #[test]
311 fn metadata_rejects_empty_leader_epochs() {
312 let err = metadata_range(0, 10, BTreeMap::new()).unwrap_err();
313 assert!(matches!(err, RemoteStorageError::InvalidArgument(_)));
314 }
315
316 #[test]
317 fn metadata_rejects_end_before_start() {
318 let err = metadata_range(10, 5, epochs()).unwrap_err();
319 assert!(matches!(err, RemoteStorageError::InvalidArgument(_)));
320 }
321
322 #[test]
323 fn with_update_advances_state_and_fields() {
324 let started = started_metadata();
325 let update = RemoteLogSegmentMetadataUpdate {
326 remote_log_segment_id: seg_id(),
327 event_timestamp_ms: 789,
328 custom_metadata: Some(CustomMetadata(vec![1, 2, 3])),
329 state: RemoteLogSegmentState::CopySegmentFinished,
330 broker_id: 2,
331 };
332 let finished = started.with_update(&update).unwrap();
333 check!(finished.state() == RemoteLogSegmentState::CopySegmentFinished);
334 check!(finished.event_timestamp_ms() == 789);
335 check!(finished.broker_id() == 2);
336 check!(finished.custom_metadata() == Some(&CustomMetadata(vec![1, 2, 3])));
337 check!(finished.start_offset() == 0);
339 check!(finished.end_offset() == 10);
340 }
341
342 #[test]
343 fn with_update_keeps_custom_metadata_when_update_omits_it() {
344 let started = started_metadata().with_custom_metadata(CustomMetadata(vec![9]));
345 let update = RemoteLogSegmentMetadataUpdate {
346 remote_log_segment_id: seg_id(),
347 event_timestamp_ms: 789,
348 custom_metadata: None,
349 state: RemoteLogSegmentState::CopySegmentFinished,
350 broker_id: 2,
351 };
352 let finished = started.with_update(&update).unwrap();
353 assert!(finished.custom_metadata() == Some(&CustomMetadata(vec![9])));
354 }
355
356 #[test]
357 fn with_update_rejects_invalid_transition() {
358 let started = started_metadata();
359 let update = RemoteLogSegmentMetadataUpdate {
360 remote_log_segment_id: seg_id(),
361 event_timestamp_ms: 789,
362 custom_metadata: None,
363 state: RemoteLogSegmentState::DeleteSegmentFinished,
364 broker_id: 2,
365 };
366 let err = started.with_update(&update).unwrap_err();
367 assert!(matches!(
368 err,
369 RemoteStorageError::InvalidSegmentTransition { .. }
370 ));
371 }
372
373 #[test]
374 fn with_update_rejects_mismatched_id() {
375 let started = started_metadata();
376 let other = RemoteLogSegmentId::new(tp(), Uuid::from_u128(1234));
377 let update = RemoteLogSegmentMetadataUpdate {
378 remote_log_segment_id: other,
379 event_timestamp_ms: 789,
380 custom_metadata: None,
381 state: RemoteLogSegmentState::CopySegmentFinished,
382 broker_id: 2,
383 };
384 let err = started.with_update(&update).unwrap_err();
385 assert!(matches!(err, RemoteStorageError::InvalidArgument(_)));
386 }
387
388 #[test]
389 fn txn_index_empty_defaults_false_and_is_settable() {
390 let md = RemoteLogSegmentMetadata::new(
391 RemoteLogSegmentId::new(
392 TopicIdPartition::new(Uuid::from_u128(1), "t", 0),
393 Uuid::from_u128(2),
394 ),
395 0,
396 9,
397 9,
398 1,
399 100,
400 RemoteLogSegmentDetails::new(
401 1024,
402 RemoteLogSegmentState::CopySegmentStarted,
403 maplit::btreemap! {LeaderEpoch(0) => 0},
404 ),
405 )
406 .unwrap();
407 assert!(!md.txn_index_empty());
408 let md = md.with_txn_index_empty(true);
409 assert!(md.txn_index_empty());
410 }
411}