1use std::collections::{HashMap, HashSet};
16use std::ops::Bound::{Excluded, Included};
17use std::ops::{Deref, DerefMut};
18use std::sync::Arc;
19use std::sync::atomic::Ordering;
20
21use bytes::BytesMut;
22use risingwave_hummock_sdk::compaction_group::hummock_version_ext::version_object_size_map;
23use risingwave_hummock_sdk::version::HummockVersion;
24use risingwave_hummock_sdk::{HummockObjectId, HummockVersionId, get_stale_object_ids};
25use risingwave_pb::hummock::hummock_version_checkpoint::{PbStaleObjects, StaleObjects};
26use risingwave_pb::hummock::{
27 CheckpointCompressionAlgorithm, PbHummockVersion, PbHummockVersionArchive,
28 PbHummockVersionCheckpoint, PbHummockVersionCheckpointEnvelope, PbVectorIndexObject,
29 PbVectorIndexObjectType,
30};
31use thiserror_ext::AsReport;
32use tracing::warn;
33
34use crate::hummock::HummockManager;
35use crate::hummock::error::Result;
36use crate::hummock::manager::versioning::Versioning;
37use crate::hummock::metrics_utils::{gc_stale_object_stats, trigger_gc_stat, trigger_split_stat};
38
39pub(crate) fn xxhash64_checksum(data: &[u8]) -> u64 {
42 use std::hash::Hasher;
43 let mut hasher = twox_hash::XxHash64::with_seed(0);
44 hasher.write(data);
45 hasher.finish()
46}
47
48#[derive(Default)]
49pub struct HummockVersionCheckpoint {
50 pub version: Arc<HummockVersion>,
51
52 pub stale_objects: HashMap<HummockVersionId, PbStaleObjects>,
58}
59
60impl HummockVersionCheckpoint {
61 pub fn from_protobuf(checkpoint: &PbHummockVersionCheckpoint) -> Self {
62 let version = checkpoint.version.as_ref().unwrap();
63 warn_if_legacy_table_change_logs_are_present(version);
64 Self {
65 version: Arc::new(HummockVersion::from_persisted_protobuf(version)),
66 stale_objects: checkpoint
67 .stale_objects
68 .iter()
69 .map(|(version_id, objects)| (*version_id, objects.clone()))
70 .collect(),
71 }
72 }
73
74 pub fn from_protobuf_owned(checkpoint: PbHummockVersionCheckpoint) -> Self {
77 let version = checkpoint.version.unwrap();
78 warn_if_legacy_table_change_logs_are_present(&version);
79 Self {
80 version: HummockVersion::from_persisted_protobuf_owned(version).into(),
81 stale_objects: checkpoint.stale_objects,
82 }
83 }
84
85 pub fn to_protobuf(&self) -> PbHummockVersionCheckpoint {
86 PbHummockVersionCheckpoint {
87 version: Some(PbHummockVersion::from(self.version.as_ref())),
88 stale_objects: self
89 .stale_objects
90 .iter()
91 .map(|(version_id, objects)| (*version_id, objects.clone()))
92 .collect(),
93 }
94 }
95}
96
97fn warn_if_legacy_table_change_logs_are_present(version: &PbHummockVersion) {
98 if !version.table_change_logs.is_empty() {
99 warn!(
100 version_id = ?version.id,
101 table_count = version.table_change_logs.len(),
102 "deprecated table change logs found in hummock version checkpoint; ignoring them"
103 );
104 }
105}
106
107fn decode_checkpoint_data(data: bytes::Bytes) -> Result<PbHummockVersionCheckpoint> {
123 use anyhow::Context;
124 use prost::Message;
125
126 let data_size = data.len();
127
128 if let Ok(envelope) = PbHummockVersionCheckpointEnvelope::decode(data.clone())
129 && let Some(expected) = envelope.checksum
130 {
131 let actual = xxhash64_checksum(&envelope.payload);
132 if actual != expected {
133 return Err(anyhow::anyhow!(
134 "checkpoint checksum mismatch: expected {:#x}, got {:#x}",
135 expected,
136 actual
137 )
138 .into());
139 }
140
141 let algo = CheckpointCompressionAlgorithm::try_from(envelope.compression_algorithm)
142 .with_context(|| {
143 format!(
144 "unknown checkpoint compression algorithm: {}",
145 envelope.compression_algorithm
146 )
147 })?;
148
149 let decompressed = decompress_payload(algo, &envelope.payload)?;
150 let ckpt = PbHummockVersionCheckpoint::decode(decompressed.as_ref())
151 .context("failed to decode checkpoint envelope payload")?;
152 if ckpt.version.is_none() {
153 return Err(anyhow::anyhow!("checkpoint missing required field `version`").into());
154 }
155
156 tracing::info!(
157 compression = ?algo,
158 compressed_size = envelope.payload.len(),
159 decompressed_size = decompressed.len(),
160 compression_ratio =
161 format!("{:.2}x", decompressed.len() as f64 / envelope.payload.len().max(1) as f64),
162 checksum = format!("{expected:#x}"),
163 "decoded compressed checkpoint"
164 );
165 return Ok(ckpt);
166 }
167
168 tracing::info!(
170 data_size,
171 "decoding checkpoint in legacy uncompressed format"
172 );
173 let ckpt =
174 PbHummockVersionCheckpoint::decode(data).context("failed to decode legacy checkpoint")?;
175 if ckpt.version.is_none() {
176 return Err(anyhow::anyhow!("legacy checkpoint missing required field `version`").into());
177 }
178 Ok(ckpt)
179}
180
181fn decompress_payload(
182 algo: CheckpointCompressionAlgorithm,
183 payload: &[u8],
184) -> Result<std::borrow::Cow<'_, [u8]>> {
185 use anyhow::Context;
186
187 match algo {
188 CheckpointCompressionAlgorithm::CheckpointCompressionUnspecified => Ok(payload.into()),
189 CheckpointCompressionAlgorithm::CheckpointCompressionZstd => {
190 Ok(zstd::stream::decode_all(payload)
191 .map(std::borrow::Cow::Owned)
192 .context("zstd decompression failed")?)
193 }
194 CheckpointCompressionAlgorithm::CheckpointCompressionLz4 => {
195 let mut decoder = lz4::Decoder::new(payload).context("lz4 decoder init failed")?;
196 let mut decompressed = Vec::new();
197 std::io::Read::read_to_end(&mut decoder, &mut decompressed)
198 .context("lz4 decompression failed")?;
199 Ok(decompressed.into())
200 }
201 }
202}
203
204pub(crate) fn compress_payload(
206 algo: risingwave_common::config::CheckpointCompression,
207 data: &[u8],
208) -> Result<Vec<u8>> {
209 use anyhow::Context;
210 use risingwave_common::config::CheckpointCompression;
211
212 match algo {
213 CheckpointCompression::None => Ok(data.to_vec()),
214 CheckpointCompression::Zstd => {
215 Ok(zstd::stream::encode_all(data, 3).context("zstd compression failed")?)
217 }
218 CheckpointCompression::Lz4 => {
219 let mut compressed = Vec::new();
220 let mut encoder = lz4::EncoderBuilder::new()
221 .level(4)
222 .build(&mut compressed)
223 .context("lz4 encoder init failed")?;
224 std::io::Write::write_all(&mut encoder, data)
225 .context("lz4 compression write failed")?;
226 let (_writer, result) = encoder.finish();
227 result.context("lz4 compression finish failed")?;
228 Ok(compressed)
229 }
230 }
231}
232
233async fn read_bytes_in_chunks<F, Fut>(
234 total_size: usize,
235 chunk_size: usize,
236 max_in_flight_chunks: usize,
237 mut read_range: F,
238) -> anyhow::Result<bytes::Bytes>
239where
240 F: FnMut(std::ops::Range<usize>) -> Fut,
241 Fut: std::future::Future<Output = anyhow::Result<bytes::Bytes>>,
242{
243 use anyhow::Context;
244 use futures::StreamExt;
245
246 let num_chunks = total_size.div_ceil(chunk_size);
247 let mut buf = BytesMut::with_capacity(total_size);
248
249 let mut chunk_stream = futures::stream::iter((0..total_size).step_by(chunk_size))
250 .enumerate()
251 .map(|(chunk_idx, offset)| {
252 let end = std::cmp::min(offset + chunk_size, total_size);
253 let range = offset..end;
254 let fut = read_range(range.clone());
255 async move {
256 fut.await.with_context(|| {
257 format!(
258 "read checkpoint chunk {}/{} range {}..{}",
259 chunk_idx + 1,
260 num_chunks,
261 range.start,
262 range.end
263 )
264 })
265 }
266 })
267 .buffered(max_in_flight_chunks);
268
269 while let Some(chunk) = chunk_stream.next().await {
270 let chunk = chunk?;
271 buf.extend_from_slice(&chunk);
272 }
273
274 Ok(buf.freeze())
275}
276
277impl HummockManager {
280 pub async fn try_read_checkpoint(&self) -> Result<Option<HummockVersionCheckpoint>> {
287 let object_metadata = match self
288 .object_store
289 .metadata(&self.version_checkpoint_path)
290 .await
291 {
292 Ok(metadata) => metadata,
293 Err(e) => {
294 if e.is_object_not_found_error() {
295 return Ok(None);
296 }
297 return Err(e.into());
298 }
299 };
300 let total_size = object_metadata.total_size;
301
302 let chunk_size = self.env.opts.checkpoint_read_chunk_size;
303 let max_in_flight_chunks = self.env.opts.checkpoint_read_max_in_flight_chunks;
304
305 let download_start = std::time::Instant::now();
306 let data = if total_size <= chunk_size {
307 self.object_store
308 .read(&self.version_checkpoint_path, 0..total_size)
309 .await?
310 } else {
311 let num_chunks = total_size.div_ceil(chunk_size);
312 let data = read_bytes_in_chunks(
313 total_size,
314 chunk_size,
315 max_in_flight_chunks,
316 |range| async {
317 Ok(self
318 .object_store
319 .read(&self.version_checkpoint_path, range)
320 .await?)
321 },
322 )
323 .await?;
324
325 tracing::info!(
326 total_size,
327 num_chunks,
328 chunk_size,
329 max_in_flight_chunks,
330 "chunked read complete"
331 );
332 data
333 };
334 let download_duration = download_start.elapsed();
335
336 let decode_start = std::time::Instant::now();
337 let ckpt = decode_checkpoint_data(data)?;
338 let decode_duration = decode_start.elapsed();
339
340 tracing::info!(
341 total_size,
342 download_ms = download_duration.as_millis() as u64,
343 decode_ms = decode_duration.as_millis() as u64,
344 "checkpoint read complete"
345 );
346
347 Ok(Some(HummockVersionCheckpoint::from_protobuf_owned(ckpt)))
348 }
349
350 pub(super) async fn write_checkpoint(
351 &self,
352 checkpoint: &HummockVersionCheckpoint,
353 ) -> Result<()> {
354 use prost::Message;
355 let raw_bytes = checkpoint.to_protobuf().encode_to_vec();
356 let raw_size = raw_bytes.len();
357
358 let compression = self.env.opts.checkpoint_compression_algorithm;
359 let compressed = compress_payload(compression, &raw_bytes)?;
360 let checksum = xxhash64_checksum(&compressed);
361
362 tracing::info!(
363 raw_size,
364 compressed_size = compressed.len(),
365 compression_ratio =
366 format!("{:.2}x", raw_size as f64 / compressed.len().max(1) as f64),
367 compression = ?compression,
368 checksum = format!("{:#x}", checksum),
369 "writing compressed checkpoint"
370 );
371
372 let envelope = PbHummockVersionCheckpointEnvelope {
373 compression_algorithm: compression as i32,
374 payload: compressed,
375 checksum: Some(checksum),
376 };
377
378 let buf = envelope.encode_to_vec();
379 self.object_store
380 .upload(&self.version_checkpoint_path, buf.into())
381 .await?;
382 Ok(())
383 }
384
385 pub(super) async fn write_version_archive(
386 &self,
387 archive: &PbHummockVersionArchive,
388 ) -> Result<()> {
389 use prost::Message;
390 let buf = archive.encode_to_vec();
391 let archive_path = format!(
392 "{}/{}",
393 self.version_archive_dir,
394 archive.version.as_ref().unwrap().id
395 );
396 self.object_store.upload(&archive_path, buf.into()).await?;
397 Ok(())
398 }
399
400 pub async fn create_version_checkpoint(&self, min_delta_log_num: u64) -> Result<u64> {
405 let timer = self.metrics.version_checkpoint_latency.start_timer();
406 let (
408 current_version,
409 old_checkpoint_version,
410 mut stale_objects,
411 version_deltas,
412 current_table_change_log,
413 new_checkpoint_id,
414 old_checkpoint_id,
415 ) = {
416 let versioning_guard = self
417 .versioning
418 .read_with_process_name("version_checkpoint_build")
419 .await;
420 let versioning: &Versioning = versioning_guard.deref();
421 let current_version = versioning.current_version.clone();
422 let old_checkpoint: &HummockVersionCheckpoint = &versioning.checkpoint;
423 let new_checkpoint_id = current_version.id;
424 let old_checkpoint_id = old_checkpoint.version.id;
425 if new_checkpoint_id < old_checkpoint_id + min_delta_log_num {
426 return Ok(0);
427 }
428 if cfg!(test) && new_checkpoint_id == old_checkpoint_id {
429 drop(versioning_guard);
430 let versioning = self
431 .versioning
432 .read_with_process_name("version_checkpoint_build")
433 .await;
434 let context_info = self
435 .context_info
436 .read_with_process_name("version_checkpoint_build")
437 .await;
438 let min_pinned_version_id = context_info.min_pinned_version_id();
439 let stale_object_stats = gc_stale_object_stats(
440 &versioning.checkpoint.stale_objects,
441 min_pinned_version_id,
442 );
443 trigger_gc_stat(
444 &self.metrics,
445 versioning.checkpoint.version.as_ref(),
446 &versioning.table_change_log,
447 stale_object_stats,
448 );
449 return Ok(0);
450 }
451 let old_checkpoint_version = old_checkpoint.version.clone();
452 let version_deltas = versioning
453 .hummock_version_deltas
454 .range((Excluded(old_checkpoint_id), Included(new_checkpoint_id)))
455 .map(|(_, version_delta)| version_delta.clone())
456 .collect::<Vec<_>>();
457 (
458 current_version,
459 old_checkpoint_version,
460 old_checkpoint.stale_objects.clone(),
461 version_deltas,
462 versioning.table_change_log.clone(),
463 new_checkpoint_id,
464 old_checkpoint_id,
465 )
466 };
467 assert!(new_checkpoint_id > old_checkpoint_id);
468 let mut archive: Option<PbHummockVersionArchive> = None;
469 let mut object_sizes = version_object_size_map(old_checkpoint_version.as_ref());
471 let mut versions_object_ids: HashSet<_> = old_checkpoint_version.get_object_ids().collect();
473 for version_delta in &version_deltas {
474 match HummockObjectId::Sstable(0.into()) {
478 HummockObjectId::Sstable(_) => {}
479 HummockObjectId::VectorFile(_) => {}
480 HummockObjectId::HnswGraphFile(_) => {}
481 };
482 for (object_id, file_size) in version_delta
483 .newly_added_sst_infos()
484 .map(|sst| (HummockObjectId::Sstable(sst.object_id), sst.file_size))
485 .chain(
486 version_delta
487 .vector_index_delta
488 .values()
489 .flat_map(|delta| delta.newly_added_objects()),
490 )
491 {
492 object_sizes.insert(object_id, file_size);
493 versions_object_ids.insert(object_id);
494 }
495 }
496
497 let current_version_object_ids = current_version
499 .get_object_ids()
500 .chain(
501 current_table_change_log
502 .values()
503 .flat_map(|l| l.get_object_ids()),
504 )
505 .collect();
506 let removed_object_ids = &versions_object_ids - ¤t_version_object_ids;
507 let total_file_size = removed_object_ids
508 .iter()
509 .map(|t| {
510 object_sizes.get(t).copied().unwrap_or_else(|| {
511 warn!(object_id = ?t, "unable to get size of removed object id");
512 0
513 })
514 })
515 .sum::<u64>();
516 stale_objects.insert(current_version.id, {
517 let mut sst_ids = vec![];
518 let mut vector_files = vec![];
519 for object_id in removed_object_ids {
520 match object_id {
521 HummockObjectId::Sstable(sst_id) => sst_ids.push(sst_id),
522 HummockObjectId::VectorFile(vector_file_id) => {
523 vector_files.push(PbVectorIndexObject {
524 id: vector_file_id.as_raw(),
525 object_type: PbVectorIndexObjectType::VectorIndexObjectVector as _,
526 })
527 }
528 HummockObjectId::HnswGraphFile(graph_file_id) => {
529 vector_files.push(PbVectorIndexObject {
530 id: graph_file_id.as_raw(),
531 object_type: PbVectorIndexObjectType::VectorIndexObjectHnswGraph as _,
532 });
533 }
534 }
535 }
536 StaleObjects {
537 id: sst_ids,
538 total_file_size,
539 vector_files,
540 }
541 });
542 if self.env.opts.enable_hummock_data_archive {
543 archive = Some(PbHummockVersionArchive {
544 version: Some(PbHummockVersion::from(old_checkpoint_version.as_ref())),
545 version_deltas: version_deltas
546 .iter()
547 .map(|version_delta| version_delta.into())
548 .collect(),
549 });
550 }
551 let min_pinned_version_id = self
552 .context_info
553 .read_with_process_name("version_checkpoint_build")
554 .await
555 .min_pinned_version_id();
556 let may_delete_object = stale_objects
557 .iter()
558 .filter_map(|(version_id, object_ids)| {
559 if *version_id >= min_pinned_version_id {
560 return None;
561 }
562 Some(get_stale_object_ids(object_ids))
563 })
564 .flatten();
565 self.gc_manager.add_may_delete_object_ids(may_delete_object);
566 stale_objects.retain(|version_id, _| *version_id >= min_pinned_version_id);
567 let new_checkpoint = HummockVersionCheckpoint {
568 version: current_version.clone(),
569 stale_objects,
570 };
571 self.write_checkpoint(&new_checkpoint).await?;
573 if let Some(archive) = archive
574 && let Err(e) = self.write_version_archive(&archive).await
575 {
576 tracing::warn!(
577 error = %e.as_report(),
578 "failed to write version archive {}",
579 archive.version.as_ref().unwrap().id
580 );
581 }
582 let min_pinned_version_id = self
583 .context_info
584 .read_with_process_name("version_checkpoint_build")
585 .await
586 .min_pinned_version_id();
587 let stale_object_stats =
588 gc_stale_object_stats(&new_checkpoint.stale_objects, min_pinned_version_id);
589 let current_version_for_metrics = {
591 let mut versioning_guard = self
592 .versioning
593 .write_with_process_name("version_checkpoint_install")
594 .await;
595 let versioning = versioning_guard.deref_mut();
596 assert!(new_checkpoint.version.id > versioning.checkpoint.version.id);
597 versioning.checkpoint = new_checkpoint;
598 versioning.current_version.clone()
599 };
600 trigger_gc_stat(
601 &self.metrics,
602 current_version.as_ref(),
603 ¤t_table_change_log,
604 stale_object_stats,
605 );
606 trigger_split_stat(&self.metrics, current_version_for_metrics.as_ref());
607 timer.observe_duration();
608 self.metrics
609 .checkpoint_version_id
610 .set(new_checkpoint_id.as_i64_id());
611
612 Ok(new_checkpoint_id - old_checkpoint_id)
613 }
614
615 pub fn pause_version_checkpoint(&self) {
616 self.pause_version_checkpoint.store(true, Ordering::Relaxed);
617 tracing::info!("hummock version checkpoint is paused.");
618 }
619
620 pub fn resume_version_checkpoint(&self) {
621 self.pause_version_checkpoint
622 .store(false, Ordering::Relaxed);
623 tracing::info!("hummock version checkpoint is resumed.");
624 }
625
626 pub fn is_version_checkpoint_paused(&self) -> bool {
627 self.pause_version_checkpoint.load(Ordering::Relaxed)
628 }
629
630 pub async fn get_checkpoint_version(&self) -> Arc<HummockVersion> {
631 let versioning_guard = self
632 .versioning
633 .read_with_process_name("get_checkpoint_version")
634 .await;
635 versioning_guard.checkpoint.version.clone()
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use bytes::Bytes;
642 use prost::Message;
643 use risingwave_common::config::CheckpointCompression;
644 use risingwave_pb::hummock::hummock_version_checkpoint::StaleObjects;
645 use risingwave_pb::hummock::{
646 PbHummockVersion, PbHummockVersionCheckpoint, PbHummockVersionCheckpointEnvelope,
647 PbTableChangeLog,
648 };
649
650 use super::{
651 HummockVersionCheckpoint, compress_payload, decode_checkpoint_data, read_bytes_in_chunks,
652 xxhash64_checksum,
653 };
654
655 #[expect(deprecated)]
656 fn make_version(id: u64) -> PbHummockVersion {
657 PbHummockVersion {
658 id: id.into(),
659 levels: Default::default(),
660 max_committed_epoch: 0,
661 table_watermarks: Default::default(),
662 table_change_logs: Default::default(),
663 state_table_info: Default::default(),
664 vector_indexes: Default::default(),
665 }
666 }
667
668 fn make_checkpoint(version_id: u64) -> PbHummockVersionCheckpoint {
669 let stale = StaleObjects {
670 id: vec![1u64.into(), 2u64.into(), 3u64.into()],
671 total_file_size: 123,
672 vector_files: vec![],
673 };
674
675 PbHummockVersionCheckpoint {
676 version: Some(make_version(version_id)),
677 stale_objects: [(1u64.into(), stale)].into_iter().collect(),
678 }
679 }
680
681 #[test]
682 fn deprecated_table_change_logs_are_ignored() {
683 let mut checkpoint = make_checkpoint(42);
684 checkpoint
685 .version
686 .as_mut()
687 .unwrap()
688 .table_change_logs
689 .insert(1.into(), PbTableChangeLog::default());
690
691 let checkpoint = HummockVersionCheckpoint::from_protobuf(&checkpoint);
692 assert!(
693 checkpoint
694 .to_protobuf()
695 .version
696 .unwrap()
697 .table_change_logs
698 .is_empty()
699 );
700 }
701
702 fn make_envelope_bytes(
703 checkpoint: &PbHummockVersionCheckpoint,
704 compression: CheckpointCompression,
705 checksum: Option<u64>,
706 ) -> Bytes {
707 let raw = checkpoint.encode_to_vec();
708 let payload = compress_payload(compression, &raw)
709 .expect("compress checkpoint payload should succeed");
710 let checksum = checksum.unwrap_or_else(|| xxhash64_checksum(&payload));
711 let envelope = PbHummockVersionCheckpointEnvelope {
712 compression_algorithm: compression as i32,
713 payload,
714 checksum: Some(checksum),
715 };
716 Bytes::from(envelope.encode_to_vec())
717 }
718
719 #[test]
720 fn decode_checkpoint_data_falls_back_to_legacy_format() {
721 let checkpoint = make_checkpoint(42);
722 let raw = Bytes::from(checkpoint.encode_to_vec());
723 let decoded = decode_checkpoint_data(raw).expect("legacy checkpoint should decode");
724 assert_eq!(decoded, checkpoint);
725 }
726
727 #[test]
728 fn decode_checkpoint_data_roundtrips_envelope_with_checksum() {
729 let checkpoint = make_checkpoint(42);
730 for compression in [
731 CheckpointCompression::None,
732 CheckpointCompression::Zstd,
733 CheckpointCompression::Lz4,
734 ] {
735 let data = make_envelope_bytes(&checkpoint, compression, None);
736 let decoded = decode_checkpoint_data(data).expect("envelope checkpoint should decode");
737 assert_eq!(decoded, checkpoint);
738 }
739 }
740
741 #[test]
742 fn decode_checkpoint_data_returns_error_on_checksum_mismatch() {
743 let checkpoint = make_checkpoint(42);
744 let raw = checkpoint.encode_to_vec();
745 let mut payload = compress_payload(CheckpointCompression::Zstd, &raw)
746 .expect("compress checkpoint payload should succeed");
747 let expected = xxhash64_checksum(&payload);
748 payload[0] ^= 0x01;
749 let envelope = PbHummockVersionCheckpointEnvelope {
750 compression_algorithm: CheckpointCompression::Zstd as i32,
751 payload,
752 checksum: Some(expected),
753 };
754 let data = Bytes::from(envelope.encode_to_vec());
755 let err = decode_checkpoint_data(data).expect_err("checksum mismatch should error");
756 assert!(err.to_string().contains("checksum mismatch"), "{err:?}");
757 }
758
759 #[test]
760 fn decode_checkpoint_data_returns_error_on_unknown_compression_algorithm() {
761 let checkpoint = make_checkpoint(42);
762 let payload = checkpoint.encode_to_vec();
763 let checksum = xxhash64_checksum(&payload);
764 let envelope = PbHummockVersionCheckpointEnvelope {
765 compression_algorithm: 123,
766 payload,
767 checksum: Some(checksum),
768 };
769 let data = Bytes::from(envelope.encode_to_vec());
770 let err =
771 decode_checkpoint_data(data).expect_err("unknown compression algorithm should error");
772 assert!(
773 err.to_string()
774 .contains("unknown checkpoint compression algorithm"),
775 "{err:?}"
776 );
777 }
778
779 #[test]
780 fn decode_checkpoint_data_returns_error_on_legacy_missing_version() {
781 let checkpoint = PbHummockVersionCheckpoint {
782 version: None,
783 stale_objects: Default::default(),
784 };
785 let data = Bytes::from(checkpoint.encode_to_vec());
786 let err = decode_checkpoint_data(data).expect_err("missing version should error");
787 assert!(
788 err.to_string()
789 .contains("legacy checkpoint missing required field `version`"),
790 "{err:?}"
791 );
792 }
793
794 #[test]
795 fn decode_checkpoint_data_returns_error_on_corrupt_envelope_payload() {
796 let garbage = b"not a valid protobuf";
797 let checksum = xxhash64_checksum(garbage);
798 let envelope = PbHummockVersionCheckpointEnvelope {
799 compression_algorithm: CheckpointCompression::None as i32,
800 payload: garbage.to_vec(),
801 checksum: Some(checksum),
802 };
803 let data = Bytes::from(envelope.encode_to_vec());
804 let err = decode_checkpoint_data(data).expect_err("corrupt envelope payload should error");
805 assert!(
806 err.to_string()
807 .contains("failed to decode checkpoint envelope payload"),
808 "{err:?}"
809 );
810 }
811
812 #[test]
813 fn decode_checkpoint_data_returns_error_on_empty_input() {
814 let err = decode_checkpoint_data(Bytes::new()).expect_err("empty checkpoint should fail");
815 assert!(
816 err.to_string()
817 .contains("legacy checkpoint missing required field `version`"),
818 "{err:?}"
819 );
820 }
821
822 #[test]
823 fn decode_checkpoint_data_returns_error_on_envelope_missing_version() {
824 let checkpoint = PbHummockVersionCheckpoint {
825 version: None,
826 stale_objects: Default::default(),
827 };
828 let raw = checkpoint.encode_to_vec();
829 let checksum = xxhash64_checksum(&raw);
830 let envelope = PbHummockVersionCheckpointEnvelope {
831 compression_algorithm: CheckpointCompression::None as i32,
832 payload: raw,
833 checksum: Some(checksum),
834 };
835 let data = Bytes::from(envelope.encode_to_vec());
836 let err =
837 decode_checkpoint_data(data).expect_err("envelope with missing version should error");
838 assert!(
839 err.to_string()
840 .contains("checkpoint missing required field `version`"),
841 "{err:?}"
842 );
843 }
844
845 #[tokio::test]
846 async fn read_bytes_in_chunks_respects_concurrency_limit_and_reassembles() {
847 use std::sync::Arc;
848 use std::sync::atomic::{AtomicUsize, Ordering};
849
850 use tokio::time::{Duration, sleep};
851
852 let total_size = 100usize;
853 let chunk_size = 10usize;
854 let max_in_flight = 3usize;
855
856 let data: Arc<Vec<u8>> = Arc::new((0..total_size).map(|i| (i % 256) as u8).collect());
857 let in_flight = Arc::new(AtomicUsize::new(0));
858 let max_seen = Arc::new(AtomicUsize::new(0));
859
860 let out = read_bytes_in_chunks(total_size, chunk_size, max_in_flight, {
861 let data = data.clone();
862 let in_flight = in_flight.clone();
863 let max_seen = max_seen.clone();
864 move |range: std::ops::Range<usize>| {
865 let data = data.clone();
866 let in_flight = in_flight.clone();
867 let max_seen = max_seen.clone();
868 async move {
869 let cur = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
870 max_seen.fetch_max(cur, Ordering::SeqCst);
871
872 sleep(Duration::from_millis(30)).await;
876
877 let bytes = Bytes::copy_from_slice(&data[range]);
878 in_flight.fetch_sub(1, Ordering::SeqCst);
879 Ok(bytes)
880 }
881 }
882 })
883 .await
884 .expect("chunked read should succeed");
885
886 assert_eq!(out.as_ref(), data.as_slice());
887 let max_seen = max_seen.load(Ordering::SeqCst);
888 assert!(max_seen <= max_in_flight, "max_seen={max_seen}");
889 assert!(
890 max_seen > 1,
891 "expected some concurrency, max_seen={max_seen}"
892 );
893 }
894
895 #[tokio::test]
896 async fn read_bytes_in_chunks_adds_range_context_on_error() {
897 let total_size = 30usize;
898 let chunk_size = 10usize;
899 let max_in_flight = 2usize;
900
901 let err = read_bytes_in_chunks(total_size, chunk_size, max_in_flight, |range| async move {
902 if range.start == 10 {
903 anyhow::bail!("boom");
904 }
905 Ok(Bytes::copy_from_slice(&vec![0u8; range.len()]))
906 })
907 .await
908 .expect_err("should fail");
909
910 let msg = err.to_string();
911 assert!(
912 msg.contains("read checkpoint chunk 2/3 range 10..20"),
913 "unexpected error message: {msg}"
914 );
915 let msg_with_chain = format!("{err:#}");
916 assert!(
917 msg_with_chain.contains("boom"),
918 "unexpected error message: {msg_with_chain}"
919 );
920 }
921}