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