1use super::{Config, Error, Identifier};
2use crate::{
3 Context,
4 journal::segmented::oversized::{
5 Config as OversizedConfig, Oversized, Record as OversizedRecord,
6 },
7};
8use commonware_codec::{
9 Buf, CodecShared, Encode as _, FixedArray, FixedSize, Read, ReadExt, Write as CodecWrite,
10};
11use commonware_cryptography::{Crc32, Hasher, crc32};
12use commonware_runtime::{
13 Blob, BufMut, IoBuf, IoBufMut, ReadOptions, WriteOptions, buffer,
14 telemetry::metrics::{Counter, MetricsExt as _},
15};
16use commonware_utils::{Array, Span};
17use futures::future::try_join;
18use std::{cmp::Ordering, collections::BTreeSet, num::NonZeroUsize, ops::Deref, sync::Arc};
19use tracing::debug;
20
21const RESIZE_THRESHOLD: u64 = 50;
24
25#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, FixedArray)]
30#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
31#[repr(transparent)]
32pub struct Cursor([u8; u64::SIZE + u64::SIZE + u32::SIZE]);
33
34impl Cursor {
35 fn new(section: u64, offset: u64, size: u32) -> Self {
37 let mut buf = [0u8; u64::SIZE + u64::SIZE + u32::SIZE];
38 buf[..u64::SIZE].copy_from_slice(§ion.to_be_bytes());
39 buf[u64::SIZE..u64::SIZE + u64::SIZE].copy_from_slice(&offset.to_be_bytes());
40 buf[u64::SIZE + u64::SIZE..].copy_from_slice(&size.to_be_bytes());
41 Self(buf)
42 }
43
44 fn section(&self) -> u64 {
46 u64::from_be_bytes(self.0[..u64::SIZE].try_into().unwrap())
47 }
48
49 fn offset(&self) -> u64 {
51 u64::from_be_bytes(self.0[u64::SIZE..u64::SIZE + u64::SIZE].try_into().unwrap())
52 }
53
54 fn size(&self) -> u32 {
56 u32::from_be_bytes(self.0[u64::SIZE + u64::SIZE..].try_into().unwrap())
57 }
58}
59
60impl Read for Cursor {
61 type Cfg = ();
62
63 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
64 <[u8; u64::SIZE + u64::SIZE + u32::SIZE]>::read(buf).map(Self)
65 }
66}
67
68impl CodecWrite for Cursor {
69 fn write(&self, buf: &mut impl BufMut) {
70 self.0.write(buf);
71 }
72}
73
74impl FixedSize for Cursor {
75 const SIZE: usize = u64::SIZE + u64::SIZE + u32::SIZE;
76}
77
78impl Span for Cursor {}
79
80impl Array for Cursor {}
81
82impl Deref for Cursor {
83 type Target = [u8];
84 fn deref(&self) -> &Self::Target {
85 &self.0
86 }
87}
88
89impl AsRef<[u8]> for Cursor {
90 fn as_ref(&self) -> &[u8] {
91 &self.0
92 }
93}
94
95impl std::fmt::Debug for Cursor {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(
98 f,
99 "Cursor(section={}, offset={}, size={})",
100 self.section(),
101 self.offset(),
102 self.size()
103 )
104 }
105}
106
107impl std::fmt::Display for Cursor {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(
110 f,
111 "Cursor(section={}, offset={}, size={})",
112 self.section(),
113 self.offset(),
114 self.size()
115 )
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
124#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
125pub struct Checkpoint {
126 epoch: u64,
128 section: u64,
130 oversized_size: u64,
132 table_size: u32,
134}
135
136impl Checkpoint {
137 const fn init(table_size: u32) -> Self {
139 Self {
140 table_size,
141 epoch: 0,
142 section: 0,
143 oversized_size: 0,
144 }
145 }
146
147 const fn is_empty(&self) -> bool {
149 self.epoch == 0 && self.section == 0 && self.oversized_size == 0 && self.table_size == 0
150 }
151}
152
153impl Read for Checkpoint {
154 type Cfg = ();
155 fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, commonware_codec::Error> {
156 let epoch = u64::read(buf)?;
157 let section = u64::read(buf)?;
158 let oversized_size = u64::read(buf)?;
159 let table_size = u32::read(buf)?;
160 Ok(Self {
161 epoch,
162 section,
163 oversized_size,
164 table_size,
165 })
166 }
167}
168
169impl CodecWrite for Checkpoint {
170 fn write(&self, buf: &mut impl BufMut) {
171 self.epoch.write(buf);
172 self.section.write(buf);
173 self.oversized_size.write(buf);
174 self.table_size.write(buf);
175 }
176}
177
178impl FixedSize for Checkpoint {
179 const SIZE: usize = u64::SIZE + u64::SIZE + u64::SIZE + u32::SIZE;
180}
181
182const TABLE_BLOB_NAME: &[u8] = b"table";
184
185#[derive(Debug, Clone, PartialEq)]
187#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
188struct Entry {
189 epoch: u64,
191 section: u64,
193 position: u64,
195 added: u8,
197 crc: u32,
199}
200
201impl Entry {
202 const FULL_SIZE: usize = Self::SIZE * 2;
204
205 fn compute_crc(epoch: u64, section: u64, position: u64, added: u8) -> u32 {
207 Crc32::hash(&[
208 &epoch.to_be_bytes(),
209 §ion.to_be_bytes(),
210 &position.to_be_bytes(),
211 &added.to_be_bytes(),
212 ])
213 .as_u32()
214 }
215
216 fn new(epoch: u64, section: u64, position: u64, added: u8) -> Self {
218 Self {
219 epoch,
220 section,
221 position,
222 added,
223 crc: Self::compute_crc(epoch, section, position, added),
224 }
225 }
226
227 const fn new_empty() -> Self {
229 Self {
230 epoch: 0,
231 section: 0,
232 position: 0,
233 added: 0,
234 crc: 0,
235 }
236 }
237
238 const fn is_empty(&self) -> bool {
240 self.epoch == 0
241 && self.section == 0
242 && self.position == 0
243 && self.added == 0
244 && self.crc == 0
245 }
246
247 fn is_valid(&self) -> bool {
251 Self::compute_crc(self.epoch, self.section, self.position, self.added) == self.crc
252 }
253}
254
255impl FixedSize for Entry {
256 const SIZE: usize = u64::SIZE + u64::SIZE + u64::SIZE + u8::SIZE + crc32::Digest::SIZE;
257}
258
259impl CodecWrite for Entry {
260 fn write(&self, buf: &mut impl BufMut) {
261 self.epoch.write(buf);
262 self.section.write(buf);
263 self.position.write(buf);
264 self.added.write(buf);
265 self.crc.write(buf);
266 }
267}
268
269impl Read for Entry {
270 type Cfg = ();
271 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
272 let epoch = u64::read(buf)?;
273 let section = u64::read(buf)?;
274 let position = u64::read(buf)?;
275 let added = u8::read(buf)?;
276 let crc = u32::read(buf)?;
277
278 Ok(Self {
279 epoch,
280 section,
281 position,
282 added,
283 crc,
284 })
285 }
286}
287
288const NO_NEXT_SECTION: u64 = u64::MAX;
290const NO_NEXT_POSITION: u64 = u64::MAX;
291
292#[derive(Debug, Clone, PartialEq)]
300struct Record<K: Array> {
301 key: K,
303 next_section: u64,
306 next_position: u64,
307 value_offset: u64,
309 value_size: u32,
311}
312
313impl<K: Array> Record<K> {
314 fn new(key: K, next: Option<(u64, u64)>, value_offset: u64, value_size: u32) -> Self {
316 let (next_section, next_position) = next.unwrap_or((NO_NEXT_SECTION, NO_NEXT_POSITION));
317 Self {
318 key,
319 next_section,
320 next_position,
321 value_offset,
322 value_size,
323 }
324 }
325
326 const fn next(&self) -> Option<(u64, u64)> {
328 if self.next_section == NO_NEXT_SECTION && self.next_position == NO_NEXT_POSITION {
329 None
330 } else {
331 Some((self.next_section, self.next_position))
332 }
333 }
334}
335
336impl<K: Array> CodecWrite for Record<K> {
337 fn write(&self, buf: &mut impl BufMut) {
338 self.key.write(buf);
339 self.next_section.write(buf);
340 self.next_position.write(buf);
341 self.value_offset.write(buf);
342 self.value_size.write(buf);
343 }
344}
345
346impl<K: Array> Read for Record<K> {
347 type Cfg = ();
348 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
349 let key = K::read(buf)?;
350 let next_section = u64::read(buf)?;
351 let next_position = u64::read(buf)?;
352 let value_offset = u64::read(buf)?;
353 let value_size = u32::read(buf)?;
354
355 Ok(Self {
356 key,
357 next_section,
358 next_position,
359 value_offset,
360 value_size,
361 })
362 }
363}
364
365impl<K: Array> FixedSize for Record<K> {
366 const SIZE: usize = K::SIZE + u64::SIZE + u64::SIZE + u64::SIZE + u32::SIZE;
368}
369
370impl<K: Array> OversizedRecord for Record<K> {
371 fn value_location(&self) -> (u64, u32) {
372 (self.value_offset, self.value_size)
373 }
374
375 fn with_location(mut self, offset: u64, size: u32) -> Self {
376 self.value_offset = offset;
377 self.value_size = size;
378 self
379 }
380}
381
382#[cfg(feature = "arbitrary")]
383impl<K: Array> arbitrary::Arbitrary<'_> for Record<K>
384where
385 K: for<'a> arbitrary::Arbitrary<'a>,
386{
387 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
388 Ok(Self {
389 key: K::arbitrary(u)?,
390 next_section: u64::arbitrary(u)?,
391 next_position: u64::arbitrary(u)?,
392 value_offset: u64::arbitrary(u)?,
393 value_size: u32::arbitrary(u)?,
394 })
395 }
396}
397
398struct Inner<E: Context, K: Array, V: CodecShared> {
400 context: E,
402
403 table_partition: String,
405 table_size: u32,
406 table_resize_threshold: u64,
407 table_resize_frequency: u8,
408 table_resize_chunk_size: u32,
409
410 table: E::Blob,
412
413 oversized: Oversized<E, Record<K>, V>,
415
416 blob_target_size: u64,
418
419 current_section: u64,
421 current_value_size: u64,
423 next_epoch: u64,
424
425 modified_sections: BTreeSet<u64>,
427 resizable: u32,
428 resize_progress: Option<u32>,
429
430 puts: Counter,
432 gets: Counter,
433 has: Counter,
434 unnecessary_reads: Counter,
435 unnecessary_writes: Counter,
436 resizes: Counter,
437}
438
439impl<E: Context, K: Array, V: CodecShared> Inner<E, K, V> {
440 #[inline]
442 const fn table_offset(table_index: u32) -> u64 {
443 table_index as u64 * Entry::FULL_SIZE as u64
444 }
445
446 fn parse_entries(mut buf: impl Buf) -> Result<(Entry, Entry), Error> {
448 let entry1 = Entry::read(&mut buf)?;
449 let entry2 = Entry::read(&mut buf)?;
450 Ok((entry1, entry2))
451 }
452
453 async fn read_table(blob: &E::Blob, table_index: u32) -> Result<(Entry, Entry), Error> {
455 let offset = Self::table_offset(table_index);
456 let read_buf = blob
457 .read_at(offset, Entry::FULL_SIZE, ReadOptions::default())
458 .await?;
459
460 Self::parse_entries(read_buf.freeze())
461 }
462
463 async fn recover_entry(
467 blob: &E::Blob,
468 entry: &mut Entry,
469 entry_offset: u64,
470 max_valid_epoch: u64,
471 ) -> Result<bool, Error> {
472 if entry.is_empty() || (entry.is_valid() && entry.epoch <= max_valid_epoch) {
473 return Ok(false);
474 }
475 debug!(
476 valid_epoch = max_valid_epoch,
477 entry_epoch = entry.epoch,
478 "found invalid table entry"
479 );
480 *entry = Entry::new_empty();
481 let zero_buf = IoBuf::from(&[0u8; Entry::SIZE]);
482 blob.write_at(entry_offset, zero_buf, WriteOptions::default())
483 .await?;
484 Ok(true)
485 }
486
487 async fn recover_table(
493 blob: &Arc<E::Blob>,
494 table_size: u32,
495 table_resize_frequency: u8,
496 max_valid_epoch: u64,
497 table_replay_buffer: NonZeroUsize,
498 ) -> Result<(bool, u32), Error> {
499 let blob_size = Self::table_offset(table_size);
501 let mut reader = buffer::Read::new(Arc::clone(blob), blob_size, table_replay_buffer);
502
503 let mut modified = false;
505 let mut resizable = 0u32;
506 for table_index in 0..table_size {
507 let offset = Self::table_offset(table_index);
508
509 let entry_buf = reader.read(Entry::FULL_SIZE).await?;
511 let (mut entry1, mut entry2) = Self::parse_entries(entry_buf)?;
512
513 let entry1_cleared =
515 Self::recover_entry(blob, &mut entry1, offset, max_valid_epoch).await?;
516 let entry2_cleared = Self::recover_entry(
517 blob,
518 &mut entry2,
519 offset + Entry::SIZE as u64,
520 max_valid_epoch,
521 )
522 .await?;
523 modified |= entry1_cleared || entry2_cleared;
524
525 if let Some((_, _, added)) = Self::read_latest_entry(&entry1, &entry2)
527 && added >= table_resize_frequency
528 {
529 resizable += 1;
530 }
531 }
532
533 Ok((modified, resizable))
534 }
535
536 const fn compute_write_offset(entry1: &Entry, entry2: &Entry, epoch: u64) -> u64 {
538 if !entry1.is_empty() && entry1.epoch == epoch {
540 return 0;
541 }
542 if !entry2.is_empty() && entry2.epoch == epoch {
543 return Entry::SIZE as u64;
544 }
545
546 match (entry1.is_empty(), entry2.is_empty()) {
548 (true, _) => 0, (_, true) => Entry::SIZE as u64, (false, false) => {
551 if entry1.epoch < entry2.epoch {
552 0
553 } else {
554 Entry::SIZE as u64
555 }
556 }
557 }
558 }
559
560 fn read_latest_entry(entry1: &Entry, entry2: &Entry) -> Option<(u64, u64, u8)> {
562 match (
563 !entry1.is_empty() && entry1.is_valid(),
564 !entry2.is_empty() && entry2.is_valid(),
565 ) {
566 (true, true) => match entry1.epoch.cmp(&entry2.epoch) {
567 Ordering::Greater => Some((entry1.section, entry1.position, entry1.added)),
568 Ordering::Less => Some((entry2.section, entry2.position, entry2.added)),
569 Ordering::Equal => {
570 unreachable!("two valid entries with the same epoch")
571 }
572 },
573 (true, false) => Some((entry1.section, entry1.position, entry1.added)),
574 (false, true) => Some((entry2.section, entry2.position, entry2.added)),
575 (false, false) => None,
576 }
577 }
578
579 async fn update_head(
581 table: &E::Blob,
582 table_index: u32,
583 entry1: &Entry,
584 entry2: &Entry,
585 update: Entry,
586 ) -> Result<(), Error> {
587 let table_offset = Self::table_offset(table_index);
589
590 let start = Self::compute_write_offset(entry1, entry2, update.epoch);
592
593 table
595 .write_at(
596 table_offset + start,
597 update.encode(),
598 WriteOptions::default(),
599 )
600 .await
601 .map_err(Error::Runtime)
602 }
603
604 async fn init_table(blob: &E::Blob, table_size: u32) -> Result<(), Error> {
606 let table_len = Self::table_offset(table_size);
607 blob.resize(table_len).await?;
608 blob.sync().await?;
609 Ok(())
610 }
611
612 async fn init(
614 context: E,
615 config: Config<V::Cfg>,
616 checkpoint: Option<Checkpoint>,
617 ) -> Result<Self, Error> {
618 assert!(
620 config.table_initial_size > 0 && config.table_initial_size.is_power_of_two(),
621 "table_initial_size must be a power of 2"
622 );
623
624 let reset = checkpoint.is_none_or(|checkpoint| checkpoint.is_empty());
626 if reset {
627 for partition in [
628 &config.key_partition,
629 &config.value_partition,
630 &config.table_partition,
631 ] {
632 match context.remove(partition, None).await {
633 Ok(()) | Err(commonware_runtime::Error::PartitionMissing(_)) => {}
634 Err(err) => return Err(Error::Runtime(err)),
635 }
636 }
637 }
638
639 let oversized_cfg = OversizedConfig {
644 index_partition: config.key_partition.clone(),
645 value_partition: config.value_partition.clone(),
646 index_page_cache: config.key_page_cache.clone(),
647 index_write_buffer: config.key_write_buffer,
648 value_write_buffer: config.value_write_buffer,
649 replay_buffer: config.table_replay_buffer,
650 compression: config.value_compression,
651 codec_config: config.codec_config,
652 };
653 let oversized_context = context.child("oversized");
654 let oversized: Oversized<E, Record<K>, V> = match checkpoint
655 .filter(|checkpoint| !checkpoint.is_empty())
656 .map(|checkpoint| (checkpoint.section, checkpoint.oversized_size))
657 {
658 Some(checkpoint) => {
659 Oversized::init_with_checkpoint(oversized_context, oversized_cfg, checkpoint)
660 .await?
661 }
662 None => Oversized::init(oversized_context, oversized_cfg).await?,
663 };
664
665 let (table, table_len) = context
667 .open(&config.table_partition, TABLE_BLOB_NAME)
668 .await?;
669 let table = Arc::new(table);
670
671 let (checkpoint, resizable) = match checkpoint {
673 Some(checkpoint) if !checkpoint.is_empty() => {
675 if table_len == 0 {
677 return Err(Error::CheckpointMismatch);
678 }
679 assert!(
680 checkpoint.table_size > 0 && checkpoint.table_size.is_power_of_two(),
681 "table_size must be a power of 2"
682 );
683
684 let expected_table_len = Self::table_offset(checkpoint.table_size);
688 if table_len < expected_table_len {
689 return Err(Error::CheckpointMismatch);
690 }
691 let mut modified = if table_len != expected_table_len {
692 table.resize(expected_table_len).await?;
693 true
694 } else {
695 false
696 };
697
698 let (table_modified, resizable) = Self::recover_table(
700 &table,
701 checkpoint.table_size,
702 config.table_resize_frequency,
703 checkpoint.epoch,
704 config.table_replay_buffer,
705 )
706 .await?;
707 if table_modified {
708 modified = true;
709 }
710
711 if modified {
713 table.sync().await?;
714 }
715
716 (checkpoint, resizable)
717 }
718
719 _ => {
721 Self::init_table(&table, config.table_initial_size).await?;
722 (Checkpoint::init(config.table_initial_size), 0)
723 }
724 };
725 let table = Arc::into_inner(table).expect("table recovery reader must be closed");
726
727 let puts = context.counter("puts", "number of put operations");
729 let gets = context.counter("gets", "number of get operations");
730 let has = context.counter("has", "number of has operations");
731 let unnecessary_reads = context.counter(
732 "unnecessary_reads",
733 "number of unnecessary reads performed during key lookups",
734 );
735 let unnecessary_writes = context.counter(
736 "unnecessary_writes",
737 "number of unnecessary writes performed during resize",
738 );
739 let resizes = context.counter("resizes", "number of table resizing operations");
740 let current_value_size = oversized.value_size(checkpoint.section).await?;
741
742 Ok(Self {
743 context,
744 table_partition: config.table_partition,
745 table_size: checkpoint.table_size,
746 table_resize_threshold: checkpoint.table_size as u64 * RESIZE_THRESHOLD / 100,
747 table_resize_frequency: config.table_resize_frequency,
748 table_resize_chunk_size: config.table_resize_chunk_size,
749 table,
750 oversized,
751 blob_target_size: config.value_target_size,
752 current_section: checkpoint.section,
753 current_value_size,
754 next_epoch: checkpoint.epoch.checked_add(1).expect("epoch overflow"),
755 modified_sections: BTreeSet::new(),
756 resizable,
757 resize_progress: None,
758 puts,
759 gets,
760 has,
761 unnecessary_reads,
762 unnecessary_writes,
763 resizes,
764 })
765 }
766
767 fn table_index(&self, key: &K) -> u32 {
779 let hash = Crc32::checksum(key.as_ref());
780 hash & (self.table_size - 1)
781 }
782
783 const fn should_resize(&self) -> bool {
785 self.resizable as u64 >= self.table_resize_threshold
786 }
787
788 async fn update_section(&mut self) -> Result<(), Error> {
790 if self.current_value_size >= self.blob_target_size {
792 self.current_section += 1;
793 debug!(
794 size = self.current_value_size,
795 section = self.current_section,
796 "updated section"
797 );
798 self.current_value_size = self.oversized.value_size(self.current_section).await?;
799 }
800
801 Ok(())
802 }
803
804 async fn put(mut self: Box<Self>, key: K, value: &V) -> Result<(Box<Self>, Cursor), Error> {
806 self.puts.inc();
807
808 self.update_section().await?;
810
811 let table_index = self.table_index(&key);
813 let (entry1, entry2) = Self::read_table(&self.table, table_index).await?;
814 let head = Self::read_latest_entry(&entry1, &entry2);
815
816 let key_entry = Record::new(
818 key,
819 head.map(|(section, position, _)| (section, position)),
820 0,
821 0,
822 );
823
824 let (position, value_offset, value_size);
826 (self.oversized, position, value_offset, value_size) = self
827 .oversized
828 .append(self.current_section, key_entry, value)
829 .await?;
830 self.current_value_size = value_offset + u64::from(value_size);
831
832 let mut added = head.map(|(_, _, added)| added).unwrap_or(0);
836 added = added.saturating_add(1);
837
838 if added == self.table_resize_frequency {
840 self.resizable += 1;
841 }
842
843 self.modified_sections.insert(self.current_section);
845 let new_entry = Entry::new(self.next_epoch, self.current_section, position, added);
846 Self::update_head(&self.table, table_index, &entry1, &entry2, new_entry).await?;
847
848 if let Some(resize_progress) = self.resize_progress
850 && table_index < resize_progress
851 {
852 self.unnecessary_writes.inc();
853
854 if added == self.table_resize_frequency {
856 self.resizable += 1;
857 }
858
859 let new_table_index = self.table_size + table_index;
863 let new_entry = Entry::new(self.next_epoch, self.current_section, position, added);
864 Self::update_head(&self.table, new_table_index, &entry1, &entry2, new_entry).await?;
865 }
866
867 let cursor = Cursor::new(self.current_section, value_offset, value_size);
868 Ok((self, cursor))
869 }
870
871 async fn get_cursor(&self, cursor: Cursor) -> Result<V, Error> {
873 let value = self
874 .oversized
875 .get_value(cursor.section(), cursor.offset(), cursor.size())
876 .await?;
877
878 Ok(value)
879 }
880
881 async fn find_key(&self, key: &K) -> Result<Option<(u64, Record<K>)>, Error> {
885 let table_index = self.table_index(key);
887 let (entry1, entry2) = Self::read_table(&self.table, table_index).await?;
888 let Some((mut section, mut position, _)) = Self::read_latest_entry(&entry1, &entry2) else {
889 return Ok(None);
890 };
891
892 loop {
894 let key_entry = self.oversized.get(section, position).await?;
896
897 if key_entry.key.as_ref() == key.as_ref() {
899 return Ok(Some((section, key_entry)));
900 }
901
902 self.unnecessary_reads.inc();
904
905 let Some(next) = key_entry.next() else {
907 break; };
909 section = next.0;
910 position = next.1;
911 }
912
913 Ok(None)
914 }
915
916 async fn get_key(&self, key: &K) -> Result<Option<V>, Error> {
918 self.gets.inc();
919
920 let Some((section, key_entry)) = self.find_key(key).await? else {
921 return Ok(None);
922 };
923 let value = self
924 .oversized
925 .get_value(section, key_entry.value_offset, key_entry.value_size)
926 .await?;
927 Ok(Some(value))
928 }
929
930 async fn get<'a>(&'a self, identifier: Identifier<'a, K>) -> Result<Option<V>, Error> {
932 match identifier {
933 Identifier::Cursor(cursor) => self.get_cursor(cursor).await.map(Some),
934 Identifier::Key(key) => self.get_key(key).await,
935 }
936 }
937
938 async fn has(&self, key: &K) -> Result<bool, Error> {
940 self.has.inc();
941
942 Ok(self.find_key(key).await?.is_some())
943 }
944
945 async fn start_resize(&mut self) -> Result<(), Error> {
947 self.resizes.inc();
948
949 let old_size = self.table_size;
951 let Some(new_size) = old_size.checked_mul(2) else {
952 return Ok(());
953 };
954 self.table.resize(Self::table_offset(new_size)).await?;
955
956 self.resize_progress = Some(0);
958 debug!(old = old_size, new = new_size, "table resize started");
959
960 Ok(())
961 }
962
963 fn rewrite_entries(buf: &mut impl BufMut, entry1: &Entry, entry2: &Entry, new_entry: &Entry) {
965 if Self::compute_write_offset(entry1, entry2, new_entry.epoch) == 0 {
966 new_entry.write(buf);
967 entry2.write(buf);
968 } else {
969 entry1.write(buf);
970 new_entry.write(buf);
971 }
972 }
973
974 async fn advance_resize(&mut self) -> Result<(), Error> {
979 let current_index = self.resize_progress.unwrap();
981 let old_size = self.table_size;
982 let chunk_end = (current_index + self.table_resize_chunk_size).min(old_size);
983 let chunk_size = chunk_end - current_index;
984
985 let chunk_bytes = chunk_size as usize * Entry::FULL_SIZE;
987 let read_offset = Self::table_offset(current_index);
988 let mut read_buf = self
989 .table
990 .read_at(read_offset, chunk_bytes, ReadOptions::default())
991 .await?
992 .freeze();
993
994 let mut writes = IoBufMut::with_capacity(chunk_bytes);
996 for _ in 0..chunk_size {
997 let (entry1, entry2) = Self::parse_entries(&mut read_buf)?;
999
1000 let head = Self::read_latest_entry(&entry1, &entry2);
1002
1003 let reset_entry = match head {
1005 Some((section, position, added)) => {
1006 if added >= self.table_resize_frequency {
1008 self.resizable -= 1;
1009 }
1010 Entry::new(self.next_epoch, section, position, 0)
1011 }
1012 None => Entry::new_empty(),
1013 };
1014
1015 Self::rewrite_entries(&mut writes, &entry1, &entry2, &reset_entry);
1017 }
1018
1019 let writes = writes.freeze();
1021 let old_write = self
1022 .table
1023 .write_at(read_offset, writes.clone(), WriteOptions::default());
1024 let new_offset = (old_size as usize * Entry::FULL_SIZE) as u64 + read_offset;
1025 let new_write = self
1026 .table
1027 .write_at(new_offset, writes, WriteOptions::default());
1028 try_join(old_write, new_write).await?;
1029
1030 if chunk_end >= old_size {
1032 self.table_size = old_size * 2;
1034 self.table_resize_threshold = self.table_size as u64 * RESIZE_THRESHOLD / 100;
1035 self.resize_progress = None;
1036 debug!(
1037 old = old_size,
1038 new = self.table_size,
1039 "table resize completed"
1040 );
1041 } else {
1042 self.resize_progress = Some(chunk_end);
1044 debug!(current = current_index, chunk_end, "table resize progress");
1045 }
1046
1047 Ok(())
1048 }
1049
1050 async fn sync(mut self: Box<Self>) -> Result<(Box<Self>, Checkpoint), Error> {
1052 self.oversized = self.oversized.sync(&self.modified_sections).await?;
1054 self.modified_sections.clear();
1055
1056 if self.should_resize() && self.resize_progress.is_none() {
1058 self.start_resize().await?;
1059 }
1060
1061 if self.resize_progress.is_some() {
1063 self.advance_resize().await?;
1064 }
1065
1066 self.table.sync().await?;
1068 let stored_epoch = self.next_epoch;
1069 self.next_epoch = self.next_epoch.checked_add(1).expect("epoch overflow");
1070
1071 let oversized_size = self.oversized.size(self.current_section)?;
1073
1074 let checkpoint = Checkpoint {
1075 epoch: stored_epoch,
1076 section: self.current_section,
1077 oversized_size,
1078 table_size: self.table_size,
1079 };
1080 Ok((self, checkpoint))
1081 }
1082
1083 async fn close(mut self: Box<Self>) -> Result<Checkpoint, Error> {
1085 while self.resize_progress.is_some() {
1087 self.advance_resize().await?;
1088 }
1089
1090 let (_, checkpoint) = self.sync().await?;
1092
1093 Ok(checkpoint)
1094 }
1095
1096 async fn destroy(self) -> Result<(), Error> {
1098 self.oversized.destroy().await?;
1100
1101 drop(self.table);
1103 self.context
1104 .remove(&self.table_partition, Some(TABLE_BLOB_NAME))
1105 .await?;
1106 self.context.remove(&self.table_partition, None).await?;
1107
1108 Ok(())
1109 }
1110}
1111
1112pub struct Freezer<E: Context, K: Array, V: CodecShared>(Box<Inner<E, K, V>>);
1117
1118impl<E: Context, K: Array, V: CodecShared> std::fmt::Debug for Freezer<E, K, V> {
1119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1120 f.debug_struct("Freezer")
1121 .field("current_section", &self.0.current_section)
1122 .field("next_epoch", &self.0.next_epoch)
1123 .finish_non_exhaustive()
1124 }
1125}
1126
1127impl<E: Context, K: Array, V: CodecShared> Freezer<E, K, V> {
1128 pub async fn init(
1132 context: E,
1133 config: Config<V::Cfg>,
1134 checkpoint: Option<Checkpoint>,
1135 ) -> Result<Self, Error> {
1136 Ok(Self(Box::new(
1137 Inner::init(context, config, checkpoint).await?,
1138 )))
1139 }
1140
1141 pub async fn put(mut self, key: K, value: &V) -> Result<(Self, Cursor), Error> {
1144 let cursor;
1145 (self.0, cursor) = self.0.put(key, value).await?;
1146 Ok((self, cursor))
1147 }
1148
1149 pub async fn get<'a>(&'a self, identifier: Identifier<'a, K>) -> Result<Option<V>, Error> {
1154 self.0.get(identifier).await
1155 }
1156
1157 pub async fn has(&self, key: &K) -> Result<bool, Error> {
1162 self.0.has(key).await
1163 }
1164
1165 pub async fn sync(mut self) -> Result<(Self, Checkpoint), Error> {
1173 let checkpoint;
1174 (self.0, checkpoint) = self.0.sync().await?;
1175 Ok((self, checkpoint))
1176 }
1177
1178 pub async fn close(self) -> Result<Checkpoint, Error> {
1180 self.0.close().await
1181 }
1182
1183 pub async fn destroy(self) -> Result<(), Error> {
1185 self.0.destroy().await
1186 }
1187
1188 #[cfg(test)]
1192 pub fn resizing(&self) -> Option<u32> {
1193 self.0.resize_progress
1194 }
1195
1196 #[cfg(test)]
1198 pub fn resizable(&self) -> u32 {
1199 self.0.resizable
1200 }
1201
1202 #[cfg(test)]
1204 pub fn table_size(&self) -> u32 {
1205 self.0.table_size
1206 }
1207}
1208
1209#[cfg(all(test, feature = "arbitrary"))]
1210mod conformance {
1211 use super::*;
1212 use commonware_codec::conformance::CodecConformance;
1213 use commonware_utils::sequence::U64;
1214
1215 commonware_conformance::conformance_tests! {
1216 CodecConformance<Cursor>,
1217 CodecConformance<Checkpoint>,
1218 CodecConformance<Entry>,
1219 CodecConformance<Record<U64>>
1220 }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226 use commonware_codec::Copying;
1227 use commonware_macros::test_traced;
1228 use commonware_runtime::{
1229 Runner, Storage, Supervisor as _, WriteOptions, buffer::paged::CacheRef, deterministic,
1230 deterministic::Context,
1231 };
1232 use commonware_utils::{
1233 NZU16, NZUsize,
1234 sequence::{FixedBytes, U64},
1235 };
1236
1237 fn test_key(key: &str) -> FixedBytes<64> {
1238 let mut buf = [0u8; 64];
1239 let key = key.as_bytes();
1240 assert!(key.len() <= buf.len());
1241 buf[..key.len()].copy_from_slice(key);
1242 FixedBytes::new(buf)
1243 }
1244
1245 fn test_key_at_index(table_size: u32, table_index: u32) -> FixedBytes<64> {
1246 assert!(table_size.is_power_of_two());
1247 assert!(table_index < table_size);
1248
1249 for value in 0u64.. {
1250 let mut buf = [0u8; 64];
1251 let bytes = value.to_be_bytes();
1252 buf[..bytes.len()].copy_from_slice(&bytes);
1253 let key = FixedBytes::new(buf);
1254 if Crc32::checksum(key.as_ref()) & (table_size - 1) == table_index {
1255 return key;
1256 }
1257 }
1258
1259 unreachable!("u64 key space exhausted");
1260 }
1261
1262 type TestFreezer = Freezer<Context, U64, u64>;
1263
1264 fn is_send<T: Send>(_: T) {}
1265
1266 #[allow(dead_code)]
1267 fn assert_freezer_futures_are_send(freezer: TestFreezer, key: U64) {
1268 is_send(freezer.get(Identifier::Key(&key)));
1269 is_send(freezer.put(key, &0u64));
1270 }
1271
1272 #[allow(dead_code)]
1273 fn assert_freezer_destroy_is_send(freezer: TestFreezer) {
1274 is_send(freezer.destroy());
1275 }
1276
1277 #[test_traced]
1278 fn issue_2966_regression() {
1279 let executor = deterministic::Runner::default();
1280 executor.start(|context| async move {
1281 let cfg = super::super::Config {
1282 key_partition: "test-key-index".into(),
1283 key_write_buffer: NZUsize!(1024),
1284 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1285 value_partition: "test-value-journal".into(),
1286 value_compression: None,
1287 value_write_buffer: NZUsize!(1024),
1288 value_target_size: 10 * 1024 * 1024,
1289 table_partition: "test-table".into(),
1290 table_initial_size: 4,
1292 table_resize_frequency: 1,
1293 table_resize_chunk_size: 4,
1294 table_replay_buffer: NZUsize!(64 * 1024),
1295 codec_config: (),
1296 };
1297 let freezer =
1298 Freezer::<_, FixedBytes<64>, i32>::init(context.child("first"), cfg.clone(), None)
1299 .await
1300 .unwrap();
1301
1302 let (freezer, _) = freezer.put(test_key("key0"), &0).await.unwrap();
1305 let (freezer, _) = freezer.put(test_key("key2"), &1).await.unwrap();
1306 freezer.close().await.unwrap();
1307
1308 let (blob, size) = context.open(&cfg.table_partition, b"table").await.unwrap();
1309 let table_data = blob
1310 .read_at(0, size as usize, ReadOptions::default())
1311 .await
1312 .unwrap()
1313 .coalesce();
1314
1315 let num_entries = size as usize / Entry::FULL_SIZE;
1317 assert_eq!(num_entries, 8);
1318
1319 let mut both_empty_count = 0;
1322 for entry_idx in 0..num_entries {
1323 let offset = entry_idx * Entry::FULL_SIZE;
1324 let buf = &table_data.as_ref()[offset..offset + Entry::FULL_SIZE];
1325 let (slot0, slot1) =
1326 Inner::<Context, FixedBytes<64>, i32>::parse_entries(Copying(buf)).unwrap();
1327 if slot0.is_empty() && slot1.is_empty() {
1328 both_empty_count += 1;
1329 }
1330 }
1331 assert_eq!(both_empty_count, 4);
1333 });
1334 }
1335
1336 #[test_traced]
1337 fn issue_2955_regression() {
1338 let executor = deterministic::Runner::default();
1339 executor.start(|context| async move {
1340 let cfg = super::super::Config {
1341 key_partition: "test-key-index".into(),
1342 key_write_buffer: NZUsize!(1024),
1343 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1344 value_partition: "test-value-journal".into(),
1345 value_compression: None,
1346 value_write_buffer: NZUsize!(1024),
1347 value_target_size: 10 * 1024 * 1024,
1348 table_partition: "test-table".into(),
1349 table_initial_size: 4,
1350 table_resize_frequency: 1,
1351 table_resize_chunk_size: 4,
1352 table_replay_buffer: NZUsize!(64 * 1024),
1353 codec_config: (),
1354 };
1355
1356 let checkpoint = {
1358 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1359 context.child("first"),
1360 cfg.clone(),
1361 None,
1362 )
1363 .await
1364 .unwrap();
1365 let (freezer, _) = freezer.put(test_key("key0"), &42).await.unwrap();
1366 let (freezer, _) = freezer.sync().await.unwrap();
1367 freezer.close().await.unwrap()
1368 };
1369
1370 {
1372 let (blob, _) = context.open(&cfg.table_partition, b"table").await.unwrap();
1373 let entry_data = blob
1374 .read_at(0, Entry::FULL_SIZE, ReadOptions::default())
1375 .await
1376 .unwrap();
1377 let mut corrupted = entry_data.coalesce();
1378 corrupted.as_mut()[Entry::SIZE - 4] ^= 0xFF;
1380 corrupted.as_mut()[Entry::FULL_SIZE - 4] ^= 0xFF;
1382 blob.write_at(0, corrupted, WriteOptions::SYNC)
1383 .await
1384 .unwrap();
1385 }
1386
1387 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1392 context.child("second"),
1393 cfg.clone(),
1394 Some(checkpoint),
1395 )
1396 .await
1397 .unwrap();
1398 drop(freezer);
1399 });
1400 }
1401
1402 #[test_traced]
1403 fn no_checkpoint_deletes_partial_sync_resize() {
1404 let executor = deterministic::Runner::default();
1405 executor.start(|context| async move {
1406 let cfg = super::super::Config {
1407 key_partition: "test-key-index".into(),
1408 key_write_buffer: NZUsize!(1024),
1409 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1410 value_partition: "test-value-journal".into(),
1411 value_compression: None,
1412 value_write_buffer: NZUsize!(1024),
1413 value_target_size: 10 * 1024 * 1024,
1414 table_partition: "test-table".into(),
1415 table_initial_size: 2,
1416 table_resize_frequency: 1,
1417 table_resize_chunk_size: 1,
1418 table_replay_buffer: NZUsize!(64 * 1024),
1419 codec_config: (),
1420 };
1421 let key = test_key_at_index(4, 3);
1422
1423 {
1424 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1425 context.child("first"),
1426 cfg.clone(),
1427 None,
1428 )
1429 .await
1430 .unwrap();
1431 let (freezer, _) = freezer.put(key.clone(), &42).await.unwrap();
1432 let (freezer, _) = freezer.sync().await.unwrap();
1433
1434 assert_eq!(freezer.resizing(), Some(1));
1435 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1436 }
1437
1438 let freezer =
1439 Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1440 .await
1441 .unwrap();
1442 assert_eq!(freezer.table_size(), 2);
1443 assert_eq!(freezer.resizing(), None);
1444 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1445 });
1446 }
1447
1448 #[test_traced]
1449 fn empty_checkpoint_deletes_existing_data() {
1450 let executor = deterministic::Runner::default();
1451 executor.start(|context| async move {
1452 let cfg = super::super::Config {
1453 key_partition: "test-key-index".into(),
1454 key_write_buffer: NZUsize!(1024),
1455 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1456 value_partition: "test-value-journal".into(),
1457 value_compression: None,
1458 value_write_buffer: NZUsize!(1024),
1459 value_target_size: 10 * 1024 * 1024,
1460 table_partition: "test-table".into(),
1461 table_initial_size: 2,
1462 table_resize_frequency: 1,
1463 table_resize_chunk_size: 1,
1464 table_replay_buffer: NZUsize!(64 * 1024),
1465 codec_config: (),
1466 };
1467 let key = test_key_at_index(4, 3);
1468
1469 {
1470 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1471 context.child("first"),
1472 cfg.clone(),
1473 None,
1474 )
1475 .await
1476 .unwrap();
1477 let (freezer, _) = freezer.put(key.clone(), &42).await.unwrap();
1478 let (freezer, _) = freezer.sync().await.unwrap();
1479 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1480 }
1481
1482 let checkpoint = Checkpoint {
1483 epoch: 0,
1484 section: 0,
1485 oversized_size: 0,
1486 table_size: 0,
1487 };
1488 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1489 context.child("second"),
1490 cfg.clone(),
1491 Some(checkpoint),
1492 )
1493 .await
1494 .unwrap();
1495 assert_eq!(freezer.table_size(), 2);
1496 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1497 });
1498 }
1499
1500 #[test_traced]
1501 fn no_checkpoint_deletes_close_started_partial_resize() {
1502 let executor = deterministic::Runner::default();
1503 executor.start(|context| async move {
1504 let cfg = super::super::Config {
1505 key_partition: "test-key-index".into(),
1506 key_write_buffer: NZUsize!(1024),
1507 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1508 value_partition: "test-value-journal".into(),
1509 value_compression: None,
1510 value_write_buffer: NZUsize!(1024),
1511 value_target_size: 10 * 1024 * 1024,
1512 table_partition: "test-table".into(),
1513 table_initial_size: 2,
1514 table_resize_frequency: 1,
1515 table_resize_chunk_size: 1,
1516 table_replay_buffer: NZUsize!(64 * 1024),
1517 codec_config: (),
1518 };
1519 let key = test_key_at_index(4, 3);
1520
1521 {
1522 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1523 context.child("first"),
1524 cfg.clone(),
1525 None,
1526 )
1527 .await
1528 .unwrap();
1529 let (freezer, _) = freezer.put(key.clone(), &42).await.unwrap();
1530 let checkpoint = freezer.close().await.unwrap();
1531 assert_eq!(checkpoint.table_size, 2);
1532 }
1533
1534 let freezer =
1535 Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1536 .await
1537 .unwrap();
1538 assert_eq!(freezer.table_size(), 2);
1539 assert_eq!(freezer.resizing(), None);
1540 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1541 });
1542 }
1543
1544 #[test_traced]
1545 fn no_checkpoint_deletes_completed_resize() {
1546 let executor = deterministic::Runner::default();
1547 executor.start(|context| async move {
1548 let cfg = super::super::Config {
1549 key_partition: "test-key-index".into(),
1550 key_write_buffer: NZUsize!(1024),
1551 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1552 value_partition: "test-value-journal".into(),
1553 value_compression: None,
1554 value_write_buffer: NZUsize!(1024),
1555 value_target_size: 10 * 1024 * 1024,
1556 table_partition: "test-table".into(),
1557 table_initial_size: 2,
1558 table_resize_frequency: 1,
1559 table_resize_chunk_size: 2,
1560 table_replay_buffer: NZUsize!(64 * 1024),
1561 codec_config: (),
1562 };
1563 let key = test_key_at_index(4, 3);
1564
1565 {
1566 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1567 context.child("first"),
1568 cfg.clone(),
1569 None,
1570 )
1571 .await
1572 .unwrap();
1573 let (freezer, _) = freezer.put(key.clone(), &42).await.unwrap();
1574 let (freezer, checkpoint) = freezer.sync().await.unwrap();
1575
1576 assert_eq!(checkpoint.table_size, 4);
1577 assert_eq!(freezer.resizing(), None);
1578 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1579 }
1580
1581 let freezer =
1582 Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1583 .await
1584 .unwrap();
1585 assert_eq!(freezer.table_size(), 2);
1586 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1587 });
1588 }
1589
1590 #[test_traced]
1591 fn checkpoint_rewinds_completed_resize() {
1592 let executor = deterministic::Runner::default();
1593 executor.start(|context| async move {
1594 let cfg = super::super::Config {
1595 key_partition: "test-key-index".into(),
1596 key_write_buffer: NZUsize!(1024),
1597 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1598 value_partition: "test-value-journal".into(),
1599 value_compression: None,
1600 value_write_buffer: NZUsize!(1024),
1601 value_target_size: 10 * 1024 * 1024,
1602 table_partition: "test-table".into(),
1603 table_initial_size: 2,
1604 table_resize_frequency: 1,
1605 table_resize_chunk_size: 2,
1606 table_replay_buffer: NZUsize!(64 * 1024),
1607 codec_config: (),
1608 };
1609 let key = test_key_at_index(4, 3);
1610
1611 let stale_checkpoint = {
1612 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1613 context.child("first"),
1614 cfg.clone(),
1615 None,
1616 )
1617 .await
1618 .unwrap();
1619 let (freezer, stale_checkpoint) = freezer.sync().await.unwrap();
1620 assert_eq!(stale_checkpoint.table_size, 2);
1621
1622 let (freezer, _) = freezer.put(key.clone(), &42).await.unwrap();
1623 let (freezer, checkpoint) = freezer.sync().await.unwrap();
1624 assert_eq!(checkpoint.table_size, 4);
1625 assert_eq!(freezer.resizing(), None);
1626 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1627
1628 stale_checkpoint
1629 };
1630
1631 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1632 context.child("second"),
1633 cfg.clone(),
1634 Some(stale_checkpoint),
1635 )
1636 .await
1637 .unwrap();
1638 assert_eq!(freezer.table_size(), 2);
1639 assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1640 });
1641 }
1642
1643 #[test_traced]
1644 fn non_empty_checkpoint_against_empty_table_errors() {
1645 let executor = deterministic::Runner::default();
1646 executor.start(|context| async move {
1647 let cfg = super::super::Config {
1648 key_partition: "test-key-index".into(),
1649 key_write_buffer: NZUsize!(1024),
1650 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1651 value_partition: "test-value-journal".into(),
1652 value_compression: None,
1653 value_write_buffer: NZUsize!(1024),
1654 value_target_size: 10 * 1024 * 1024,
1655 table_partition: "test-table".into(),
1656 table_initial_size: 2,
1657 table_resize_frequency: 1,
1658 table_resize_chunk_size: 1,
1659 table_replay_buffer: NZUsize!(64 * 1024),
1660 codec_config: (),
1661 };
1662
1663 let checkpoint = Checkpoint {
1664 epoch: 1,
1665 section: 0,
1666 oversized_size: 0,
1667 table_size: 2,
1668 };
1669 let result = Freezer::<_, FixedBytes<64>, i32>::init(
1670 context.child("storage"),
1671 cfg.clone(),
1672 Some(checkpoint),
1673 )
1674 .await;
1675 assert!(matches!(result, Err(Error::CheckpointMismatch)));
1676 });
1677 }
1678
1679 #[test_traced]
1683 fn non_empty_checkpoint_against_short_table_errors() {
1684 let executor = deterministic::Runner::default();
1685 executor.start(|context| async move {
1686 let cfg = super::super::Config {
1687 key_partition: "test-key-index".into(),
1688 key_write_buffer: NZUsize!(1024),
1689 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1690 value_partition: "test-value-journal".into(),
1691 value_compression: None,
1692 value_write_buffer: NZUsize!(1024),
1693 value_target_size: 10 * 1024 * 1024,
1694 table_partition: "test-table".into(),
1695 table_initial_size: 2,
1696 table_resize_frequency: 1,
1697 table_resize_chunk_size: 1,
1698 table_replay_buffer: NZUsize!(64 * 1024),
1699 codec_config: (),
1700 };
1701
1702 let short_len = Entry::FULL_SIZE as u64;
1703 let (table, _) = context
1704 .open(&cfg.table_partition, TABLE_BLOB_NAME)
1705 .await
1706 .unwrap();
1707 table.resize(short_len).await.unwrap();
1708 table.sync().await.unwrap();
1709 drop(table);
1710
1711 let checkpoint = Checkpoint {
1712 epoch: 1,
1713 section: 0,
1714 oversized_size: 0,
1715 table_size: 2,
1716 };
1717 for child in ["first", "retry"] {
1718 let result = Freezer::<_, FixedBytes<64>, i32>::init(
1719 context.child(child),
1720 cfg.clone(),
1721 Some(checkpoint),
1722 )
1723 .await;
1724 assert!(matches!(result, Err(Error::CheckpointMismatch)));
1725 }
1726
1727 let (_, size) = context
1729 .open(&cfg.table_partition, TABLE_BLOB_NAME)
1730 .await
1731 .unwrap();
1732 assert_eq!(size, short_len);
1733 });
1734 }
1735
1736 #[test_traced]
1737 fn corrupted_committed_value_surfaces_at_read() {
1738 let executor = deterministic::Runner::default();
1739 executor.start(|context| async move {
1740 let cfg = super::super::Config {
1741 key_partition: "test-key-index".into(),
1742 key_write_buffer: NZUsize!(1024),
1743 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1744 value_partition: "test-value-journal".into(),
1745 value_compression: None,
1746 value_write_buffer: NZUsize!(1024),
1747 value_target_size: 10 * 1024 * 1024,
1748 table_partition: "test-table".into(),
1749 table_initial_size: 4,
1750 table_resize_frequency: 1,
1751 table_resize_chunk_size: 4,
1752 table_replay_buffer: NZUsize!(64 * 1024),
1753 codec_config: (),
1754 };
1755
1756 let checkpoint = {
1758 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1759 context.child("first"),
1760 cfg.clone(),
1761 None,
1762 )
1763 .await
1764 .unwrap();
1765 let (freezer, _) = freezer.put(test_key("key0"), &42).await.unwrap();
1766 let (freezer, _) = freezer.put(test_key("key1"), &43).await.unwrap();
1767 let (freezer, _) = freezer.sync().await.unwrap();
1768 freezer.close().await.unwrap()
1769 };
1770 assert!(checkpoint.oversized_size > 0);
1771
1772 {
1774 let (blob, len) = context
1775 .open(&cfg.value_partition, &checkpoint.section.to_be_bytes())
1776 .await
1777 .unwrap();
1778 let byte = blob
1779 .read_at(len - 1, 1, ReadOptions::default())
1780 .await
1781 .unwrap();
1782 let mut corrupted = byte.coalesce();
1783 corrupted.as_mut()[0] ^= 0xFF;
1784 blob.write_at(len - 1, corrupted, WriteOptions::SYNC)
1785 .await
1786 .unwrap();
1787 }
1788
1789 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1793 context.child("second"),
1794 cfg.clone(),
1795 Some(checkpoint),
1796 )
1797 .await
1798 .unwrap();
1799 assert!(matches!(
1800 freezer.get(Identifier::Key(&test_key("key1"))).await,
1801 Err(Error::Journal(crate::journal::Error::ChecksumMismatch(
1802 _,
1803 _
1804 )))
1805 ));
1806 assert_eq!(
1807 freezer
1808 .get(Identifier::Key(&test_key("key0")))
1809 .await
1810 .unwrap(),
1811 Some(42)
1812 );
1813
1814 let (freezer, _) = freezer.put(test_key("key2"), &44).await.unwrap();
1816 let (freezer, _) = freezer.sync().await.unwrap();
1817 assert_eq!(
1818 freezer
1819 .get(Identifier::Key(&test_key("key2")))
1820 .await
1821 .unwrap(),
1822 Some(44)
1823 );
1824 });
1825 }
1826
1827 #[test_traced]
1828 fn incomplete_committed_section_fails_init() {
1829 let executor = deterministic::Runner::default();
1830 executor.start(|context| async move {
1831 let cfg = super::super::Config {
1833 key_partition: "test-key-index".into(),
1834 key_write_buffer: NZUsize!(1024),
1835 key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
1836 value_partition: "test-value-journal".into(),
1837 value_compression: None,
1838 value_write_buffer: NZUsize!(1024),
1839 value_target_size: 8,
1840 table_partition: "test-table".into(),
1841 table_initial_size: 4,
1842 table_resize_frequency: 1,
1843 table_resize_chunk_size: 4,
1844 table_replay_buffer: NZUsize!(64 * 1024),
1845 codec_config: (),
1846 };
1847
1848 let checkpoint = {
1850 let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1851 context.child("first"),
1852 cfg.clone(),
1853 None,
1854 )
1855 .await
1856 .unwrap();
1857 let (freezer, _) = freezer.put(test_key("key0"), &42).await.unwrap();
1858 let (freezer, _) = freezer.put(test_key("key1"), &43).await.unwrap();
1859 let (freezer, _) = freezer.sync().await.unwrap();
1860 freezer.close().await.unwrap()
1861 };
1862 assert!(checkpoint.section > 0);
1863
1864 {
1867 let (blob, len) = context
1868 .open(&cfg.value_partition, &0u64.to_be_bytes())
1869 .await
1870 .unwrap();
1871 assert!(len > 0);
1872 blob.resize(len - 1).await.unwrap();
1873 blob.sync().await.unwrap();
1874 }
1875
1876 for instance in ["second", "third"] {
1880 let result = Freezer::<_, FixedBytes<64>, i32>::init(
1881 context.child(instance),
1882 cfg.clone(),
1883 Some(checkpoint),
1884 )
1885 .await;
1886 assert!(matches!(
1887 result,
1888 Err(Error::Journal(crate::journal::Error::Corruption(_)))
1889 ));
1890 }
1891 });
1892 }
1893}