1use std::{
6 collections::{BTreeSet, HashMap},
7 path::Path,
8 sync::Mutex,
9 time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use iota_metrics::spawn_monitored_task;
13use iota_sdk_types::{TransactionEffects, TransactionEvents, Version};
14use iota_types::{global_state_hash::GlobalStateHash, storage::MarkerValue};
15use serde::{Deserialize, Serialize};
16pub use typed_store::DbSnapshot;
17use typed_store::{
18 DBMapUtils, DbIterator,
19 metrics::SamplingInterval,
20 rocks::{
21 DBMap, DBMapTableConfigMap, DBOptions, MetricConf, ReadWriteOptions, default_db_options,
22 read_size_from_env, synced_write_options,
23 },
24 rocksdb::LiveFile,
25 traits::Map,
26};
27
28use super::*;
29use crate::{
30 authority::{
31 authority_store_types::{
32 StoreObject, StoreObjectValueV2, StoreObjectWrapper, get_store_object,
33 try_construct_object,
34 },
35 epoch_markers::EpochMarkers,
36 epoch_start_configuration::EpochStartConfiguration,
37 historic_ledger::HistoricLedger,
38 historic_objects::HistoricObjects,
39 ledger_backlog_migration::LedgerBacklogMigrationProgress,
40 object_backlog_sweep::ObjectBacklogSweepProgress,
41 },
42 epoch_buckets::historic_root,
43};
44
45const ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE: &str = "OBJECTS_BLOCK_CACHE_MB";
46pub(crate) const ENV_VAR_LOCKS_BLOCK_CACHE_SIZE: &str = "LOCKS_BLOCK_CACHE_MB";
47const ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE: &str = "TRANSACTIONS_BLOCK_CACHE_MB";
48const ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE: &str = "EFFECTS_BLOCK_CACHE_MB";
49
50const OBJECTS_PRUNER_WATERMARK_CF: &str = "pruned_checkpoint";
55
56#[derive(Default)]
58pub struct AuthorityPerpetualTablesOptions {
59 pub enable_write_stall: bool,
61 pub historic_db_path: Option<PathBuf>,
67}
68
69impl AuthorityPerpetualTablesOptions {
70 fn apply_to(&self, mut db_options: DBOptions) -> DBOptions {
71 if !self.enable_write_stall {
72 db_options = db_options.disable_write_throttling();
73 }
74 db_options
75 }
76}
77
78#[derive(DBMapUtils)]
81pub struct AuthorityPerpetualTables {
82 pub(crate) objects: DBMap<ObjectKey, StoreObjectWrapper>,
100
101 pub(crate) live_owned_object_markers: DBMap<ObjectReference, ()>,
103
104 pub(crate) transactions: DBMap<TransactionDigest, TrustedTransaction>,
112
113 pub(crate) effects: DBMap<TransactionEffectsDigest, TransactionEffects>,
128
129 pub(crate) executed_effects: DBMap<TransactionDigest, TransactionEffectsDigest>,
137
138 pub(crate) events_2: DBMap<TransactionDigest, TransactionEvents>,
143
144 pub(crate) executed_transactions_to_checkpoint:
155 DBMap<TransactionDigest, (EpochId, CheckpointSequenceNumber)>,
156
157 pub(crate) root_state_hash_by_epoch:
161 DBMap<EpochId, (CheckpointSequenceNumber, GlobalStateHash)>,
162
163 pub(crate) epoch_start_configuration: DBMap<(), EpochStartConfiguration>,
165
166 pub(crate) total_iota_supply: DBMap<(), TotalIotaSupplyCheck>,
170
171 pub(crate) expected_storage_fund_imbalance: DBMap<(), i64>,
176
177 pub(crate) object_per_epoch_marker_table: DBMap<(EpochId, ObjectKey), MarkerValue>,
181
182 pub(crate) object_backlog_sweep_progress: DBMap<(), ObjectBacklogSweepProgress>,
187
188 pub(crate) object_backlog_sweep_bound: DBMap<(), CheckpointSequenceNumber>,
195
196 pub(crate) object_backlog_sweep_epoch: DBMap<(), EpochId>,
201
202 pub(crate) ledger_backlog_migration_progress: DBMap<(), LedgerBacklogMigrationProgress>,
207
208 pub(crate) ledger_backlog_migration_floor: DBMap<(), EpochId>,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
216pub(crate) struct TotalIotaSupplyCheck {
217 pub(crate) total_supply: u64,
219 pub(crate) last_check_epoch: EpochId,
221}
222
223impl AuthorityPerpetualTablesReadOnly {
224 pub fn ledger_backlog_migrated(&self) -> Result<bool, TypedStoreError> {
228 self.ledger_backlog_migration_progress
229 .try_catch_up_with_primary()?;
230 Ok(matches!(
231 self.ledger_backlog_migration_progress.get(&())?,
232 Some(LedgerBacklogMigrationProgress::Done | LedgerBacklogMigrationProgress::Cleared)
233 ))
234 }
235}
236
237impl AuthorityPerpetualTables {
238 pub fn path(parent_path: &Path) -> PathBuf {
239 parent_path.join("perpetual")
240 }
241
242 pub fn open(
243 parent_path: &Path,
244 db_options_override: Option<AuthorityPerpetualTablesOptions>,
245 ) -> Self {
246 Self::open_with_db_options(parent_path, db_options_override).0
247 }
248
249 pub fn open_with_historic_objects(
253 parent_path: &Path,
254 db_options_override: Option<AuthorityPerpetualTablesOptions>,
255 ) -> Result<(Self, HistoricObjects, HistoricLedger, EpochMarkers), TypedStoreError> {
256 let (tables, db_options, ledger_db_options, historic_root) =
257 Self::open_with_db_options(parent_path, db_options_override);
258 let mut historic_objects = HistoricObjects::open(
259 tables.objects.db.clone(),
260 &db_options,
261 tables.objects.clone(),
262 &historic_root,
263 )?;
264 historic_objects.objects_pruned_through = tables.objects_pruner_watermark()?;
265 if let Some(epoch) = tables.object_backlog_sweep_epoch.get(&())? {
266 historic_objects.record_backlog_sweep_epoch(epoch);
267 }
268 if matches!(
269 tables.object_backlog_sweep_progress.get(&())?,
270 Some(ObjectBacklogSweepProgress::Done)
271 ) {
272 historic_objects.mark_backlog_swept();
273 }
274 let historic_ledger = HistoricLedger::open(
275 tables.objects.db.clone(),
276 &ledger_db_options,
277 &historic_root,
278 )?;
279 let epoch_markers = EpochMarkers::open(tables.objects.db.clone(), &db_options)?;
280 Ok((tables, historic_objects, historic_ledger, epoch_markers))
281 }
282
283 fn open_with_db_options(
287 parent_path: &Path,
288 db_options_override: Option<AuthorityPerpetualTablesOptions>,
289 ) -> (Self, DBOptions, DBOptions, PathBuf) {
290 let db_options_override = db_options_override.unwrap_or_default();
291 let db_options =
292 db_options_override.apply_to(default_db_options().optimize_db_for_write_throughput(4));
293 let path = Self::path(parent_path);
294 let historic_root = historic_root(&path, db_options_override.historic_db_path.as_deref());
295 let ledger_db_options = historic_ledger_db_options(&db_options);
296 let table_options =
297 Self::table_options(&path, &db_options, &ledger_db_options, &historic_root);
298 let tables = Self::open_tables_read_write(
299 path,
300 MetricConf::new("perpetual")
301 .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0)),
302 Some(db_options.options.clone()),
303 Some(table_options),
304 );
305 (tables, db_options, ledger_db_options, historic_root)
306 }
307
308 fn table_options(
314 path: &Path,
315 db_options: &DBOptions,
316 ledger_db_options: &DBOptions,
317 historic_root: &Path,
318 ) -> DBMapTableConfigMap {
319 let mut table_options = BTreeMap::from([
320 (
321 "objects".to_string(),
322 objects_table_config(db_options.clone()),
323 ),
324 (
325 "live_owned_object_markers".to_string(),
326 live_owned_object_markers_table_config(db_options.clone()),
327 ),
328 (
329 "transactions".to_string(),
330 transactions_table_config(db_options.clone()),
331 ),
332 (
333 "effects".to_string(),
334 effects_table_config(db_options.clone()),
335 ),
336 ]);
337 table_options.extend(HistoricObjects::extra_column_family_options(
338 path,
339 db_options,
340 historic_root,
341 ));
342 table_options.extend(HistoricLedger::extra_column_family_options(
343 path,
344 ledger_db_options,
345 historic_root,
346 ));
347 table_options.extend(EpochMarkers::extra_column_family_options(path, db_options));
348 DBMapTableConfigMap::new(table_options)
349 }
350
351 pub fn open_readonly(
354 parent_path: &Path,
355 historic_db_path: Option<&Path>,
356 ) -> AuthorityPerpetualTablesReadOnly {
357 let path = Self::path(parent_path);
358 let db_options = default_db_options().optimize_db_for_write_throughput(4);
359 let table_options = Self::table_options(
360 &path,
361 &db_options,
362 &historic_ledger_db_options(&db_options),
363 &historic_root(&path, historic_db_path),
364 );
365 Self::get_read_only_handle_with_table_options(
366 path,
367 None,
368 None,
369 MetricConf::new("perpetual_readonly"),
370 table_options,
371 )
372 }
373
374 pub fn find_object_lt_or_eq_version(
380 &self,
381 object_id: ObjectId,
382 version: Version,
383 ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, IotaError> {
384 let mut iter = self.objects.safe_range_iter_reversed(
385 ObjectKey::min_for_id(&object_id)..=ObjectKey(object_id, version),
386 );
387 match iter.next() {
388 Some(Ok((key, o))) => Ok(Some((key, o.migrate()))),
391 Some(Err(e)) => Err(e.into()),
392 None => Ok(None),
393 }
394 }
395
396 fn construct_object(
397 &self,
398 object_key: &ObjectKey,
399 store_object: StoreObjectValueV2,
400 ) -> Result<Object, IotaError> {
401 try_construct_object(object_key, store_object)
402 }
403
404 pub fn object(
407 &self,
408 object_key: &ObjectKey,
409 store_object: StoreObjectWrapper,
410 ) -> Result<Option<Object>, IotaError> {
411 let StoreObject::Value(store_object) = store_object.migrate().into_inner() else {
412 return Ok(None);
413 };
414 Ok(Some(self.construct_object(object_key, *store_object)?))
415 }
416
417 pub fn object_reference(
418 &self,
419 object_key: &ObjectKey,
420 store_object: StoreObjectWrapper,
421 ) -> Result<ObjectReference, IotaError> {
422 let obj_ref = match store_object.migrate().into_inner() {
423 StoreObject::Value(object) => self.construct_object(object_key, *object)?.object_ref(),
424 StoreObject::Deleted => {
425 ObjectReference::new(object_key.0, object_key.1, ObjectDigest::OBJECT_DELETED)
426 }
427 StoreObject::Wrapped => {
428 ObjectReference::new(object_key.0, object_key.1, ObjectDigest::OBJECT_WRAPPED)
429 }
430 };
431 Ok(obj_ref)
432 }
433
434 pub fn tombstone_reference(
435 &self,
436 object_key: &ObjectKey,
437 store_object: &StoreObjectWrapper,
438 ) -> Result<Option<ObjectReference>, IotaError> {
439 let obj_ref = match store_object.inner() {
440 StoreObject::Deleted => Some(ObjectReference::new(
441 object_key.0,
442 object_key.1,
443 ObjectDigest::OBJECT_DELETED,
444 )),
445 StoreObject::Wrapped => Some(ObjectReference::new(
446 object_key.0,
447 object_key.1,
448 ObjectDigest::OBJECT_WRAPPED,
449 )),
450 _ => None,
451 };
452 Ok(obj_ref)
453 }
454
455 pub fn get_latest_object_ref_or_tombstone(
456 &self,
457 object_id: ObjectId,
458 ) -> Result<Option<ObjectReference>, IotaError> {
459 let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
460
461 if let Some(Ok((object_key, value))) = iterator.next() {
462 if object_key.0 == object_id {
463 return Ok(Some(self.object_reference(&object_key, value)?));
464 }
465 }
466 Ok(None)
467 }
468
469 pub fn get_latest_object_or_tombstone(
470 &self,
471 object_id: ObjectId,
472 ) -> Result<Option<(ObjectKey, StoreObjectWrapper)>, IotaError> {
473 let mut iterator = self.objects.safe_iter_with_prefix_reversed(&object_id);
474
475 if let Some(Ok((object_key, value))) = iterator.next() {
476 if object_key.0 == object_id {
477 return Ok(Some((object_key, value.migrate())));
480 }
481 }
482 Ok(None)
483 }
484
485 pub fn get_recovery_epoch_at_restart(&self) -> IotaResult<EpochId> {
486 Ok(self
487 .epoch_start_configuration
488 .get(&())?
489 .expect("Must have current epoch.")
490 .epoch_start_state()
491 .epoch())
492 }
493
494 pub fn set_epoch_start_configuration(
495 &self,
496 epoch_start_configuration: &EpochStartConfiguration,
497 ) -> IotaResult {
498 let mut wb = self.epoch_start_configuration.batch();
499 wb.insert_batch(
500 &self.epoch_start_configuration,
501 std::iter::once(((), epoch_start_configuration)),
502 )?;
503 wb.write()?;
504 Ok(())
505 }
506
507 pub fn get_newer_object_keys(
508 &self,
509 object: &(ObjectId, Version),
510 ) -> IotaResult<Vec<ObjectKey>> {
511 let mut objects = vec![];
512 for result in self.objects.safe_iter_with_prefix_from(
513 &object.0,
514 std::ops::Bound::Included(&object.1.next().unwrap()),
515 ) {
516 let (key, _) = result?;
517 objects.push(key);
518 }
519 Ok(objects)
520 }
521
522 pub fn database_is_empty(&self) -> IotaResult<bool> {
523 Ok(self.objects.safe_iter().next().is_none())
524 }
525
526 pub fn db_snapshot(&self) -> DbSnapshot<'_> {
530 self.objects.db.snapshot()
531 }
532
533 pub fn iter_live_object_set_at<'a>(
535 &'a self,
536 db_snapshot: &'a DbSnapshot<'a>,
537 ) -> LiveSetIter<'a> {
538 LiveSetIter {
539 iter: Box::new(self.objects.safe_iter_at_snapshot(db_snapshot)),
540 tables: self,
541 prev: None,
542 }
543 }
544
545 pub fn iter_live_object_set(&self) -> LiveSetIter<'_> {
546 LiveSetIter {
547 iter: Box::new(self.objects.safe_iter()),
548 tables: self,
549 prev: None,
550 }
551 }
552
553 pub fn range_iter_live_object_set(
554 &self,
555 lower_bound: Option<ObjectId>,
556 upper_bound: Option<ObjectId>,
557 ) -> LiveSetIter<'_> {
558 let lower_bound = lower_bound.as_ref().map(ObjectKey::min_for_id);
559 let upper_bound = upper_bound.as_ref().map(ObjectKey::max_for_id);
560
561 LiveSetIter {
562 iter: Box::new(self.objects.safe_iter_with_bounds(lower_bound, upper_bound)),
563 tables: self,
564 prev: None,
565 }
566 }
567
568 pub fn compact(&self) -> Result<(), TypedStoreError> {
571 self.objects.compact_range(
572 &ObjectKey(ObjectId::ZERO, Version::MIN_VALID_INCL),
573 &ObjectKey(ObjectId::MAX, Version::MAX_VALID_EXCL),
574 )
575 }
576
577 fn periodically_compacted_tables(&self) -> BTreeSet<&str> {
581 [
582 self.objects.cf_name(),
583 self.transactions.cf_name(),
584 self.effects.cf_name(),
585 self.executed_effects.cf_name(),
586 self.events_2.cf_name(),
587 self.executed_transactions_to_checkpoint.cf_name(),
588 ]
589 .into_iter()
590 .collect()
591 }
592
593 fn compact_next_sst_file(
601 &self,
602 delay_days: usize,
603 last_processed: &Mutex<HashMap<String, SystemTime>>,
604 ) -> Result<Option<LiveFile>, anyhow::Error> {
605 let compacted_tables = self.periodically_compacted_tables();
606 let db_path = self.objects.db.path_for_pruning();
607 let mut state = last_processed
608 .lock()
609 .expect("failed to obtain a lock for last processed SST files");
610 let mut sst_file_for_compaction: Option<LiveFile> = None;
611 let time_threshold =
612 SystemTime::now() - Duration::from_secs(delay_days as u64 * 24 * 60 * 60);
613 for sst_file in self.objects.db.live_files()? {
614 if !compacted_tables.contains(sst_file.column_family_name.as_str()) {
617 continue;
618 }
619 let file_path = db_path.join(sst_file.name.clone().trim_matches('/'));
620 let last_modified = std::fs::metadata(file_path)?.modified()?;
621 if sst_file.level < 1
622 || sst_file.start_key.is_none()
623 || sst_file.end_key.is_none()
624 || last_modified > time_threshold
625 || state.get(&sst_file.name).unwrap_or(&UNIX_EPOCH) > &time_threshold
626 {
627 continue;
628 }
629 if let Some(candidate) = &sst_file_for_compaction {
630 if candidate.size > sst_file.size {
631 continue;
632 }
633 }
634 sst_file_for_compaction = Some(sst_file);
635 }
636 let Some(sst_file) = sst_file_for_compaction else {
637 return Ok(None);
638 };
639 info!(
640 "Manual compaction of sst file {:?}. Size: {:?}, level: {:?}",
641 sst_file.name, sst_file.size, sst_file.level
642 );
643 self.objects.compact_range_raw(
644 &sst_file.column_family_name,
645 sst_file.start_key.clone().unwrap(),
646 sst_file.end_key.clone().unwrap(),
647 )?;
648 state.insert(sst_file.name.clone(), SystemTime::now());
649 Ok(Some(sst_file))
650 }
651
652 pub fn spawn_periodic_compaction(self: &Arc<Self>, delay_days: usize) {
657 let perpetual_tables = Arc::downgrade(self);
659 spawn_monitored_task!(async move {
660 let last_processed = Arc::new(Mutex::new(HashMap::new()));
661 loop {
662 let Some(tables) = perpetual_tables.upgrade() else {
663 break;
664 };
665 let state = last_processed.clone();
666 let result = tokio::task::spawn_blocking(move || {
667 tables.compact_next_sst_file(delay_days, &state)
668 })
669 .await;
670 let mut sleep_interval_secs = 1;
671 match result {
672 Err(err) => error!("Failed to compact sst file: {:?}", err),
673 Ok(Err(err)) => error!("Failed to compact sst file: {:?}", err),
674 Ok(Ok(None)) => {
675 sleep_interval_secs = 3600;
676 }
677 _ => {}
678 }
679 tokio::time::sleep(Duration::from_secs(sleep_interval_secs)).await;
680 }
681 });
682 }
683
684 pub fn get_root_state_hash(
685 &self,
686 epoch: EpochId,
687 ) -> IotaResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
688 Ok(self.root_state_hash_by_epoch.get(&epoch)?)
689 }
690
691 pub fn insert_root_state_hash(
692 &self,
693 epoch: EpochId,
694 last_checkpoint_of_epoch: CheckpointSequenceNumber,
695 hash: GlobalStateHash,
696 ) -> IotaResult {
697 self.root_state_hash_by_epoch
698 .insert(&epoch, &(last_checkpoint_of_epoch, hash))?;
699 Ok(())
700 }
701
702 fn objects_pruner_watermark(
706 &self,
707 ) -> Result<Option<CheckpointSequenceNumber>, TypedStoreError> {
708 if let Some(watermark) = self.object_backlog_sweep_bound.get(&())? {
709 return Ok(Some(watermark));
710 }
711 let db = &self.objects.db;
712 if db.cf_handle(OBJECTS_PRUNER_WATERMARK_CF).is_none() {
713 return Ok(None);
714 }
715 DBMap::<(), CheckpointSequenceNumber>::reopen(
716 db,
717 Some(OBJECTS_PRUNER_WATERMARK_CF),
718 &ReadWriteOptions::default(),
719 true,
720 )?
721 .get(&())
722 }
723
724 pub fn retire_objects_pruner_watermark(&self) -> Result<(), TypedStoreError> {
732 let db = &self.objects.db;
733 if db.cf_handle(OBJECTS_PRUNER_WATERMARK_CF).is_none() {
734 return Ok(());
735 }
736 let pruned: DBMap<(), CheckpointSequenceNumber> = DBMap::reopen(
737 db,
738 Some(OBJECTS_PRUNER_WATERMARK_CF),
739 &ReadWriteOptions::default(),
740 true,
741 )?;
742 if let Some(watermark) = pruned.get(&())? {
743 let mut batch = self.object_backlog_sweep_bound.batch();
746 batch.insert_batch(&self.object_backlog_sweep_bound, [((), watermark)])?;
747 batch.write_opt(&synced_write_options())?;
748 info!(watermark, "carried the objects pruner's watermark over");
749 }
750 drop(pruned);
751 db.drop_cf(OBJECTS_PRUNER_WATERMARK_CF)
752 }
753
754 pub fn mark_object_backlog_swept(&self) -> IotaResult {
760 self.object_backlog_sweep_progress
761 .insert(&(), &ObjectBacklogSweepProgress::Done)?;
762 Ok(())
763 }
764
765 pub fn mark_object_backlog_swept_into(&self, epoch: EpochId) -> IotaResult {
769 let mut batch = self.object_backlog_sweep_progress.batch();
770 batch.insert_batch(&self.object_backlog_sweep_epoch, [((), epoch)])?;
771 batch.insert_batch(
772 &self.object_backlog_sweep_progress,
773 [((), ObjectBacklogSweepProgress::Done)],
774 )?;
775 batch.write()?;
776 Ok(())
777 }
778
779 pub fn mark_object_backlog_sweep_skipped(&self) -> IotaResult {
784 self.object_backlog_sweep_progress
785 .insert(&(), &ObjectBacklogSweepProgress::Skipped)?;
786 Ok(())
787 }
788
789 pub fn mark_ledger_backlog_migrated(&self) -> IotaResult {
796 self.ledger_backlog_migration_progress
797 .insert(&(), &LedgerBacklogMigrationProgress::Cleared)?;
798 Ok(())
799 }
800
801 pub fn insert_store_object_v1_test_only(&self, object: Object) -> IotaResult {
802 use crate::authority::authority_store_types::{StoreObjectV1, StoreObjectValue};
803
804 let object_reference = object.object_ref();
805 let v2_value = match get_store_object(object, None).into_inner() {
806 StoreObject::Value(v) => *v,
807 other => unreachable!("get_store_object must produce a Value variant, got {other:?}"),
808 };
809 let v1_value = StoreObjectValue {
810 data: v2_value.data,
811 owner: v2_value.owner,
812 previous_transaction: v2_value.previous_transaction,
813 storage_rebate: v2_value.storage_rebate,
814 };
815 let wrapper = StoreObjectWrapper::V1(StoreObjectV1::Value(Box::new(v1_value)));
816
817 let mut wb = self.objects.batch();
818 wb.insert_batch(
819 &self.objects,
820 std::iter::once((ObjectKey::from(object_reference), wrapper)),
821 )?;
822 wb.write()?;
823 Ok(())
824 }
825
826 pub fn insert_store_object_v2_test_only(
827 &self,
828 object: Object,
829 previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
830 ) -> IotaResult {
831 let object_reference = object.object_ref();
832 let wrapper = get_store_object(object, previous_transaction_checkpoint);
833
834 let mut wb = self.objects.batch();
835 wb.insert_batch(
836 &self.objects,
837 std::iter::once((ObjectKey::from(object_reference), wrapper)),
838 )?;
839 wb.write()?;
840 Ok(())
841 }
842}
843
844impl ObjectStore for AuthorityPerpetualTables {
845 fn try_get_object(
847 &self,
848 object_id: &ObjectId,
849 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
850 let obj_entry = self
851 .objects
852 .safe_iter_with_prefix_reversed(object_id)
853 .next();
854
855 match obj_entry.transpose()? {
856 Some((ObjectKey(obj_id, version), obj)) if obj_id == *object_id => Ok(self
857 .object(&ObjectKey(obj_id, version), obj)
858 .map_err(iota_types::storage::error::Error::custom)?),
859 _ => Ok(None),
860 }
861 }
862
863 fn try_get_object_by_key(
864 &self,
865 object_id: &ObjectId,
866 version: VersionNumber,
867 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
868 Ok(self
869 .objects
870 .get(&ObjectKey(*object_id, version))
871 .map_err(iota_types::storage::error::Error::custom)?
872 .map(|object| self.object(&ObjectKey(*object_id, version), object))
873 .transpose()
874 .map_err(iota_types::storage::error::Error::custom)?
875 .flatten())
876 }
877}
878
879#[derive(Eq, PartialEq, Debug, Clone, Hash)]
888pub struct LiveObject {
889 pub object: Object,
890 pub previous_transaction_checkpoint: Option<CheckpointSequenceNumber>,
891}
892
893impl LiveObject {
894 pub fn object_id(&self) -> ObjectId {
895 self.object.id()
896 }
897
898 pub fn version(&self) -> Version {
899 self.object.version()
900 }
901
902 pub fn object_reference(&self) -> ObjectReference {
903 self.object.object_ref()
904 }
905}
906
907#[derive(Deserialize, Serialize)]
911pub struct SnapshotLiveObject {
912 pub object: Object,
913 pub previous_transaction_checkpoint: CheckpointSequenceNumber,
914}
915
916impl From<SnapshotLiveObject> for LiveObject {
917 fn from(snap: SnapshotLiveObject) -> Self {
918 let SnapshotLiveObject {
919 object,
920 previous_transaction_checkpoint,
921 } = snap;
922 LiveObject {
923 object,
924 previous_transaction_checkpoint: Some(previous_transaction_checkpoint),
925 }
926 }
927}
928
929pub struct LiveSetIter<'a> {
932 iter: DbIterator<'a, (ObjectKey, StoreObjectWrapper)>,
933 tables: &'a AuthorityPerpetualTables,
934 prev: Option<(ObjectKey, StoreObjectWrapper)>,
935}
936
937impl LiveSetIter<'_> {
938 fn store_object_wrapper_to_live_object(
939 &self,
940 object_key: ObjectKey,
941 store_object: StoreObjectWrapper,
942 ) -> Option<LiveObject> {
943 match store_object.migrate().into_inner() {
944 StoreObject::Value(value) => {
945 let previous_transaction_checkpoint = value.previous_transaction_checkpoint;
946 let object = self
947 .tables
948 .construct_object(&object_key, *value)
949 .expect("Constructing object from store cannot fail");
950 Some(LiveObject {
951 object,
952 previous_transaction_checkpoint,
953 })
954 }
955 StoreObject::Wrapped | StoreObject::Deleted => None,
956 }
957 }
958}
959
960impl Iterator for LiveSetIter<'_> {
961 type Item = Result<LiveObject, TypedStoreError>;
962
963 fn next(&mut self) -> Option<Self::Item> {
964 loop {
965 match self.iter.next() {
966 Some(Ok((next_key, next_value))) => {
967 let prev = self.prev.take();
968 self.prev = Some((next_key, next_value));
969
970 if let Some((prev_key, prev_value)) = prev {
971 if prev_key.0 != next_key.0 {
972 if let Some(live_object) =
973 self.store_object_wrapper_to_live_object(prev_key, prev_value)
974 {
975 return Some(Ok(live_object));
976 }
977 }
978 }
979 }
980 Some(Err(err)) => {
981 self.prev = None;
984 return Some(Err(err));
985 }
986 None => {
987 if let Some((key, value)) = self.prev.take() {
988 if let Some(live_object) =
989 self.store_object_wrapper_to_live_object(key, value)
990 {
991 return Some(Ok(live_object));
992 }
993 }
994 return None;
995 }
996 }
997 }
998 }
999}
1000
1001fn live_owned_object_markers_table_config(db_options: DBOptions) -> DBOptions {
1003 DBOptions {
1004 options: db_options
1005 .clone()
1006 .optimize_for_write_throughput()
1007 .optimize_for_read(read_size_from_env(ENV_VAR_LOCKS_BLOCK_CACHE_SIZE).unwrap_or(1024))
1008 .options,
1009 rw_options: db_options.rw_options,
1010 }
1011}
1012
1013fn objects_table_config(db_options: DBOptions) -> DBOptions {
1014 db_options
1015 .optimize_for_write_throughput()
1016 .optimize_for_read(read_size_from_env(ENV_VAR_OBJECTS_BLOCK_CACHE_SIZE).unwrap_or(5 * 1024))
1017}
1018
1019fn transactions_table_config(db_options: DBOptions) -> DBOptions {
1020 db_options
1021 .optimize_for_write_throughput()
1022 .optimize_for_point_lookup(
1023 read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512),
1024 )
1025}
1026
1027fn effects_table_config(db_options: DBOptions) -> DBOptions {
1028 db_options
1029 .optimize_for_write_throughput()
1030 .optimize_for_point_lookup(
1031 read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
1032 )
1033}
1034
1035fn historic_ledger_db_options(db_options: &DBOptions) -> DBOptions {
1040 db_options.clone().set_block_options(
1041 read_size_from_env(ENV_VAR_TRANSACTIONS_BLOCK_CACHE_SIZE).unwrap_or(512)
1042 + read_size_from_env(ENV_VAR_EFFECTS_BLOCK_CACHE_SIZE).unwrap_or(1024),
1043 16 << 10,
1044 )
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::*;
1050 use crate::authority::authority_store_types::StoreObjectV2;
1051
1052 #[tokio::test]
1055 async fn a_db_snapshot_scan_is_unchanged_by_later_writes() {
1056 let tmp_dir = iota_common::tempdir();
1057 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1058
1059 let kept_id = ObjectId::random();
1060 let deleted_id = ObjectId::random();
1061 for id in [kept_id, deleted_id] {
1062 perpetual_db
1063 .insert_store_object_v2_test_only(
1064 Object::immutable_with_id_for_testing(id),
1065 Some(0),
1066 )
1067 .unwrap();
1068 }
1069 let live_set = |tables: &AuthorityPerpetualTables| -> Vec<(ObjectId, Version)> {
1070 tables
1071 .iter_live_object_set()
1072 .map(|live| live.map(|live| (live.object_id(), live.version())))
1073 .collect::<Result<_, _>>()
1074 .unwrap()
1075 };
1076 let before = live_set(&perpetual_db);
1077 assert_eq!(before.len(), 2);
1078
1079 let db_snapshot = perpetual_db.db_snapshot();
1080
1081 perpetual_db
1084 .insert_store_object_v2_test_only(
1085 Object::immutable_with_id_for_testing(ObjectId::random()),
1086 Some(1),
1087 )
1088 .unwrap();
1089 let mut wb = perpetual_db.objects.batch();
1090 wb.insert_batch(
1091 &perpetual_db.objects,
1092 std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1093 ObjectKey(deleted_id, Version::from_u64(2)),
1094 StoreObjectV2::Deleted.into(),
1095 )),
1096 )
1097 .unwrap();
1098 wb.write().unwrap();
1099
1100 let through_view: Vec<_> = perpetual_db
1101 .iter_live_object_set_at(&db_snapshot)
1102 .map(|live| live.map(|live| (live.object_id(), live.version())))
1103 .collect::<Result<_, _>>()
1104 .unwrap();
1105 assert_eq!(
1106 through_view, before,
1107 "the snapshot must not show writes made after it was taken"
1108 );
1109
1110 assert_ne!(
1113 live_set(&perpetual_db),
1114 before,
1115 "a scan outside the snapshot must show the later writes"
1116 );
1117 }
1118
1119 #[tokio::test]
1124 async fn live_set_iter_filters_wrapped_and_deleted_store_rows() {
1125 let tmp_dir = iota_common::tempdir();
1126 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1127
1128 let live_id = ObjectId::random();
1131 let wrapped_id = ObjectId::random();
1132 let deleted_id = ObjectId::random();
1133
1134 let live_object = Object::immutable_with_id_for_testing(live_id);
1135 perpetual_db
1136 .insert_store_object_v2_test_only(live_object, None)
1137 .unwrap();
1138
1139 let mut wb = perpetual_db.objects.batch();
1140 let wrapped_key = ObjectKey(wrapped_id, Version::from_u64(1));
1141 wb.insert_batch(
1142 &perpetual_db.objects,
1143 std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1144 wrapped_key,
1145 StoreObjectV2::Wrapped.into(),
1146 )),
1147 )
1148 .unwrap();
1149 let deleted_key = ObjectKey(deleted_id, Version::from_u64(1));
1150 wb.insert_batch(
1151 &perpetual_db.objects,
1152 std::iter::once::<(ObjectKey, StoreObjectWrapper)>((
1153 deleted_key,
1154 StoreObjectV2::Deleted.into(),
1155 )),
1156 )
1157 .unwrap();
1158 wb.write().unwrap();
1159
1160 let yielded: Vec<_> = perpetual_db
1161 .iter_live_object_set()
1162 .collect::<Result<_, _>>()
1163 .unwrap();
1164 assert_eq!(yielded.len(), 1, "wrapped/deleted rows must be filtered");
1165 assert_eq!(yielded[0].object.id(), live_id);
1166 }
1167
1168 #[tokio::test]
1177 async fn live_set_iter_propagates_previous_transaction_checkpoint() {
1178 let tmp_dir = iota_common::tempdir();
1179 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1180
1181 let object = Object::immutable_with_id_for_testing(ObjectId::random());
1183 let object_ref = object.object_ref();
1184 let object_key = ObjectKey::from(object_ref);
1185 let distinct_checkpoint: u64 = 0xCAFE_F00D_BEEF_1234;
1186
1187 let store_object_value =
1188 match get_store_object(object, Some(distinct_checkpoint)).into_inner() {
1189 StoreObject::Value(value) => value,
1190 other => panic!("expected StoreObject::Value, got {other:?}"),
1191 };
1192 let wrapper: StoreObjectWrapper = StoreObjectV2::Value(store_object_value).into();
1193 let mut wb = perpetual_db.objects.batch();
1194 wb.insert_batch(
1195 &perpetual_db.objects,
1196 std::iter::once((object_key, wrapper)),
1197 )
1198 .unwrap();
1199 wb.write().unwrap();
1200
1201 let yielded: Vec<_> = perpetual_db
1202 .iter_live_object_set()
1203 .collect::<Result<_, _>>()
1204 .unwrap();
1205 assert_eq!(yielded.len(), 1);
1206 assert_eq!(
1207 yielded[0].previous_transaction_checkpoint,
1208 Some(distinct_checkpoint),
1209 "LiveSetIter must surface the on-row checkpoint, not a default"
1210 );
1211 }
1212
1213 #[tokio::test]
1216 async fn live_set_iter_surfaces_read_errors() {
1217 let tmp_dir = iota_common::tempdir();
1218 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1219
1220 let object = Object::immutable_with_id_for_testing(ObjectId::random());
1221 let object_key = ObjectKey::from(object.object_ref());
1222 let rows = vec![
1223 Ok((object_key, get_store_object(object, Some(1)))),
1224 Err(TypedStoreError::RocksDB(
1225 "injected read failure".to_string(),
1226 )),
1227 ];
1228
1229 let yielded: Vec<_> = LiveSetIter {
1230 iter: Box::new(rows.into_iter()),
1231 tables: &perpetual_db,
1232 prev: None,
1233 }
1234 .collect();
1235
1236 assert_eq!(yielded.len(), 1);
1237 assert!(
1238 yielded[0].is_err(),
1239 "a failed read must not be reported as a complete scan"
1240 );
1241 }
1242
1243 #[tokio::test]
1249 async fn get_latest_object_or_tombstone_migrates_legacy_v1_row() {
1250 let tmp_dir = iota_common::tempdir();
1251 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1252
1253 let object_id = ObjectId::random();
1254 let object = Object::immutable_with_id_for_testing(object_id);
1255 let object_ref = object.object_ref();
1256 perpetual_db
1257 .insert_store_object_v1_test_only(object)
1258 .unwrap();
1259
1260 let (object_key, wrapper) = perpetual_db
1261 .get_latest_object_or_tombstone(object_id)
1262 .unwrap()
1263 .expect("row must be found");
1264 assert!(
1265 matches!(wrapper, StoreObjectWrapper::V2(_)),
1266 "read boundary must migrate the V1 row to V2"
1267 );
1268
1269 assert!(
1271 perpetual_db
1272 .tombstone_reference(&object_key, &wrapper)
1273 .unwrap()
1274 .is_none(),
1275 "a live value is not a tombstone"
1276 );
1277 let reconstructed = perpetual_db
1278 .object(&object_key, wrapper)
1279 .unwrap()
1280 .expect("value must reconstruct");
1281 assert_eq!(reconstructed.object_ref(), object_ref);
1282 }
1283
1284 fn open_with_pruner_watermark(
1286 dir: &Path,
1287 watermark: Option<CheckpointSequenceNumber>,
1288 ) -> AuthorityPerpetualTables {
1289 let db = AuthorityPerpetualTables::open(dir, None);
1290 db.objects
1291 .db
1292 .create_cf(
1293 OBJECTS_PRUNER_WATERMARK_CF,
1294 &typed_store::rocksdb::Options::default(),
1295 )
1296 .unwrap();
1297 if let Some(watermark) = watermark {
1298 DBMap::<(), CheckpointSequenceNumber>::reopen(
1299 &db.objects.db,
1300 Some(OBJECTS_PRUNER_WATERMARK_CF),
1301 &ReadWriteOptions::default(),
1302 true,
1303 )
1304 .unwrap()
1305 .insert(&(), &watermark)
1306 .unwrap();
1307 }
1308 db
1309 }
1310
1311 #[tokio::test]
1316 async fn the_objects_pruner_watermark_is_read_in_place_until_retired() {
1317 let tmp_dir = iota_common::tempdir();
1318 let db = open_with_pruner_watermark(tmp_dir.path(), Some(4_242));
1319 drop(db);
1320 let db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1321
1322 assert!(
1323 db.objects
1324 .db
1325 .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1326 .is_some()
1327 );
1328 assert_eq!(db.objects_pruner_watermark().unwrap(), Some(4_242));
1329 assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), None);
1330
1331 db.retire_objects_pruner_watermark().unwrap();
1332
1333 assert!(
1334 db.objects
1335 .db
1336 .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1337 .is_none()
1338 );
1339 assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), Some(4_242));
1340 assert_eq!(db.objects_pruner_watermark().unwrap(), Some(4_242));
1341 db.retire_objects_pruner_watermark()
1342 .expect("retiring again does nothing");
1343 }
1344
1345 #[tokio::test]
1348 async fn no_watermark_is_carried_over_when_the_pruner_never_ran() {
1349 let tmp_dir = iota_common::tempdir();
1350 let db = open_with_pruner_watermark(tmp_dir.path(), None);
1351
1352 db.retire_objects_pruner_watermark().unwrap();
1353
1354 assert_eq!(db.object_backlog_sweep_bound.get(&()).unwrap(), None);
1355 assert!(
1356 db.objects
1357 .db
1358 .cf_handle(OBJECTS_PRUNER_WATERMARK_CF)
1359 .is_none()
1360 );
1361 }
1362
1363 #[tokio::test]
1364 async fn mark_object_backlog_swept_records_done() {
1365 let tmp_dir = iota_common::tempdir();
1366 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1367
1368 assert_eq!(
1369 perpetual_db.object_backlog_sweep_progress.get(&()).unwrap(),
1370 None
1371 );
1372
1373 perpetual_db.mark_object_backlog_swept().unwrap();
1374
1375 assert_eq!(
1376 perpetual_db.object_backlog_sweep_progress.get(&()).unwrap(),
1377 Some(ObjectBacklogSweepProgress::Done)
1378 );
1379 }
1380
1381 #[tokio::test]
1384 async fn only_a_finished_sweep_marks_the_backlog_swept_at_open() {
1385 for (skip, swept) in [(true, false), (false, true)] {
1386 let tmp_dir = iota_common::tempdir();
1387 {
1388 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1389 if skip {
1390 perpetual_db.mark_object_backlog_sweep_skipped().unwrap();
1391 } else {
1392 perpetual_db.mark_object_backlog_swept().unwrap();
1393 }
1394 let weak_db = perpetual_db.objects.db.close_handle();
1395 drop(perpetual_db);
1396 assert!(typed_store::database::wait_for_database_close(weak_db).await);
1397 }
1398 let (_perpetual_db, historic_objects, ..) =
1399 AuthorityPerpetualTables::open_with_historic_objects(tmp_dir.path(), None).unwrap();
1400 assert_eq!(historic_objects.backlog_swept(), swept, "skipped: {skip}");
1401 }
1402 }
1403
1404 #[cfg(not(target_env = "msvc"))]
1407 #[tokio::test]
1408 async fn compact_reclaims_the_space_of_deleted_object_versions() {
1409 fn sst_size(path: &Path) -> u64 {
1410 let mut size = 0;
1411 for entry in std::fs::read_dir(path).unwrap() {
1412 let path = entry.unwrap().path();
1413 if path.extension().is_some_and(|ext| ext == "sst") {
1414 size += std::fs::metadata(path).unwrap().len();
1415 }
1416 }
1417 size
1418 }
1419
1420 let tmp_dir = iota_common::tempdir();
1421 let perpetual_db = AuthorityPerpetualTables::open(tmp_dir.path(), None);
1422 let total_unique_object_ids = 10_000;
1423 let num_versions_per_object = 10;
1424 let mut id = ObjectId::ZERO;
1425 let mut to_delete = vec![];
1426 for _ in 0..total_unique_object_ids {
1427 for i in (0..num_versions_per_object).rev() {
1428 if i < num_versions_per_object - 2 {
1429 to_delete.push(ObjectKey(id, Version::from(i)));
1430 }
1431 let object = get_store_object(Object::immutable_with_id_for_testing(id), None);
1432 perpetual_db
1433 .objects
1434 .insert(&ObjectKey(id, Version::from(i)), &object)
1435 .unwrap();
1436 }
1437 id = id.next_lexicographical();
1438 }
1439
1440 let db_path = tmp_dir.path().join("perpetual");
1441 perpetual_db.compact().unwrap();
1442 let before_compaction_size = sst_size(&db_path);
1443
1444 let mut batch = perpetual_db.objects.batch();
1445 batch
1446 .delete_batch(&perpetual_db.objects, to_delete.into_iter())
1447 .unwrap();
1448 batch.write().unwrap();
1449
1450 perpetual_db.compact().unwrap();
1451 let after_compaction_size = sst_size(&db_path);
1452
1453 more_asserts::assert_lt!(after_compaction_size, before_compaction_size);
1454 }
1455
1456 #[tokio::test]
1459 async fn periodic_compaction_passes_over_the_historic_bucket_files() {
1460 let tmp_dir = iota_common::tempdir();
1461 let (perpetual_db, historic_objects, _historic_ledger, _epoch_markers) =
1462 AuthorityPerpetualTables::open_with_historic_objects(tmp_dir.path(), None).unwrap();
1463 let object = Object::immutable_with_id_for_testing(ObjectId::random());
1464 let bucket = historic_objects.ensure(1).unwrap();
1465 let mut batch = perpetual_db.objects.batch();
1466 batch
1467 .insert_batch_tagged(
1468 &bucket.objects,
1469 [(ObjectKey(object.id(), object.version()), object)],
1470 )
1471 .unwrap();
1472 batch.write().unwrap();
1473 perpetual_db.objects.flush_all().unwrap();
1474
1475 perpetual_db
1476 .compact_next_sst_file(0, &Mutex::new(HashMap::new()))
1477 .unwrap();
1478 }
1479}