1use std::collections::HashMap;
92use std::num::NonZeroUsize;
93use std::ops::Bound;
94use std::sync::Arc;
95use std::sync::Mutex;
96
97use lru::LruCache;
98
99use anyhow::Context as _;
100use prometheus::IntCounter;
101use prometheus::IntGauge;
102use prometheus::Registry;
103use prometheus::register_int_counter_with_registry;
104use prometheus::register_int_gauge_with_registry;
105use sui_consistent_store::Batch;
106use sui_consistent_store::Db;
107use sui_consistent_store::FrameworkSchema;
108use sui_consistent_store::PipelineTaskKey;
109use sui_indexer_alt_framework::service::Service;
110use sui_types::base_types::ObjectID;
111use sui_types::effects::TransactionEffects;
112use sui_types::effects::TransactionEffectsAPI;
113use sui_types::message_envelope::Message;
114use tokio::time::MissedTickBehavior;
115use tracing::debug;
116use tracing::info;
117use tracing::warn;
118
119use crate::RpcStoreSchema;
120use crate::config::PrunerConfig;
121use crate::indexer::Store;
122use crate::indexer::restore::HISTORY_COHORT;
123use crate::indexer::restore::LIVE_COHORT;
124use crate::schema::checkpoint_seq_by_digest;
125use crate::schema::event_bitmap;
126use crate::schema::object_version_by_checkpoint;
127use crate::schema::objects;
128use crate::schema::primitives::U64Be;
129use crate::schema::pruning_watermark;
130use crate::schema::pruning_watermark::Watermarks;
131use crate::schema::transaction_bitmap;
132use crate::schema::tx_seq_by_digest;
133
134pub struct PrunerMetrics {
136 pub checkpoint_lo: IntGauge,
139 pub tx_seq_lo: IntGauge,
142 pub chunks_committed: IntCounter,
144 pub objects_deleted: IntCounter,
146}
147
148impl PrunerMetrics {
149 pub fn new(prefix: Option<&str>, registry: &Registry) -> Arc<Self> {
150 let prefix = prefix.unwrap_or("rpc_store_pruner");
151 let name = |n| format!("{prefix}_{n}");
152
153 Arc::new(Self {
154 checkpoint_lo: register_int_gauge_with_registry!(
155 name("checkpoint_lo"),
156 "Lowest still-available checkpoint sequence number (pruning floor)",
157 registry,
158 )
159 .unwrap(),
160 tx_seq_lo: register_int_gauge_with_registry!(
161 name("tx_seq_lo"),
162 "Lowest still-available transaction sequence number (pruning floor)",
163 registry,
164 )
165 .unwrap(),
166 chunks_committed: register_int_counter_with_registry!(
167 name("chunks_committed"),
168 "Total pruning chunks committed",
169 registry,
170 )
171 .unwrap(),
172 objects_deleted: register_int_counter_with_registry!(
173 name("objects_deleted"),
174 "Total superseded object versions and tombstones deleted by the pruner",
175 registry,
176 )
177 .unwrap(),
178 })
179 }
180}
181
182pub const DEFAULT_RETRACTION_CURSORS_CAPACITY: usize = 200_000;
184
185#[derive(Debug)]
190pub struct RetractionCursors {
191 cache: LruCache<ObjectID, u64>,
192}
193
194impl Default for RetractionCursors {
195 fn default() -> Self {
196 Self::with_capacity(DEFAULT_RETRACTION_CURSORS_CAPACITY)
197 }
198}
199
200impl RetractionCursors {
201 pub fn new() -> Self {
202 Self::default()
203 }
204
205 pub fn with_capacity(capacity: usize) -> Self {
206 let capacity = NonZeroUsize::new(capacity.max(1)).expect("capacity is at least 1");
207 Self {
208 cache: LruCache::new(capacity),
209 }
210 }
211
212 pub fn lower_bound(&self, id: &ObjectID) -> u64 {
215 self.cache.peek(id).copied().unwrap_or(0)
216 }
217
218 pub fn advance(&mut self, id: ObjectID, cp: u64) {
220 if let Some(existing) = self.cache.get_mut(&id) {
221 *existing = (*existing).max(cp);
222 } else {
223 self.cache.put(id, cp);
224 }
225 }
226
227 pub fn len(&self) -> usize {
228 self.cache.len()
229 }
230
231 pub fn is_empty(&self) -> bool {
232 self.cache.is_empty()
233 }
234
235 pub fn cap(&self) -> usize {
236 self.cache.cap().get()
237 }
238}
239
240#[derive(Default)]
265struct Retractions(HashMap<ObjectID, (u64, bool)>);
266
267impl Retractions {
268 fn record(&mut self, id: ObjectID, cp: u64, removed: bool) {
269 self.0
270 .entry(id)
271 .and_modify(|(recorded_cp, recorded_removed)| {
272 if cp > *recorded_cp {
273 *recorded_cp = cp;
274 *recorded_removed = removed;
275 } else if cp == *recorded_cp {
276 *recorded_removed |= removed;
277 }
278 })
279 .or_insert((cp, removed));
280 }
281
282 fn record_written(&mut self, id: ObjectID, cp: u64) {
284 if let Some((recorded_cp, recorded_removed)) = self.0.get_mut(&id)
285 && *recorded_cp == cp
286 {
287 *recorded_removed = false;
288 }
289 }
290
291 fn stage(
292 &self,
293 batch: &mut Batch,
294 schema: &RpcStoreSchema,
295 cursors: &RetractionCursors,
296 ) -> anyhow::Result<()> {
297 for (&id, &(cp, removed)) in &self.0 {
298 let lo_cp = cursors.lower_bound(&id);
299 retract_object_version_by_checkpoint(batch, schema, id, lo_cp, cp, removed)?;
300 }
301 Ok(())
302 }
303
304 fn commit_to_cursors(self, cursors: &mut RetractionCursors) {
305 for (id, (cp, _removed)) in self.0 {
306 cursors.advance(id, cp);
307 }
308 }
309}
310
311pub fn start_pruner(
318 store: Store,
319 config: PrunerConfig,
320 metrics: Arc<PrunerMetrics>,
321) -> anyhow::Result<Service> {
322 anyhow::ensure!(
323 config.retention_epochs >= 1,
324 "PrunerConfig::retention_epochs must be >= 1; 0 would prune the current epoch",
325 );
326 anyhow::ensure!(
327 config.max_checkpoints_per_tick >= 1,
328 "PrunerConfig::max_checkpoints_per_tick must be >= 1; 0 would never make progress",
329 );
330
331 let cursors = Arc::new(Mutex::new(RetractionCursors::default()));
332
333 let service = Service::new().spawn_aborting(async move {
334 let mut ticker = tokio::time::interval(config.interval());
335 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
336
337 loop {
338 ticker.tick().await;
339
340 let store = store.clone();
341 let config = config.clone();
342 let metrics = metrics.clone();
343 let cursors = cursors.clone();
344
345 let res = tokio::task::spawn_blocking(move || {
348 let mut guard = cursors.lock().unwrap_or_else(|p| p.into_inner());
349 prune_once(store.db(), store.schema(), &mut guard, &config, &metrics)
350 })
351 .await;
352
353 match res {
354 Ok(Ok(())) => {}
355 Ok(Err(e)) => {
356 warn!("rpc-store pruner pass failed (will retry next interval): {e:#}")
357 }
358 Err(e) => {
359 if e.is_panic() {
360 std::panic::resume_unwind(e.into_panic());
361 } else {
362 panic!("rpc-store pruner task failed: {e}");
363 }
364 }
365 }
366 }
367 });
368
369 Ok(service)
370}
371
372fn prune_once(
375 db: &Db,
376 schema: &RpcStoreSchema,
377 retraction_cursors: &mut RetractionCursors,
378 config: &PrunerConfig,
379 metrics: &PrunerMetrics,
380) -> anyhow::Result<()> {
381 let Some(current_epoch) = current_committed_epoch(db)? else {
382 debug!("rpc-store pruner: no committed watermark yet; nothing to prune");
383 return Ok(());
384 };
385
386 let Some(retention_lo) =
387 retention_checkpoint_floor(schema, current_epoch, config.retention_epochs)?
388 else {
389 debug!(
390 current_epoch,
391 "rpc-store pruner: retention floor not yet reached; nothing to prune"
392 );
393 return Ok(());
394 };
395
396 let target_lo = clamp_to_snapshot(retention_lo, db.snapshot_range().map(|r| *r.start()));
398
399 let mut cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
400 if target_lo <= cursor.checkpoint_lo {
401 debug!(
402 target_lo,
403 current_lo = cursor.checkpoint_lo,
404 "rpc-store pruner: floor already at or beyond target"
405 );
406 return Ok(());
407 }
408
409 let tick_target = target_lo.min(cursor.checkpoint_lo + config.max_checkpoints_per_tick);
414
415 info!(
416 from = cursor.checkpoint_lo,
417 to = tick_target,
418 target = target_lo,
419 current_epoch,
420 "rpc-store pruner: advancing floor"
421 );
422
423 while cursor.checkpoint_lo < tick_target {
424 let chunk_ckpt_hi = (cursor.checkpoint_lo + config.max_chunk_checkpoints).min(tick_target);
425 cursor = prune_chunk(
426 db,
427 schema,
428 retraction_cursors,
429 cursor,
430 chunk_ckpt_hi,
431 metrics,
432 )?;
433 metrics.checkpoint_lo.set(cursor.checkpoint_lo as i64);
434 metrics.tx_seq_lo.set(cursor.tx_seq_lo as i64);
435 metrics.chunks_committed.inc();
436 }
437
438 if cursor.checkpoint_lo >= target_lo {
445 db.compact_range_cf(transaction_bitmap::NAME, None, None)
446 .context("Compacting transaction_bitmap after prune")?;
447 db.compact_range_cf(event_bitmap::NAME, None, None)
448 .context("Compacting event_bitmap after prune")?;
449 }
450
451 Ok(())
452}
453
454fn prune_chunk(
457 db: &Db,
458 schema: &RpcStoreSchema,
459 retraction_cursors: &mut RetractionCursors,
460 cursor: Watermarks,
461 chunk_ckpt_hi: u64,
462 metrics: &PrunerMetrics,
463) -> anyhow::Result<Watermarks> {
464 let ckpt_lo = cursor.checkpoint_lo;
465 let tx_lo = cursor.tx_seq_lo;
466
467 let last_ckpt = chunk_ckpt_hi - 1;
474 let tx_hi = schema
475 .get_checkpoint_summary(last_ckpt)?
476 .with_context(|| format!("checkpoint_summary missing for checkpoint {last_ckpt}"))?
477 .data()
478 .network_total_transactions;
479
480 let mut batch = db.batch();
481 let mut retractions = Retractions::default();
482 let mut objects_deleted: u64 = 0;
483 let mut tx_cursor = tx_lo;
492 for seq in ckpt_lo..chunk_ckpt_hi {
493 let summary = schema
498 .get_checkpoint_summary(seq)?
499 .with_context(|| format!("checkpoint_summary missing for checkpoint {seq}"))?;
500 let ckpt_tx_hi = summary.data().network_total_transactions;
501
502 for tx_seq in tx_cursor..ckpt_tx_hi {
503 let Some((effects, _unchanged)) = schema.get_effects(tx_seq)? else {
504 continue;
505 };
506 for (id, version) in effects.modified_at_versions() {
507 batch.delete(&schema.objects, &objects::Key { id, version })?;
508 retractions.record(id, seq, false);
513 objects_deleted += 1;
514 }
515 for (id, version) in effects.all_tombstones() {
516 batch.delete(&schema.objects, &objects::Key { id, version })?;
517 retractions.record(id, seq, true);
520 objects_deleted += 1;
521 }
522 for (oref, _, _) in effects.all_changed_objects() {
523 retractions.record_written(oref.0, seq);
524 }
525 batch.delete(
526 &schema.tx_seq_by_digest,
527 &tx_seq_by_digest::Key(*effects.transaction_digest()),
528 )?;
529 }
530 tx_cursor = ckpt_tx_hi;
531
532 batch.delete(
534 &schema.checkpoint_seq_by_digest,
535 &checkpoint_seq_by_digest::Key(summary.data().digest()),
536 )?;
537 }
538
539 batch.delete_range(&schema.transactions, &U64Be(tx_lo), &U64Be(tx_hi))?;
543 batch.delete_range(&schema.effects, &U64Be(tx_lo), &U64Be(tx_hi))?;
544 batch.delete_range(&schema.events, &U64Be(tx_lo), &U64Be(tx_hi))?;
545 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
546 batch.delete_range(
547 &schema.checkpoint_summary,
548 &U64Be(ckpt_lo),
549 &U64Be(chunk_ckpt_hi),
550 )?;
551 batch.delete_range(
552 &schema.checkpoint_contents,
553 &U64Be(ckpt_lo),
554 &U64Be(chunk_ckpt_hi),
555 )?;
556
557 retractions.stage(&mut batch, schema, retraction_cursors)?;
558
559 let new = Watermarks {
561 tx_seq_lo: tx_hi,
562 checkpoint_lo: chunk_ckpt_hi,
563 };
564 let (k, v) = pruning_watermark::store(&new);
565 batch.put(&schema.pruning_watermark, &k, &v)?;
566
567 batch.commit()?;
568
569 retractions.commit_to_cursors(retraction_cursors);
570
571 schema.set_pruning_floor(new.tx_seq_lo);
574 metrics.objects_deleted.inc_by(objects_deleted);
575
576 Ok(new)
577}
578
579fn retract_object_version_by_checkpoint(
622 batch: &mut Batch,
623 schema: &RpcStoreSchema,
624 id: ObjectID,
625 lo_cp: u64,
626 cp: u64,
627 removed: bool,
628) -> anyhow::Result<()> {
629 if lo_cp < cp {
630 let lo = object_version_by_checkpoint::Key {
631 id,
632 checkpoint: lo_cp,
633 };
634 let hi = object_version_by_checkpoint::Key { id, checkpoint: cp };
635 for entry in schema
636 .object_version_by_checkpoint
637 .iter((Bound::Included(lo), Bound::Excluded(hi)))?
638 {
639 let (key, _value) = entry?;
640 batch.delete(&schema.object_version_by_checkpoint, &key)?;
641 }
642 }
643 if removed {
644 let hi = object_version_by_checkpoint::Key { id, checkpoint: cp };
645 batch.delete(&schema.object_version_by_checkpoint, &hi)?;
646 }
647 Ok(())
648}
649
650pub fn prune_history_cohort(
699 db: &Db,
700 schema: &RpcStoreSchema,
701 retraction_cursors: &mut RetractionCursors,
702 pruned_checkpoint_watermark: u64,
703 pruned_tx_seq_exclusive: u64,
704 effects: &[(u64, TransactionEffects)],
705) -> anyhow::Result<()> {
706 let cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
707 let tx_lo = cursor.tx_seq_lo;
708 let tx_hi = pruned_tx_seq_exclusive;
709 let checkpoint_lo = pruned_checkpoint_watermark.saturating_add(1);
712
713 if tx_hi <= tx_lo && checkpoint_lo <= cursor.checkpoint_lo {
716 return Ok(());
717 }
718
719 let mut batch = db.batch();
720 let mut retractions = Retractions::default();
721 for entry in schema.iter_tx_seq_digests(tx_lo, tx_hi)? {
727 let (_tx_seq, digest) = entry?;
728 batch.delete(&schema.tx_seq_by_digest, &tx_seq_by_digest::Key(digest))?;
729 }
730 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
731
732 for (checkpoint, effects) in effects {
743 for (id, _version) in effects.modified_at_versions() {
744 retractions.record(id, *checkpoint, false);
745 }
746 for (id, _version) in effects.all_tombstones() {
747 retractions.record(id, *checkpoint, true);
748 }
749 for (oref, _, _) in effects.all_changed_objects() {
750 retractions.record_written(oref.0, *checkpoint);
751 }
752 }
753 retractions.stage(&mut batch, schema, retraction_cursors)?;
754
755 let new = Watermarks {
759 tx_seq_lo: tx_hi.max(tx_lo),
760 checkpoint_lo: checkpoint_lo.max(cursor.checkpoint_lo),
761 };
762 let (k, v) = pruning_watermark::store(&new);
763 batch.put(&schema.pruning_watermark, &k, &v)?;
764 batch.commit()?;
765
766 retractions.commit_to_cursors(retraction_cursors);
767
768 schema.set_pruning_floor(new.tx_seq_lo);
774
775 Ok(())
776}
777
778pub fn embedded_prunable_checkpoint(db: &Db) -> anyhow::Result<Option<u64>> {
800 let framework = db.framework();
801 let mut min_hi: Option<u64> = None;
802 for name in LIVE_COHORT.iter().chain(HISTORY_COHORT) {
803 let key = PipelineTaskKey::new(*name);
804 let Some(watermark) = framework
805 .watermarks
806 .get(&key)
807 .with_context(|| format!("reading watermark for {name}"))?
808 else {
809 return Ok(None);
810 };
811 let hi = watermark.checkpoint_hi_inclusive;
812 min_hi = Some(min_hi.map_or(hi, |m| m.min(hi)));
813 }
814 Ok(min_hi)
815}
816
817fn current_committed_epoch(db: &Db) -> anyhow::Result<Option<u64>> {
825 let framework = FrameworkSchema::new(db.clone());
826 let mut min_epoch: Option<u64> = None;
827 for entry in framework.watermarks.iter(..)? {
828 let (_, watermark) = entry?;
829 let epoch = watermark.epoch_hi_inclusive;
830 min_epoch = Some(min_epoch.map_or(epoch, |m| m.min(epoch)));
831 }
832 Ok(min_epoch)
833}
834
835fn retention_checkpoint_floor(
842 schema: &RpcStoreSchema,
843 current_epoch: u64,
844 retention_epochs: u64,
845) -> anyhow::Result<Option<u64>> {
846 debug_assert!(retention_epochs >= 1, "validated in start_pruner");
847
848 let oldest_retained = current_epoch.saturating_sub(retention_epochs - 1);
850 if oldest_retained == 0 {
851 return Ok(None);
853 }
854
855 let Some(info) = schema.get_epoch(oldest_retained)? else {
856 return Ok(None);
857 };
858 Ok(info.start_checkpoint)
859}
860
861fn clamp_to_snapshot(retention_lo: u64, oldest_snapshot: Option<u64>) -> u64 {
868 match oldest_snapshot {
869 Some(snap) => retention_lo.min(snap),
870 None => retention_lo,
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use std::sync::Arc;
877
878 use prometheus::Registry;
879 use sui_consistent_store::Db;
880 use sui_consistent_store::DbOptions;
881 use sui_consistent_store::PipelineTaskKey;
882 use sui_consistent_store::Watermark;
883 use sui_indexer_alt_framework::pipeline::Processor;
884 use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
885
886 use super::*;
887 use crate::schema::epochs;
888 use crate::schema::primitives::U64Varint;
889
890 fn fresh_db() -> (tempfile::TempDir, Db, RpcStoreSchema) {
891 let dir = tempfile::tempdir().unwrap();
892 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
893 (dir, db, schema)
894 }
895
896 fn stamp_watermarks(db: &Db, names: &[&str], hi: u64) {
898 let framework = FrameworkSchema::new(db.clone());
899 let mut batch = db.batch();
900 for name in names {
901 batch
902 .put(
903 &framework.watermarks,
904 &PipelineTaskKey::new(*name),
905 &Watermark::for_checkpoint(hi),
906 )
907 .unwrap();
908 }
909 batch.commit().unwrap();
910 }
911
912 #[test]
916 fn embedded_prunable_checkpoint_is_min_across_cohorts() {
917 let (_dir, db, _schema) = fresh_db();
918
919 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
921
922 stamp_watermarks(&db, LIVE_COHORT, 1_000);
926 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
927
928 stamp_watermarks(&db, HISTORY_COHORT, 40);
931 stamp_watermarks(&db, &[HISTORY_COHORT[0]], 25);
932 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(25));
933
934 stamp_watermarks(&db, HISTORY_COHORT, 2_000);
937 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(1_000));
938 }
939
940 async fn seed(
947 db: &Db,
948 schema: &RpcStoreSchema,
949 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
950 ) {
951 let mut batch = db.batch();
952 for row in crate::indexer::objects::Objects
953 .process(checkpoint)
954 .await
955 .unwrap()
956 {
957 batch
958 .put(
959 &schema.objects,
960 &objects::Key {
961 id: row.id,
962 version: row.version,
963 },
964 &row.value,
965 )
966 .unwrap();
967 }
968 for row in crate::indexer::effects::Effects
969 .process(checkpoint)
970 .await
971 .unwrap()
972 {
973 batch
974 .put(&schema.effects, &U64Be(row.tx_seq), &row.value)
975 .unwrap();
976 }
977 for row in crate::indexer::checkpoint_summary::CheckpointSummary
978 .process(checkpoint)
979 .await
980 .unwrap()
981 {
982 batch
983 .put(&schema.checkpoint_summary, &U64Be(row.seq), &row.value)
984 .unwrap();
985 }
986 for row in crate::indexer::tx_seq_by_digest::TxSeqByDigest
987 .process(checkpoint)
988 .await
989 .unwrap()
990 {
991 batch
992 .put(
993 &schema.tx_seq_by_digest,
994 &tx_seq_by_digest::Key(row.digest),
995 &U64Varint(row.tx_seq),
996 )
997 .unwrap();
998 }
999 for row in crate::indexer::checkpoint_seq_by_digest::CheckpointSeqByDigest
1000 .process(checkpoint)
1001 .await
1002 .unwrap()
1003 {
1004 batch
1005 .put(
1006 &schema.checkpoint_seq_by_digest,
1007 &checkpoint_seq_by_digest::Key(row.digest),
1008 &U64Varint(row.seq),
1009 )
1010 .unwrap();
1011 }
1012 batch.commit().unwrap();
1013 }
1014
1015 async fn seed_object_version_by_checkpoint(
1018 db: &Db,
1019 schema: &RpcStoreSchema,
1020 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
1021 ) {
1022 use crate::indexer::object_version_by_checkpoint::ObjectVersionByCheckpoint;
1023 use crate::indexer::object_version_by_checkpoint::Row;
1024
1025 let mut batch = db.batch();
1026 for row in ObjectVersionByCheckpoint::default()
1027 .process(checkpoint)
1028 .await
1029 .unwrap()
1030 {
1031 let Row::Change {
1032 id,
1033 checkpoint,
1034 version,
1035 } = row
1036 else {
1037 continue;
1038 };
1039 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, version);
1040 batch
1041 .put(&schema.object_version_by_checkpoint, &k, &v)
1042 .unwrap();
1043 }
1044 batch.commit().unwrap();
1045 }
1046
1047 fn removed_and_recreated_checkpoints() -> (
1050 Arc<sui_types::full_checkpoint_content::Checkpoint>,
1051 Arc<sui_types::full_checkpoint_content::Checkpoint>,
1052 ) {
1053 let mut builder = TestCheckpointBuilder::new(0)
1054 .start_transaction(0)
1055 .create_owned_object(0)
1056 .create_owned_object(1)
1057 .finish_transaction();
1058 let cp0 = Arc::new(builder.build_checkpoint());
1059 builder = builder
1060 .start_transaction(0)
1061 .delete_object(0)
1062 .finish_transaction()
1063 .start_transaction(0)
1064 .create_owned_object(0)
1065 .finish_transaction()
1066 .start_transaction(0)
1067 .wrap_object(1)
1068 .finish_transaction()
1069 .start_transaction(0)
1070 .unwrap_object(1)
1071 .finish_transaction();
1072 let cp1 = Arc::new(builder.build_checkpoint());
1073 (cp0, cp1)
1074 }
1075
1076 fn seed_checkpoint_versions(
1077 db: &Db,
1078 schema: &RpcStoreSchema,
1079 id: ObjectID,
1080 rows: &[(u64, u64)],
1081 ) {
1082 let mut batch = db.batch();
1083 for &(checkpoint, version) in rows {
1084 let (k, v) = object_version_by_checkpoint::store(
1085 id,
1086 checkpoint,
1087 sui_types::base_types::SequenceNumber::from_u64(version),
1088 );
1089 batch
1090 .put(&schema.object_version_by_checkpoint, &k, &v)
1091 .unwrap();
1092 }
1093 batch.commit().unwrap();
1094 }
1095
1096 #[test]
1097 fn retractions_stage_widest_range_per_object() {
1098 let (_dir, db, schema) = fresh_db();
1099 let obj = TestCheckpointBuilder::derive_object_id(0);
1100 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (1, 2), (2, 3)]);
1101
1102 let mut retractions = Retractions::default();
1103 retractions.record(obj, 1, true);
1104 retractions.record(obj, 2, false);
1105 let mut batch = db.batch();
1106 retractions
1107 .stage(&mut batch, &schema, &RetractionCursors::default())
1108 .unwrap();
1109 batch.commit().unwrap();
1110
1111 assert_eq!(
1112 schema.get_object_version_at_checkpoint(obj, 1).unwrap(),
1113 None,
1114 "the widest range must cover lower-checkpoint removals",
1115 );
1116 assert_eq!(
1117 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1118 Some(sui_types::base_types::SequenceNumber::from_u64(3)),
1119 "a lower removed=true retraction must not delete the latest anchor",
1120 );
1121 }
1122
1123 #[test]
1124 fn retractions_or_removed_on_equal_checkpoint() {
1125 let (_dir, db, schema) = fresh_db();
1126 let obj = TestCheckpointBuilder::derive_object_id(0);
1127 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (2, 3)]);
1128
1129 let mut retractions = Retractions::default();
1130 retractions.record(obj, 2, false);
1131 retractions.record(obj, 2, true);
1132 let mut batch = db.batch();
1133 retractions
1134 .stage(&mut batch, &schema, &RetractionCursors::default())
1135 .unwrap();
1136 batch.commit().unwrap();
1137
1138 assert_eq!(
1139 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1140 None,
1141 "same-checkpoint removals must drop the checkpoint row",
1142 );
1143 }
1144
1145 #[test]
1146 fn retractions_write_after_removal_keeps_checkpoint_row() {
1147 let (_dir, db, schema) = fresh_db();
1148 let obj = TestCheckpointBuilder::derive_object_id(0);
1149 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (2, 3)]);
1150
1151 let mut retractions = Retractions::default();
1152 retractions.record(obj, 2, false);
1153 retractions.record(obj, 2, true);
1154 retractions.record_written(obj, 2);
1155 let mut batch = db.batch();
1156 retractions
1157 .stage(&mut batch, &schema, &RetractionCursors::default())
1158 .unwrap();
1159 batch.commit().unwrap();
1160
1161 assert_eq!(
1162 schema.get_object_version_at_checkpoint(obj, 0).unwrap(),
1163 None,
1164 );
1165 assert_eq!(
1166 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1167 Some(sui_types::base_types::SequenceNumber::from_u64(3)),
1168 "a write after a same-checkpoint removal must keep the checkpoint row",
1169 );
1170 }
1171
1172 #[test]
1173 fn retractions_write_at_other_checkpoint_does_not_clear_removal() {
1174 let (_dir, db, schema) = fresh_db();
1175 let obj = TestCheckpointBuilder::derive_object_id(0);
1176 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (2, 3)]);
1177
1178 let mut retractions = Retractions::default();
1179 retractions.record(obj, 2, true);
1180 retractions.record_written(obj, 1);
1181 let mut batch = db.batch();
1182 retractions
1183 .stage(&mut batch, &schema, &RetractionCursors::default())
1184 .unwrap();
1185 batch.commit().unwrap();
1186
1187 assert_eq!(
1188 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1189 None,
1190 );
1191 }
1192
1193 #[test]
1200 fn retraction_point_deletes_deep_history() {
1201 let (_dir, db, schema) = fresh_db();
1202 let obj = TestCheckpointBuilder::derive_object_id(0);
1203 let neighbor = TestCheckpointBuilder::derive_object_id(1);
1204
1205 let rows: Vec<(u64, u64)> = (0..1_000u64).map(|c| (c, c + 1)).collect();
1209 seed_checkpoint_versions(&db, &schema, obj, &rows);
1210 seed_checkpoint_versions(&db, &schema, neighbor, &[(10, 42)]);
1211
1212 let mut retractions = Retractions::default();
1216 for (checkpoint, _) in &rows {
1217 retractions.record(obj, *checkpoint, false);
1218 }
1219 let mut batch = db.batch();
1220 retractions
1221 .stage(&mut batch, &schema, &RetractionCursors::default())
1222 .unwrap();
1223 batch.commit().unwrap();
1224
1225 assert_eq!(
1228 schema.get_object_version_at_checkpoint(obj, 998).unwrap(),
1229 None,
1230 "history below the coalesced checkpoint must be fully point-deleted",
1231 );
1232 assert_eq!(
1233 schema.get_object_version_at_checkpoint(obj, 999).unwrap(),
1234 Some(sui_types::base_types::SequenceNumber::from_u64(1_000)),
1235 "the anchor at the coalesced checkpoint must survive",
1236 );
1237 let remaining: Vec<u64> = schema
1238 .iter_object_versions_by_checkpoint(obj)
1239 .unwrap()
1240 .map(|r| r.unwrap().0.checkpoint)
1241 .collect();
1242 assert_eq!(remaining, vec![999], "only the anchor row remains");
1243
1244 assert_eq!(
1246 schema
1247 .get_object_version_at_checkpoint(neighbor, 10)
1248 .unwrap(),
1249 Some(sui_types::base_types::SequenceNumber::from_u64(42)),
1250 "the bounded scan must not delete a neighboring object's rows",
1251 );
1252 }
1253
1254 #[test]
1255 fn clamp_to_snapshot_holds_floor_at_or_below_oldest_snapshot() {
1256 assert_eq!(clamp_to_snapshot(100, None), 100);
1258 assert_eq!(clamp_to_snapshot(100, Some(250)), 100);
1260 assert_eq!(clamp_to_snapshot(300, Some(250)), 250);
1262 assert_eq!(clamp_to_snapshot(250, Some(250)), 250);
1264 }
1265
1266 #[test]
1267 fn retention_floor_none_when_chain_younger_than_window() {
1268 let (_dir, _db, schema) = fresh_db();
1269 assert!(retention_checkpoint_floor(&schema, 2, 5).unwrap().is_none());
1272 }
1273
1274 #[test]
1275 fn retention_floor_is_start_checkpoint_of_oldest_retained_epoch() {
1276 let (_dir, db, schema) = fresh_db();
1277 let mut batch = db.batch();
1279 batch
1280 .merge(
1281 &schema.epochs,
1282 &U64Be(3),
1283 &epochs::start(1, 1, 0, Some(300), None),
1284 )
1285 .unwrap();
1286 batch.commit().unwrap();
1287 assert_eq!(
1290 retention_checkpoint_floor(&schema, 5, 3).unwrap(),
1291 Some(300)
1292 );
1293 }
1294
1295 #[test]
1296 fn retention_floor_none_when_oldest_epoch_row_missing() {
1297 let (_dir, _db, schema) = fresh_db();
1298 assert!(
1300 retention_checkpoint_floor(&schema, 10, 2)
1301 .unwrap()
1302 .is_none()
1303 );
1304 }
1305
1306 #[test]
1307 fn current_committed_epoch_takes_min_across_watermarks() {
1308 let (_dir, db, _schema) = fresh_db();
1309 let framework = FrameworkSchema::new(db.clone());
1310 let mut batch = db.batch();
1311 batch
1312 .put(
1313 &framework.watermarks,
1314 &PipelineTaskKey::new("a"),
1315 &Watermark {
1316 epoch_hi_inclusive: 7,
1317 ..Default::default()
1318 },
1319 )
1320 .unwrap();
1321 batch
1322 .put(
1323 &framework.watermarks,
1324 &PipelineTaskKey::new("b"),
1325 &Watermark {
1326 epoch_hi_inclusive: 5,
1327 ..Default::default()
1328 },
1329 )
1330 .unwrap();
1331 batch.commit().unwrap();
1332 assert_eq!(current_committed_epoch(&db).unwrap(), Some(5));
1333 }
1334
1335 #[test]
1336 fn current_committed_epoch_none_when_no_watermarks() {
1337 let (_dir, db, _schema) = fresh_db();
1338 assert!(current_committed_epoch(&db).unwrap().is_none());
1339 }
1340
1341 #[test]
1345 fn merge_written_bitmap_buckets_require_two_compactions_for_reclamation() {
1346 let (_dir, db, schema) = fresh_db();
1347 let dimension = b"sender:alice".to_vec();
1348 let floor = transaction_bitmap::TX_BUCKET_SIZE;
1349 let retained_tx_seq = floor + 5;
1350
1351 let (tx_low_key, tx_low_value) = transaction_bitmap::store_match(dimension.clone(), 5);
1352 let (tx_high_key, tx_high_value) =
1353 transaction_bitmap::store_match(dimension.clone(), retained_tx_seq);
1354 let (event_low_key, event_low_value) = event_bitmap::store_match(dimension.clone(), 5, 0);
1355 let (event_high_key, event_high_value) =
1356 event_bitmap::store_match(dimension.clone(), retained_tx_seq, 0);
1357
1358 let mut batch = db.batch();
1359 batch
1360 .merge(&schema.transaction_bitmap, &tx_low_key, &tx_low_value)
1361 .unwrap();
1362 batch
1363 .merge(&schema.transaction_bitmap, &tx_high_key, &tx_high_value)
1364 .unwrap();
1365 batch
1366 .merge(&schema.event_bitmap, &event_low_key, &event_low_value)
1367 .unwrap();
1368 batch
1369 .merge(&schema.event_bitmap, &event_high_key, &event_high_value)
1370 .unwrap();
1371 batch.commit().unwrap();
1372 db.flush().unwrap();
1373
1374 let (watermark_key, watermark_value) = pruning_watermark::store(&Watermarks {
1375 tx_seq_lo: floor,
1376 checkpoint_lo: 1,
1377 });
1378 let mut batch = db.batch();
1379 batch
1380 .put(&schema.pruning_watermark, &watermark_key, &watermark_value)
1381 .unwrap();
1382 batch.commit().unwrap();
1383 schema.set_pruning_floor(floor);
1384
1385 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1386 .unwrap();
1387 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1388
1389 assert!(
1390 schema
1391 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1392 .unwrap()
1393 .is_some()
1394 );
1395 assert!(
1396 schema
1397 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1398 .unwrap()
1399 .is_some()
1400 );
1401 assert!(
1402 schema
1403 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1404 .unwrap()
1405 .is_some()
1406 );
1407 assert!(
1408 schema
1409 .get_event_bitmap(dimension.clone(), event_high_key.bucket)
1410 .unwrap()
1411 .is_some()
1412 );
1413
1414 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1415 .unwrap();
1416 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1417
1418 assert!(
1419 schema
1420 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1421 .unwrap()
1422 .is_none()
1423 );
1424 assert!(
1425 schema
1426 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1427 .unwrap()
1428 .is_some()
1429 );
1430 assert!(
1431 schema
1432 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1433 .unwrap()
1434 .is_none()
1435 );
1436 assert!(
1437 schema
1438 .get_event_bitmap(dimension, event_high_key.bucket)
1439 .unwrap()
1440 .is_some()
1441 );
1442 }
1443
1444 #[tokio::test]
1447 async fn prune_chunk_publishes_the_db_local_bitmap_floor() {
1448 let (_dir, db, schema) = fresh_db();
1449 let checkpoint = Arc::new(
1450 TestCheckpointBuilder::new(0)
1451 .start_transaction(0)
1452 .create_owned_object(0)
1453 .finish_transaction()
1454 .start_transaction(0)
1455 .transfer_object(0, 1)
1456 .finish_transaction()
1457 .build_checkpoint(),
1458 );
1459 seed(&db, &schema, &checkpoint).await;
1460
1461 let metrics = PrunerMetrics::new(None, &Registry::new());
1462 let new = prune_chunk(
1463 &db,
1464 &schema,
1465 &mut RetractionCursors::default(),
1466 Watermarks::default(),
1467 1,
1468 &metrics,
1469 )
1470 .unwrap();
1471 assert_eq!(
1472 schema.current_pruning_floor(),
1473 new.tx_seq_lo,
1474 "the chunk must publish its committed tx_seq floor",
1475 );
1476 }
1477
1478 #[test]
1479 fn start_pruner_rejects_zero_retention() {
1480 let (_dir, db, schema) = fresh_db();
1481 let store = Store::new(db, Arc::new(schema));
1482 let config = PrunerConfig {
1483 retention_epochs: 0,
1484 ..PrunerConfig::default()
1485 };
1486 let err =
1487 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1488 assert!(
1489 format!("{err:#}").contains("retention_epochs"),
1490 "expected a retention_epochs validation error, got: {err:#}",
1491 );
1492 }
1493
1494 #[test]
1495 fn start_pruner_rejects_zero_checkpoints_per_tick() {
1496 let (_dir, db, schema) = fresh_db();
1497 let store = Store::new(db, Arc::new(schema));
1498 let config = PrunerConfig {
1499 max_checkpoints_per_tick: 0,
1500 ..PrunerConfig::default()
1501 };
1502 let err =
1503 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1504 assert!(
1505 format!("{err:#}").contains("max_checkpoints_per_tick"),
1506 "expected a max_checkpoints_per_tick validation error, got: {err:#}",
1507 );
1508 }
1509
1510 #[tokio::test]
1518 async fn prune_once_advances_at_most_the_per_tick_budget() {
1519 let (_dir, db, schema) = fresh_db();
1520
1521 let mut builder = TestCheckpointBuilder::new(0);
1525 let mut checkpoints = Vec::new();
1526 for i in 0..5u64 {
1527 builder = builder
1528 .start_transaction(0)
1529 .create_owned_object(i)
1530 .finish_transaction();
1531 checkpoints.push(Arc::new(builder.build_checkpoint()));
1532 }
1533 for cp in &checkpoints {
1534 seed(&db, &schema, cp).await;
1535 }
1536
1537 let framework = FrameworkSchema::new(db.clone());
1542 let mut batch = db.batch();
1543 batch
1544 .put(
1545 &framework.watermarks,
1546 &PipelineTaskKey::new("p"),
1547 &Watermark {
1548 epoch_hi_inclusive: 2,
1549 ..Default::default()
1550 },
1551 )
1552 .unwrap();
1553 batch
1554 .merge(
1555 &schema.epochs,
1556 &U64Be(2),
1557 &epochs::start(1, 1, 0, Some(5), None),
1558 )
1559 .unwrap();
1560 batch.commit().unwrap();
1561
1562 let config = PrunerConfig {
1563 retention_epochs: 1,
1564 interval_ms: 1,
1565 max_chunk_checkpoints: 2,
1566 max_checkpoints_per_tick: 2,
1567 };
1568 let metrics = PrunerMetrics::new(None, &Registry::new());
1569
1570 let floor = |schema: &RpcStoreSchema| {
1571 schema
1572 .get_pruning_watermarks()
1573 .unwrap()
1574 .unwrap_or_default()
1575 .checkpoint_lo
1576 };
1577
1578 let mut cursors = RetractionCursors::default();
1580 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1581 assert_eq!(floor(&schema), 2, "first tick advances by the budget");
1582 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1583 assert_eq!(floor(&schema), 4, "second tick advances by the budget");
1584 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1585 assert_eq!(floor(&schema), 5, "third tick reaches the target");
1586 assert!(schema.get_effects(4).unwrap().is_none());
1589 assert!(schema.get_checkpoint_summary(4).unwrap().is_none());
1590 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1591 assert_eq!(floor(&schema), 5, "a pass at the target is a no-op");
1592 }
1593
1594 #[tokio::test]
1601 async fn prune_chunk_deletes_history_and_preserves_live_object() {
1602 let (_dir, db, schema) = fresh_db();
1603
1604 let checkpoint = Arc::new(
1605 TestCheckpointBuilder::new(0)
1606 .start_transaction(0)
1607 .create_owned_object(0)
1608 .finish_transaction()
1609 .start_transaction(0)
1610 .transfer_object(0, 1)
1611 .finish_transaction()
1612 .build_checkpoint(),
1613 );
1614
1615 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1616 let v_a = checkpoint.transactions[0].effects.lamport_version();
1617 let v_b = checkpoint.transactions[1].effects.lamport_version();
1618 assert_ne!(v_a, v_b, "the transfer must bump the object's version");
1619 let digest0 = *checkpoint.transactions[0].effects.transaction_digest();
1620 let digest1 = *checkpoint.transactions[1].effects.transaction_digest();
1621 let ckpt_digest = checkpoint.summary.data().digest();
1622
1623 seed(&db, &schema, &checkpoint).await;
1624
1625 assert!(schema.get_object_by_key(obj0, v_a).unwrap().is_some());
1627 assert!(schema.get_object_by_key(obj0, v_b).unwrap().is_some());
1628 assert!(schema.get_effects(0).unwrap().is_some());
1629 assert!(schema.get_effects(1).unwrap().is_some());
1630 assert!(schema.get_checkpoint_summary(0).unwrap().is_some());
1631
1632 let metrics = PrunerMetrics::new(None, &Registry::new());
1634 let new = prune_chunk(
1635 &db,
1636 &schema,
1637 &mut RetractionCursors::default(),
1638 Watermarks::default(),
1639 1,
1640 &metrics,
1641 )
1642 .unwrap();
1643 assert_eq!(
1644 new,
1645 Watermarks {
1646 tx_seq_lo: 2,
1647 checkpoint_lo: 1,
1648 },
1649 );
1650
1651 assert!(
1653 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1654 "superseded version v_a should be pruned",
1655 );
1656 assert!(
1657 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1658 "live version v_b must be preserved",
1659 );
1660
1661 assert!(schema.get_effects(0).unwrap().is_none());
1663 assert!(schema.get_effects(1).unwrap().is_none());
1664 assert!(schema.get_checkpoint_summary(0).unwrap().is_none());
1665
1666 assert!(
1668 schema
1669 .tx_seq_by_digest
1670 .get(&tx_seq_by_digest::Key(digest0))
1671 .unwrap()
1672 .is_none()
1673 );
1674 assert!(
1675 schema
1676 .tx_seq_by_digest
1677 .get(&tx_seq_by_digest::Key(digest1))
1678 .unwrap()
1679 .is_none()
1680 );
1681 assert!(
1682 schema
1683 .checkpoint_seq_by_digest
1684 .get(&checkpoint_seq_by_digest::Key(ckpt_digest))
1685 .unwrap()
1686 .is_none()
1687 );
1688
1689 assert_eq!(
1691 schema.get_pruning_watermarks().unwrap().unwrap(),
1692 Watermarks {
1693 tx_seq_lo: 2,
1694 checkpoint_lo: 1,
1695 },
1696 );
1697 }
1698
1699 #[tokio::test]
1708 async fn prune_chunk_retracts_version_only_when_superseding_tx_is_pruned() {
1709 let (_dir, db, schema) = fresh_db();
1710
1711 let mut builder = TestCheckpointBuilder::new(0)
1714 .start_transaction(0)
1715 .create_owned_object(0)
1716 .finish_transaction();
1717 let cp0 = Arc::new(builder.build_checkpoint());
1718 builder = builder
1719 .start_transaction(0)
1720 .transfer_object(0, 1)
1721 .finish_transaction();
1722 let cp1 = Arc::new(builder.build_checkpoint());
1723
1724 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1725 let v_a = cp0.transactions[0].effects.lamport_version();
1726 let v_b = cp1.transactions[0].effects.lamport_version();
1727 assert_ne!(v_a, v_b);
1728
1729 seed(&db, &schema, &cp0).await;
1730 seed(&db, &schema, &cp1).await;
1731 let metrics = PrunerMetrics::new(None, &Registry::new());
1732 let mut cursors = RetractionCursors::default();
1733
1734 let after_first = prune_chunk(
1737 &db,
1738 &schema,
1739 &mut cursors,
1740 Watermarks::default(),
1741 1,
1742 &metrics,
1743 )
1744 .unwrap();
1745 assert_eq!(
1746 after_first,
1747 Watermarks {
1748 tx_seq_lo: 1,
1749 checkpoint_lo: 1,
1750 },
1751 );
1752 assert!(schema.get_effects(0).unwrap().is_none());
1753 assert!(schema.get_effects(1).unwrap().is_some());
1754 assert!(
1755 schema.get_object_by_key(obj0, v_a).unwrap().is_some(),
1756 "v_a must survive while its superseding tx is still retained",
1757 );
1758
1759 let after_second =
1762 prune_chunk(&db, &schema, &mut cursors, after_first, 2, &metrics).unwrap();
1763 assert_eq!(
1764 after_second,
1765 Watermarks {
1766 tx_seq_lo: 2,
1767 checkpoint_lo: 2,
1768 },
1769 );
1770 assert!(schema.get_effects(1).unwrap().is_none());
1771 assert!(
1772 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1773 "v_a must be retracted once its superseding tx is pruned",
1774 );
1775 assert!(
1776 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1777 "live v_b must be preserved",
1778 );
1779 }
1780
1781 #[tokio::test]
1792 async fn prune_chunk_retracts_object_version_by_checkpoint() {
1793 let (_dir, db, schema) = fresh_db();
1794
1795 let mut builder = TestCheckpointBuilder::new(0)
1796 .start_transaction(0)
1797 .create_owned_object(0)
1798 .finish_transaction();
1799 let cp0 = Arc::new(builder.build_checkpoint());
1800 builder = builder
1801 .start_transaction(0)
1802 .transfer_object(0, 1)
1803 .finish_transaction();
1804 let cp1 = Arc::new(builder.build_checkpoint());
1805
1806 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1807 let v_a = cp0.transactions[0].effects.lamport_version();
1808 let v_b = cp1.transactions[0].effects.lamport_version();
1809 assert_ne!(v_a, v_b);
1810
1811 for cp in [&cp0, &cp1] {
1818 seed(&db, &schema, cp).await;
1819 seed_object_version_by_checkpoint(&db, &schema, cp).await;
1820 }
1821
1822 assert_eq!(
1824 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1825 Some(v_a),
1826 );
1827 assert_eq!(
1828 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1829 Some(v_b),
1830 );
1831
1832 let metrics = PrunerMetrics::new(None, &Registry::new());
1833 let mut cursors = RetractionCursors::default();
1834
1835 let after_first = prune_chunk(
1838 &db,
1839 &schema,
1840 &mut cursors,
1841 Watermarks::default(),
1842 1,
1843 &metrics,
1844 )
1845 .unwrap();
1846 assert_eq!(
1847 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1848 Some(v_a),
1849 "cp0-pinned entry must survive while its superseding tx is retained",
1850 );
1851
1852 prune_chunk(&db, &schema, &mut cursors, after_first, 2, &metrics).unwrap();
1855 assert_eq!(
1856 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1857 None,
1858 "cp0-pinned entry must be retracted once its superseding tx is pruned",
1859 );
1860 assert_eq!(
1861 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1862 Some(v_b),
1863 "cp1-pinned floor entry must be preserved",
1864 );
1865 }
1866
1867 #[tokio::test]
1871 async fn prune_chunk_keeps_object_removed_and_recreated_in_one_checkpoint() {
1872 let (_dir, db, schema) = fresh_db();
1873 let (cp0, cp1) = removed_and_recreated_checkpoints();
1874 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1875 let obj1 = TestCheckpointBuilder::derive_object_id(1);
1876 let v0 = cp1.transactions[1].effects.lamport_version();
1877 let v1 = cp1.transactions[3].effects.lamport_version();
1878
1879 for cp in [&cp0, &cp1] {
1880 seed(&db, &schema, cp).await;
1881 seed_object_version_by_checkpoint(&db, &schema, cp).await;
1882 }
1883 assert_eq!(
1884 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1885 Some(v0),
1886 );
1887 assert_eq!(
1888 schema.get_object_version_at_checkpoint(obj1, 1).unwrap(),
1889 Some(v1),
1890 );
1891
1892 let metrics = PrunerMetrics::new(None, &Registry::new());
1893 prune_chunk(
1894 &db,
1895 &schema,
1896 &mut RetractionCursors::default(),
1897 Watermarks::default(),
1898 2,
1899 &metrics,
1900 )
1901 .unwrap();
1902
1903 assert_eq!(
1904 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1905 Some(v0),
1906 "a deleted then re-created object must keep its live row",
1907 );
1908 assert_eq!(
1909 schema.get_object_version_at_checkpoint(obj1, 1).unwrap(),
1910 Some(v1),
1911 "a wrapped then unwrapped object must keep its live row",
1912 );
1913 assert!(schema.get_object_by_key(obj0, v0).unwrap().is_some());
1914 assert!(schema.get_object_by_key(obj1, v1).unwrap().is_some());
1915 }
1916
1917 #[tokio::test]
1920 async fn prune_history_cohort_keeps_object_removed_and_recreated_in_one_checkpoint() {
1921 let (_dir, db, schema) = fresh_db();
1922 let (cp0, cp1) = removed_and_recreated_checkpoints();
1923 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1924 let obj1 = TestCheckpointBuilder::derive_object_id(1);
1925 let v0 = cp1.transactions[1].effects.lamport_version();
1926 let v1 = cp1.transactions[3].effects.lamport_version();
1927
1928 for cp in [&cp0, &cp1] {
1929 seed_object_version_by_checkpoint(&db, &schema, cp).await;
1930 }
1931
1932 let effects: Vec<(u64, TransactionEffects)> = cp0
1933 .transactions
1934 .iter()
1935 .map(|tx| (0u64, tx.effects.clone()))
1936 .chain(cp1.transactions.iter().map(|tx| (1u64, tx.effects.clone())))
1937 .collect();
1938 prune_history_cohort(
1939 &db,
1940 &schema,
1941 &mut RetractionCursors::default(),
1942 1,
1943 0,
1944 &effects,
1945 )
1946 .unwrap();
1947
1948 assert_eq!(
1949 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
1950 Some(v0),
1951 "a deleted then re-created object must keep its live row",
1952 );
1953 assert_eq!(
1954 schema.get_object_version_at_checkpoint(obj1, 2).unwrap(),
1955 Some(v1),
1956 "a wrapped then unwrapped object must keep its live row",
1957 );
1958 }
1959
1960 #[test]
1965 fn prune_history_cohort_retracts_object_version_by_checkpoint() {
1966 let (_dir, db, schema) = fresh_db();
1967
1968 let mut builder = TestCheckpointBuilder::new(0)
1971 .start_transaction(0)
1972 .create_owned_object(0)
1973 .create_owned_object(1)
1974 .finish_transaction();
1975 let cp0 = Arc::new(builder.build_checkpoint());
1976 builder = builder
1977 .start_transaction(0)
1978 .transfer_object(0, 1)
1979 .finish_transaction()
1980 .start_transaction(0)
1981 .delete_object(1)
1982 .finish_transaction();
1983 let cp1 = Arc::new(builder.build_checkpoint());
1984
1985 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1986 let obj1 = TestCheckpointBuilder::derive_object_id(1);
1987
1988 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
1992 let mut batch = db.batch();
1993 for (id, checkpoint, version) in [(obj0, 0, 1), (obj0, 1, 2), (obj1, 0, 1), (obj1, 1, 2)] {
1994 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, ver(version));
1995 batch
1996 .put(&schema.object_version_by_checkpoint, &k, &v)
1997 .unwrap();
1998 }
1999 batch.commit().unwrap();
2000
2001 assert_eq!(
2003 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2004 Some(ver(1)),
2005 );
2006 assert_eq!(
2007 schema.get_object_version_at_checkpoint(obj1, 0).unwrap(),
2008 Some(ver(1)),
2009 );
2010
2011 let effects: Vec<(u64, TransactionEffects)> = cp0
2016 .transactions
2017 .iter()
2018 .map(|tx| (0u64, tx.effects.clone()))
2019 .chain(cp1.transactions.iter().map(|tx| (1u64, tx.effects.clone())))
2020 .collect();
2021 prune_history_cohort(
2022 &db,
2023 &schema,
2024 &mut RetractionCursors::default(),
2025 1,
2026 0,
2027 &effects,
2028 )
2029 .unwrap();
2030 assert_eq!(
2033 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2034 None,
2035 "a superseded object's pre-anchor row must be retracted",
2036 );
2037 assert_eq!(
2038 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
2039 Some(ver(2)),
2040 "the anchor a read at the floor resolves to must survive",
2041 );
2042
2043 assert_eq!(
2045 schema.get_object_version_at_checkpoint(obj1, 2).unwrap(),
2046 None,
2047 "a removed object's rows must be dropped entirely",
2048 );
2049
2050 assert_eq!(
2052 schema
2053 .get_pruning_watermarks()
2054 .unwrap()
2055 .unwrap()
2056 .checkpoint_lo,
2057 2,
2058 );
2059 }
2060
2061 #[test]
2067 fn prune_history_cohort_deletes_tx_metadata_and_advances_floor() {
2068 use sui_types::digests::TransactionDigest;
2069
2070 use crate::schema::tx_metadata_by_seq;
2071
2072 let (_dir, db, schema) = fresh_db();
2073
2074 let digests: Vec<TransactionDigest> =
2077 (0u8..6).map(|i| TransactionDigest::new([i; 32])).collect();
2078 let mut batch = db.batch();
2079 for (tx_seq, digest) in digests.iter().enumerate() {
2080 let tx_seq = tx_seq as u64;
2081 batch
2082 .put(
2083 &schema.tx_metadata_by_seq,
2084 &U64Be(tx_seq),
2085 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
2086 digest: *digest,
2087 checkpoint_seq: tx_seq,
2088 ckpt_position: 0,
2089 event_count: 0,
2090 timestamp_ms: 0,
2091 }),
2092 )
2093 .unwrap();
2094 batch
2095 .put(
2096 &schema.tx_seq_by_digest,
2097 &tx_seq_by_digest::Key(*digest),
2098 &U64Varint(tx_seq),
2099 )
2100 .unwrap();
2101 }
2102 batch.commit().unwrap();
2103
2104 prune_history_cohort(&db, &schema, &mut RetractionCursors::default(), 2, 3, &[]).unwrap();
2107 for tx_seq in 0..3 {
2109 assert!(
2110 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_none(),
2111 "tx_metadata {tx_seq} should be pruned",
2112 );
2113 }
2114 for tx_seq in 3..6 {
2115 assert!(
2116 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_some(),
2117 "tx_metadata {tx_seq} should be retained",
2118 );
2119 }
2120
2121 for digest in &digests[0..3] {
2123 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_none());
2124 }
2125 for digest in &digests[3..6] {
2126 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_some());
2127 }
2128
2129 assert_eq!(
2131 schema.get_pruning_watermarks().unwrap(),
2132 Some(Watermarks {
2133 tx_seq_lo: 3,
2134 checkpoint_lo: 3,
2135 }),
2136 );
2137
2138 prune_history_cohort(&db, &schema, &mut RetractionCursors::default(), 2, 3, &[]).unwrap();
2140 assert_eq!(
2141 schema.get_pruning_watermarks().unwrap(),
2142 Some(Watermarks {
2143 tx_seq_lo: 3,
2144 checkpoint_lo: 3,
2145 }),
2146 );
2147 }
2148
2149 #[test]
2154 fn prune_history_cohort_handles_sparse_tx_seqs() {
2155 use sui_types::digests::TransactionDigest;
2156
2157 use crate::schema::tx_metadata_by_seq;
2158
2159 let (_dir, db, schema) = fresh_db();
2160
2161 let entries = [
2163 (0u64, [10u8; 32]),
2164 (500_000u64, [11u8; 32]),
2165 (999_999u64, [12u8; 32]),
2166 ];
2167 let mut batch = db.batch();
2168 for (tx_seq, digest_bytes) in entries {
2169 let digest = TransactionDigest::new(digest_bytes);
2170 batch
2171 .put(
2172 &schema.tx_metadata_by_seq,
2173 &U64Be(tx_seq),
2174 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
2175 digest,
2176 checkpoint_seq: tx_seq,
2177 ckpt_position: 0,
2178 event_count: 0,
2179 timestamp_ms: 0,
2180 }),
2181 )
2182 .unwrap();
2183 batch
2184 .put(
2185 &schema.tx_seq_by_digest,
2186 &tx_seq_by_digest::Key(digest),
2187 &U64Varint(tx_seq),
2188 )
2189 .unwrap();
2190 }
2191 batch.commit().unwrap();
2192
2193 prune_history_cohort(
2197 &db,
2198 &schema,
2199 &mut RetractionCursors::default(),
2200 0,
2201 600_000,
2202 &[],
2203 )
2204 .unwrap();
2205 assert!(schema.get_tx_metadata_by_seq(0).unwrap().is_none());
2206 assert!(schema.get_tx_metadata_by_seq(500_000).unwrap().is_none());
2207 assert!(schema.get_tx_metadata_by_seq(999_999).unwrap().is_some());
2208 assert!(
2209 schema
2210 .get_tx_seq_by_digest(&TransactionDigest::new([10u8; 32]))
2211 .unwrap()
2212 .is_none()
2213 );
2214 assert!(
2215 schema
2216 .get_tx_seq_by_digest(&TransactionDigest::new([11u8; 32]))
2217 .unwrap()
2218 .is_none()
2219 );
2220 assert!(
2221 schema
2222 .get_tx_seq_by_digest(&TransactionDigest::new([12u8; 32]))
2223 .unwrap()
2224 .is_some()
2225 );
2226 assert_eq!(
2227 schema.get_pruning_watermarks().unwrap(),
2228 Some(Watermarks {
2229 tx_seq_lo: 600_000,
2230 checkpoint_lo: 1,
2231 }),
2232 );
2233 }
2234 fn all_object_version_by_checkpoint_rows(
2235 schema: &RpcStoreSchema,
2236 ) -> Vec<(
2237 object_version_by_checkpoint::Key,
2238 sui_types::base_types::SequenceNumber,
2239 )> {
2240 schema
2241 .object_version_by_checkpoint
2242 .iter(..)
2243 .unwrap()
2244 .map(|res| {
2245 let (key, value) = res.unwrap();
2246 (
2247 key,
2248 sui_types::base_types::SequenceNumber::from_u64(value.into_inner().version),
2249 )
2250 })
2251 .collect()
2252 }
2253 #[test]
2257 fn prune_history_cohort_differential_with_warm_and_fresh_cursors() {
2258 let (_dir_warm, db_warm, schema_warm) = fresh_db();
2259 let (_dir_fresh, db_fresh, schema_fresh) = fresh_db();
2260
2261 let mut builder = TestCheckpointBuilder::new(0)
2262 .start_transaction(0)
2263 .create_owned_object(0)
2264 .create_owned_object(1)
2265 .create_owned_object(2)
2266 .finish_transaction();
2267 let cp0 = Arc::new(builder.build_checkpoint());
2268
2269 builder = builder
2270 .start_transaction(0)
2271 .transfer_object(0, 1)
2272 .transfer_object(1, 2)
2273 .create_owned_object(3)
2274 .finish_transaction();
2275 let cp1 = Arc::new(builder.build_checkpoint());
2276
2277 builder = builder
2278 .start_transaction(0)
2279 .transfer_object(0, 2)
2280 .delete_object(2)
2281 .create_owned_object(4)
2282 .finish_transaction();
2283 let cp2 = Arc::new(builder.build_checkpoint());
2284
2285 builder = builder
2286 .start_transaction(0)
2287 .transfer_object(0, 3)
2288 .transfer_object(3, 4)
2289 .delete_object(4)
2290 .finish_transaction();
2291 let cp3 = Arc::new(builder.build_checkpoint());
2292
2293 builder = builder
2294 .start_transaction(0)
2295 .transfer_object(0, 4)
2296 .delete_object(1)
2297 .transfer_object(3, 5)
2298 .finish_transaction();
2299 let cp4 = Arc::new(builder.build_checkpoint());
2300
2301 builder = builder
2302 .start_transaction(0)
2303 .transfer_object(0, 5)
2304 .finish_transaction();
2305 let cp5 = Arc::new(builder.build_checkpoint());
2306
2307 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2308 let obj1 = TestCheckpointBuilder::derive_object_id(1);
2309 let obj2 = TestCheckpointBuilder::derive_object_id(2);
2310 let obj3 = TestCheckpointBuilder::derive_object_id(3);
2311 let obj4 = TestCheckpointBuilder::derive_object_id(4);
2312
2313 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2314 let rows = [
2315 (obj0, 0, 1),
2316 (obj0, 1, 2),
2317 (obj0, 2, 3),
2318 (obj0, 3, 4),
2319 (obj0, 4, 5),
2320 (obj0, 5, 6),
2321 (obj1, 0, 1),
2322 (obj1, 1, 2),
2323 (obj1, 4, 3),
2324 (obj2, 0, 1),
2325 (obj2, 2, 2),
2326 (obj3, 1, 1),
2327 (obj3, 3, 2),
2328 (obj3, 4, 3),
2329 (obj4, 2, 1),
2330 (obj4, 3, 2),
2331 ];
2332
2333 for (db, schema) in [(&db_warm, &schema_warm), (&db_fresh, &schema_fresh)] {
2334 let mut batch = db.batch();
2335 for (id, cp, version) in rows {
2336 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(version));
2337 batch
2338 .put(&schema.object_version_by_checkpoint, &k, &v)
2339 .unwrap();
2340 }
2341 batch.commit().unwrap();
2342 }
2343
2344 let checkpoints = [
2345 (&cp0, 0u64),
2346 (&cp1, 1),
2347 (&cp2, 2),
2348 (&cp3, 3),
2349 (&cp4, 4),
2350 (&cp5, 5),
2351 ];
2352
2353 let effects_b1: Vec<(u64, TransactionEffects)> = checkpoints[0..=1]
2355 .iter()
2356 .flat_map(|(cp, seq)| {
2357 cp.transactions
2358 .iter()
2359 .map(move |tx| (*seq, tx.effects.clone()))
2360 })
2361 .collect();
2362 let effects_b2: Vec<(u64, TransactionEffects)> = checkpoints[2..=3]
2364 .iter()
2365 .flat_map(|(cp, seq)| {
2366 cp.transactions
2367 .iter()
2368 .map(move |tx| (*seq, tx.effects.clone()))
2369 })
2370 .collect();
2371 let effects_b3: Vec<(u64, TransactionEffects)> = checkpoints[4..=5]
2373 .iter()
2374 .flat_map(|(cp, seq)| {
2375 cp.transactions
2376 .iter()
2377 .map(move |tx| (*seq, tx.effects.clone()))
2378 })
2379 .collect();
2380
2381 let mut warm_cursors = RetractionCursors::default();
2383 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 1, 0, &effects_b1).unwrap();
2384 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 3, 0, &effects_b2).unwrap();
2385 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 5, 0, &effects_b3).unwrap();
2386
2387 prune_history_cohort(
2389 &db_fresh,
2390 &schema_fresh,
2391 &mut RetractionCursors::default(),
2392 1,
2393 0,
2394 &effects_b1,
2395 )
2396 .unwrap();
2397 prune_history_cohort(
2398 &db_fresh,
2399 &schema_fresh,
2400 &mut RetractionCursors::default(),
2401 3,
2402 0,
2403 &effects_b2,
2404 )
2405 .unwrap();
2406 prune_history_cohort(
2407 &db_fresh,
2408 &schema_fresh,
2409 &mut RetractionCursors::default(),
2410 5,
2411 0,
2412 &effects_b3,
2413 )
2414 .unwrap();
2415
2416 let rows_warm = all_object_version_by_checkpoint_rows(&schema_warm);
2417 let rows_fresh = all_object_version_by_checkpoint_rows(&schema_fresh);
2418 assert_eq!(rows_warm, rows_fresh);
2419 let obj0_checkpoints: Vec<u64> = rows_warm
2423 .iter()
2424 .filter(|(k, _)| k.id == obj0)
2425 .map(|(k, _)| k.checkpoint)
2426 .collect();
2427 assert_eq!(obj0_checkpoints, vec![5], "obj0 keeps only its cp5 anchor");
2428 }
2429
2430 #[test]
2433 fn prune_history_cohort_simulated_restart() {
2434 let (_dir_uninterrupted, db_uninterrupted, schema_uninterrupted) = fresh_db();
2435 let (_dir_restarted, db_restarted, schema_restarted) = fresh_db();
2436
2437 let mut builder = TestCheckpointBuilder::new(0)
2438 .start_transaction(0)
2439 .create_owned_object(0)
2440 .create_owned_object(1)
2441 .finish_transaction();
2442 let cp0 = Arc::new(builder.build_checkpoint());
2443
2444 builder = builder
2445 .start_transaction(0)
2446 .transfer_object(0, 1)
2447 .transfer_object(1, 2)
2448 .finish_transaction();
2449 let cp1 = Arc::new(builder.build_checkpoint());
2450
2451 builder = builder
2452 .start_transaction(0)
2453 .transfer_object(0, 2)
2454 .delete_object(1)
2455 .finish_transaction();
2456 let cp2 = Arc::new(builder.build_checkpoint());
2457
2458 builder = builder
2459 .start_transaction(0)
2460 .transfer_object(0, 3)
2461 .finish_transaction();
2462 let cp3 = Arc::new(builder.build_checkpoint());
2463
2464 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2465 let obj1 = TestCheckpointBuilder::derive_object_id(1);
2466
2467 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2468 let rows = [
2469 (obj0, 0, 1),
2470 (obj0, 1, 2),
2471 (obj0, 2, 3),
2472 (obj0, 3, 4),
2473 (obj1, 0, 1),
2474 (obj1, 1, 2),
2475 (obj1, 2, 3),
2476 ];
2477
2478 for (db, schema) in [
2479 (&db_uninterrupted, &schema_uninterrupted),
2480 (&db_restarted, &schema_restarted),
2481 ] {
2482 let mut batch = db.batch();
2483 for (id, cp, version) in rows {
2484 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(version));
2485 batch
2486 .put(&schema.object_version_by_checkpoint, &k, &v)
2487 .unwrap();
2488 }
2489 batch.commit().unwrap();
2490 }
2491
2492 let checkpoints = [(&cp0, 0u64), (&cp1, 1), (&cp2, 2), (&cp3, 3)];
2493
2494 let effects_b1: Vec<(u64, TransactionEffects)> = checkpoints[0..=1]
2495 .iter()
2496 .flat_map(|(cp, seq)| {
2497 cp.transactions
2498 .iter()
2499 .map(move |tx| (*seq, tx.effects.clone()))
2500 })
2501 .collect();
2502 let effects_b2: Vec<(u64, TransactionEffects)> = checkpoints[2..=3]
2503 .iter()
2504 .flat_map(|(cp, seq)| {
2505 cp.transactions
2506 .iter()
2507 .map(move |tx| (*seq, tx.effects.clone()))
2508 })
2509 .collect();
2510
2511 let mut uninterrupted_cursors = RetractionCursors::default();
2513 prune_history_cohort(
2514 &db_uninterrupted,
2515 &schema_uninterrupted,
2516 &mut uninterrupted_cursors,
2517 1,
2518 0,
2519 &effects_b1,
2520 )
2521 .unwrap();
2522 prune_history_cohort(
2523 &db_uninterrupted,
2524 &schema_uninterrupted,
2525 &mut uninterrupted_cursors,
2526 3,
2527 0,
2528 &effects_b2,
2529 )
2530 .unwrap();
2531
2532 let mut restarted_cursors = RetractionCursors::default();
2534 prune_history_cohort(
2535 &db_restarted,
2536 &schema_restarted,
2537 &mut restarted_cursors,
2538 1,
2539 0,
2540 &effects_b1,
2541 )
2542 .unwrap();
2543 restarted_cursors = RetractionCursors::default();
2545 prune_history_cohort(
2546 &db_restarted,
2547 &schema_restarted,
2548 &mut restarted_cursors,
2549 3,
2550 0,
2551 &effects_b2,
2552 )
2553 .unwrap();
2554
2555 let rows_uninterrupted = all_object_version_by_checkpoint_rows(&schema_uninterrupted);
2556 let rows_restarted = all_object_version_by_checkpoint_rows(&schema_restarted);
2557 assert_eq!(rows_uninterrupted, rows_restarted);
2558 }
2559
2560 #[test]
2563 fn prune_history_cohort_hot_object_across_batches() {
2564 let (_dir, db, schema) = fresh_db();
2565
2566 let mut builder = TestCheckpointBuilder::new(0)
2567 .start_transaction(0)
2568 .create_owned_object(0)
2569 .finish_transaction();
2570 let cp0 = Arc::new(builder.build_checkpoint());
2571
2572 builder = builder
2573 .start_transaction(0)
2574 .transfer_object(0, 1)
2575 .finish_transaction();
2576 let cp1 = Arc::new(builder.build_checkpoint());
2577
2578 builder = builder
2579 .start_transaction(0)
2580 .transfer_object(0, 2)
2581 .finish_transaction();
2582 let cp2 = Arc::new(builder.build_checkpoint());
2583
2584 builder = builder
2585 .start_transaction(0)
2586 .transfer_object(0, 3)
2587 .finish_transaction();
2588 let cp3 = Arc::new(builder.build_checkpoint());
2589
2590 builder = builder
2591 .start_transaction(0)
2592 .transfer_object(0, 4)
2593 .finish_transaction();
2594 let cp4 = Arc::new(builder.build_checkpoint());
2595
2596 builder = builder
2597 .start_transaction(0)
2598 .transfer_object(0, 5)
2599 .finish_transaction();
2600 let cp5 = Arc::new(builder.build_checkpoint());
2601
2602 let hot_obj = TestCheckpointBuilder::derive_object_id(0);
2603 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2604
2605 let mut batch = db.batch();
2606 for (checkpoint, version) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)] {
2607 let (k, v) = object_version_by_checkpoint::store(hot_obj, checkpoint, ver(version));
2608 batch
2609 .put(&schema.object_version_by_checkpoint, &k, &v)
2610 .unwrap();
2611 }
2612 batch.commit().unwrap();
2613
2614 let mut cursors = RetractionCursors::default();
2615
2616 let effects_b1 = vec![
2618 (0u64, cp0.transactions[0].effects.clone()),
2619 (1u64, cp1.transactions[0].effects.clone()),
2620 ];
2621 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_b1).unwrap();
2622 assert_eq!(cursors.lower_bound(&hot_obj), 1);
2623 assert_eq!(
2624 schema.get_object_version_at_checkpoint(hot_obj, 0).unwrap(),
2625 None
2626 );
2627 assert_eq!(
2628 schema.get_object_version_at_checkpoint(hot_obj, 1).unwrap(),
2629 Some(ver(2))
2630 );
2631
2632 let effects_b2 = vec![
2634 (2u64, cp2.transactions[0].effects.clone()),
2635 (3u64, cp3.transactions[0].effects.clone()),
2636 ];
2637 prune_history_cohort(&db, &schema, &mut cursors, 3, 0, &effects_b2).unwrap();
2638 assert_eq!(cursors.lower_bound(&hot_obj), 3);
2639 assert_eq!(
2640 schema.get_object_version_at_checkpoint(hot_obj, 0).unwrap(),
2641 None
2642 );
2643 assert_eq!(
2644 schema.get_object_version_at_checkpoint(hot_obj, 1).unwrap(),
2645 None
2646 );
2647 assert_eq!(
2648 schema.get_object_version_at_checkpoint(hot_obj, 2).unwrap(),
2649 None
2650 );
2651 assert_eq!(
2652 schema.get_object_version_at_checkpoint(hot_obj, 3).unwrap(),
2653 Some(ver(4))
2654 );
2655
2656 let effects_b3 = vec![
2658 (4u64, cp4.transactions[0].effects.clone()),
2659 (5u64, cp5.transactions[0].effects.clone()),
2660 ];
2661 prune_history_cohort(&db, &schema, &mut cursors, 5, 0, &effects_b3).unwrap();
2662 assert_eq!(cursors.lower_bound(&hot_obj), 5);
2663 assert_eq!(
2664 schema.get_object_version_at_checkpoint(hot_obj, 3).unwrap(),
2665 None
2666 );
2667 assert_eq!(
2668 schema.get_object_version_at_checkpoint(hot_obj, 4).unwrap(),
2669 None
2670 );
2671 assert_eq!(
2672 schema.get_object_version_at_checkpoint(hot_obj, 6).unwrap(),
2673 Some(ver(6))
2674 );
2675 }
2676
2677 #[test]
2684 fn prune_history_cohort_eviction_fallback() {
2685 let (_dir_tiny, db_tiny, schema_tiny) = fresh_db();
2686 let (_dir_default, db_default, schema_default) = fresh_db();
2687
2688 let mut builder = TestCheckpointBuilder::new(0)
2692 .start_transaction(0)
2693 .create_owned_object(0)
2694 .create_owned_object(1)
2695 .create_owned_object(2)
2696 .finish_transaction();
2697 let cp0 = builder.build_checkpoint();
2698 builder = builder
2699 .start_transaction(0)
2700 .transfer_object(0, 1)
2701 .transfer_object(1, 1)
2702 .transfer_object(2, 1)
2703 .finish_transaction();
2704 let cp1 = builder.build_checkpoint();
2705 builder = builder
2706 .start_transaction(0)
2707 .transfer_object(0, 2)
2708 .transfer_object(1, 2)
2709 .transfer_object(2, 2)
2710 .finish_transaction();
2711 let cp2 = builder.build_checkpoint();
2712 builder = builder
2713 .start_transaction(0)
2714 .transfer_object(0, 3)
2715 .transfer_object(1, 3)
2716 .transfer_object(2, 3)
2717 .finish_transaction();
2718 let cp3 = builder.build_checkpoint();
2719
2720 let objs: Vec<ObjectID> = (0..3u64)
2721 .map(TestCheckpointBuilder::derive_object_id)
2722 .collect();
2723 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2724
2725 for (db, schema) in [(&db_tiny, &schema_tiny), (&db_default, &schema_default)] {
2726 let mut batch = db.batch();
2727 for &id in &objs {
2728 for cp in 0..4u64 {
2729 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(cp + 1));
2730 batch
2731 .put(&schema.object_version_by_checkpoint, &k, &v)
2732 .unwrap();
2733 }
2734 }
2735 batch.commit().unwrap();
2736 }
2737
2738 let effects_b1: Vec<(u64, TransactionEffects)> = [(0u64, &cp0), (1, &cp1)]
2739 .into_iter()
2740 .flat_map(|(seq, cp)| {
2741 cp.transactions
2742 .iter()
2743 .map(move |tx| (seq, tx.effects.clone()))
2744 })
2745 .collect();
2746 let effects_b2: Vec<(u64, TransactionEffects)> = [(2u64, &cp2), (3, &cp3)]
2747 .into_iter()
2748 .flat_map(|(seq, cp)| {
2749 cp.transactions
2750 .iter()
2751 .map(move |tx| (seq, tx.effects.clone()))
2752 })
2753 .collect();
2754
2755 let mut tiny_cursors = RetractionCursors::with_capacity(1);
2758 prune_history_cohort(&db_tiny, &schema_tiny, &mut tiny_cursors, 1, 0, &effects_b1).unwrap();
2759 assert!(
2760 objs.iter()
2761 .filter(|id| tiny_cursors.lower_bound(id) > 0)
2762 .count()
2763 <= 1,
2764 "capacity 1 must have evicted all but at most one cursor"
2765 );
2766 prune_history_cohort(&db_tiny, &schema_tiny, &mut tiny_cursors, 3, 0, &effects_b2).unwrap();
2767
2768 let mut default_cursors = RetractionCursors::default();
2770 prune_history_cohort(
2771 &db_default,
2772 &schema_default,
2773 &mut default_cursors,
2774 1,
2775 0,
2776 &effects_b1,
2777 )
2778 .unwrap();
2779 for id in &objs {
2780 assert_eq!(
2781 default_cursors.lower_bound(id),
2782 1,
2783 "default capacity keeps every cursor warm"
2784 );
2785 }
2786 prune_history_cohort(
2787 &db_default,
2788 &schema_default,
2789 &mut default_cursors,
2790 3,
2791 0,
2792 &effects_b2,
2793 )
2794 .unwrap();
2795
2796 assert_eq!(tiny_cursors.len(), 1);
2799 assert!(default_cursors.len() > tiny_cursors.len());
2800
2801 let rows_tiny = all_object_version_by_checkpoint_rows(&schema_tiny);
2804 let rows_default = all_object_version_by_checkpoint_rows(&schema_default);
2805 assert_eq!(rows_tiny, rows_default);
2806 let mut expected: Vec<_> = objs
2807 .iter()
2808 .map(|&id| {
2809 (
2810 object_version_by_checkpoint::Key { id, checkpoint: 3 },
2811 ver(4),
2812 )
2813 })
2814 .collect();
2815 expected.sort_by_key(|(k, _)| k.id);
2816 assert_eq!(rows_tiny, expected);
2817 }
2818
2819 #[test]
2823 fn prune_history_cohort_removed_at_or_below_cursor() {
2824 let (_dir, db, schema) = fresh_db();
2825
2826 let mut builder = TestCheckpointBuilder::new(0)
2827 .start_transaction(0)
2828 .create_owned_object(0)
2829 .finish_transaction();
2830 let _cp0 = Arc::new(builder.build_checkpoint());
2831
2832 builder = builder
2833 .start_transaction(0)
2834 .transfer_object(0, 1)
2835 .finish_transaction()
2836 .start_transaction(0)
2837 .delete_object(0)
2838 .finish_transaction();
2839 let cp1 = Arc::new(builder.build_checkpoint());
2840
2841 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2842 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2843
2844 let mut batch = db.batch();
2845 let (k0, v0) = object_version_by_checkpoint::store(obj0, 0, ver(1));
2846 let (k1, v1) = object_version_by_checkpoint::store(obj0, 1, ver(2));
2847 batch
2848 .put(&schema.object_version_by_checkpoint, &k0, &v0)
2849 .unwrap();
2850 batch
2851 .put(&schema.object_version_by_checkpoint, &k1, &v1)
2852 .unwrap();
2853 batch.commit().unwrap();
2854
2855 let mut cursors = RetractionCursors::default();
2856
2857 let effects_supersede: Vec<(u64, TransactionEffects)> =
2859 vec![(1u64, cp1.transactions[0].effects.clone())];
2860 prune_history_cohort(&db, &schema, &mut cursors, 0, 0, &effects_supersede).unwrap();
2861 assert_eq!(cursors.lower_bound(&obj0), 1);
2862 assert_eq!(
2863 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2864 None
2865 );
2866 assert_eq!(
2867 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2868 Some(ver(2))
2869 );
2870
2871 let effects_remove: Vec<(u64, TransactionEffects)> =
2873 vec![(1u64, cp1.transactions[1].effects.clone())];
2874 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_remove).unwrap();
2875 assert_eq!(cursors.lower_bound(&obj0), 1);
2876
2877 assert_eq!(
2879 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2880 None
2881 );
2882 assert_eq!(
2883 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2884 None
2885 );
2886 assert_eq!(
2887 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
2888 None
2889 );
2890
2891 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_remove).unwrap();
2893 assert_eq!(
2894 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2895 None
2896 );
2897 assert_eq!(
2898 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2899 None
2900 );
2901 assert_eq!(
2902 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
2903 None
2904 );
2905 }
2906
2907 #[test]
2913 fn retraction_cursors_hot_object_survives_lru_churn() {
2914 let hot = TestCheckpointBuilder::derive_object_id(0);
2915 let mut cursors = RetractionCursors::with_capacity(4);
2916 for batch in 1..=5u64 {
2917 cursors.advance(hot, batch);
2918 for j in 0..2u64 {
2919 cursors.advance(
2920 TestCheckpointBuilder::derive_object_id(100 + batch * 10 + j),
2921 batch,
2922 );
2923 }
2924 assert_eq!(
2925 cursors.lower_bound(&hot),
2926 batch,
2927 "hot object must survive batch {batch}'s cold churn"
2928 );
2929 }
2930 assert_eq!(
2933 cursors.lower_bound(&TestCheckpointBuilder::derive_object_id(110)),
2934 0
2935 );
2936 assert_eq!(cursors.len(), 4);
2937 }
2938
2939 #[test]
2944 fn prune_history_cohort_stale_floor_redelivery_is_noop() {
2945 let (_dir, db, schema) = fresh_db();
2946
2947 let mut builder = TestCheckpointBuilder::new(0)
2948 .start_transaction(0)
2949 .create_owned_object(0)
2950 .finish_transaction();
2951 let cp0 = builder.build_checkpoint();
2952 builder = builder
2953 .start_transaction(0)
2954 .transfer_object(0, 1)
2955 .finish_transaction();
2956 let cp1 = builder.build_checkpoint();
2957 builder = builder
2958 .start_transaction(0)
2959 .transfer_object(0, 2)
2960 .finish_transaction();
2961 let cp2 = builder.build_checkpoint();
2962 builder = builder
2963 .start_transaction(0)
2964 .transfer_object(0, 3)
2965 .finish_transaction();
2966 let cp3 = builder.build_checkpoint();
2967
2968 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2969 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2970
2971 let mut batch = db.batch();
2972 for cp in 0..4u64 {
2973 let (k, v) = object_version_by_checkpoint::store(obj0, cp, ver(cp + 1));
2974 batch
2975 .put(&schema.object_version_by_checkpoint, &k, &v)
2976 .unwrap();
2977 }
2978 batch.commit().unwrap();
2979
2980 let effects_all: Vec<(u64, TransactionEffects)> =
2983 [(0u64, &cp0), (1, &cp1), (2, &cp2), (3, &cp3)]
2984 .into_iter()
2985 .flat_map(|(seq, cp)| {
2986 cp.transactions
2987 .iter()
2988 .map(move |tx| (seq, tx.effects.clone()))
2989 })
2990 .collect();
2991 let mut cursors = RetractionCursors::default();
2992 prune_history_cohort(&db, &schema, &mut cursors, 3, 0, &effects_all).unwrap();
2993 assert_eq!(cursors.lower_bound(&obj0), 3);
2994 assert_eq!(
2995 schema.get_object_version_at_checkpoint(obj0, 3).unwrap(),
2996 Some(ver(4))
2997 );
2998
2999 let redelivered: Vec<(u64, TransactionEffects)> =
3003 vec![(1, cp1.transactions[0].effects.clone())];
3004 prune_history_cohort(&db, &schema, &mut cursors, 1, 5, &redelivered).unwrap();
3005
3006 assert_eq!(cursors.lower_bound(&obj0), 3);
3009 assert_eq!(
3010 schema.get_object_version_at_checkpoint(obj0, 3).unwrap(),
3011 Some(ver(4))
3012 );
3013 let rows = all_object_version_by_checkpoint_rows(&schema);
3014 assert_eq!(
3015 rows,
3016 vec![(
3017 object_version_by_checkpoint::Key {
3018 id: obj0,
3019 checkpoint: 3
3020 },
3021 ver(4)
3022 )]
3023 );
3024 }
3025
3026 #[tokio::test]
3029 async fn prune_chunk_standalone_retraction_cursors_and_restart() {
3030 use crate::indexer::object_version_by_checkpoint::ObjectVersionByCheckpoint;
3031
3032 let (_dir1, db1, schema1) = fresh_db();
3033 let (_dir2, db2, schema2) = fresh_db();
3034 let (_dir3, db3, schema3) = fresh_db();
3035
3036 let mut builder = TestCheckpointBuilder::new(0)
3037 .start_transaction(0)
3038 .create_owned_object(0)
3039 .create_owned_object(1)
3040 .finish_transaction();
3041 let cp0 = Arc::new(builder.build_checkpoint());
3042
3043 builder = builder
3044 .start_transaction(0)
3045 .transfer_object(0, 1)
3046 .transfer_object(1, 2)
3047 .finish_transaction();
3048 let cp1 = Arc::new(builder.build_checkpoint());
3049
3050 builder = builder
3051 .start_transaction(0)
3052 .transfer_object(0, 2)
3053 .delete_object(1)
3054 .finish_transaction();
3055 let cp2 = Arc::new(builder.build_checkpoint());
3056
3057 builder = builder
3058 .start_transaction(0)
3059 .transfer_object(0, 3)
3060 .finish_transaction();
3061 let cp3 = Arc::new(builder.build_checkpoint());
3062
3063 let checkpoints = [&cp0, &cp1, &cp2, &cp3];
3064
3065 for (db, schema) in [(&db1, &schema1), (&db2, &schema2), (&db3, &schema3)] {
3066 for cp in &checkpoints {
3067 seed(db, schema, cp).await;
3068 let mut batch = db.batch();
3069 for row in ObjectVersionByCheckpoint::default()
3070 .process(cp)
3071 .await
3072 .unwrap()
3073 {
3074 let crate::indexer::object_version_by_checkpoint::Row::Change {
3075 id,
3076 checkpoint,
3077 version,
3078 } = row
3079 else {
3080 continue;
3081 };
3082 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, version);
3083 batch
3084 .put(&schema.object_version_by_checkpoint, &k, &v)
3085 .unwrap();
3086 }
3087 batch.commit().unwrap();
3088 }
3089 }
3090
3091 let metrics = PrunerMetrics::new(None, &Registry::new());
3092
3093 let mut cursors1 = RetractionCursors::default();
3095 let w1 = prune_chunk(
3096 &db1,
3097 &schema1,
3098 &mut cursors1,
3099 Watermarks::default(),
3100 2,
3101 &metrics,
3102 )
3103 .unwrap();
3104 prune_chunk(&db1, &schema1, &mut cursors1, w1, 4, &metrics).unwrap();
3105
3106 let w2 = prune_chunk(
3108 &db2,
3109 &schema2,
3110 &mut RetractionCursors::default(),
3111 Watermarks::default(),
3112 2,
3113 &metrics,
3114 )
3115 .unwrap();
3116 prune_chunk(
3117 &db2,
3118 &schema2,
3119 &mut RetractionCursors::default(),
3120 w2,
3121 4,
3122 &metrics,
3123 )
3124 .unwrap();
3125
3126 let mut cursors3 = RetractionCursors::default();
3128 let w3 = prune_chunk(
3129 &db3,
3130 &schema3,
3131 &mut cursors3,
3132 Watermarks::default(),
3133 2,
3134 &metrics,
3135 )
3136 .unwrap();
3137 cursors3 = RetractionCursors::default();
3138 prune_chunk(&db3, &schema3, &mut cursors3, w3, 4, &metrics).unwrap();
3139
3140 let rows1 = all_object_version_by_checkpoint_rows(&schema1);
3141 let rows2 = all_object_version_by_checkpoint_rows(&schema2);
3142 let rows3 = all_object_version_by_checkpoint_rows(&schema3);
3143
3144 assert_eq!(rows1, rows2);
3145 assert_eq!(rows1, rows3);
3146 assert!(!rows1.is_empty());
3147 }
3148}