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 stage(
283 &self,
284 batch: &mut Batch,
285 schema: &RpcStoreSchema,
286 cursors: &RetractionCursors,
287 ) -> anyhow::Result<()> {
288 for (&id, &(cp, removed)) in &self.0 {
289 let lo_cp = cursors.lower_bound(&id);
290 retract_object_version_by_checkpoint(batch, schema, id, lo_cp, cp, removed)?;
291 }
292 Ok(())
293 }
294
295 fn commit_to_cursors(self, cursors: &mut RetractionCursors) {
296 for (id, (cp, _removed)) in self.0 {
297 cursors.advance(id, cp);
298 }
299 }
300}
301
302pub fn start_pruner(
309 store: Store,
310 config: PrunerConfig,
311 metrics: Arc<PrunerMetrics>,
312) -> anyhow::Result<Service> {
313 anyhow::ensure!(
314 config.retention_epochs >= 1,
315 "PrunerConfig::retention_epochs must be >= 1; 0 would prune the current epoch",
316 );
317 anyhow::ensure!(
318 config.max_checkpoints_per_tick >= 1,
319 "PrunerConfig::max_checkpoints_per_tick must be >= 1; 0 would never make progress",
320 );
321
322 let cursors = Arc::new(Mutex::new(RetractionCursors::default()));
323
324 let service = Service::new().spawn_aborting(async move {
325 let mut ticker = tokio::time::interval(config.interval());
326 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
327
328 loop {
329 ticker.tick().await;
330
331 let store = store.clone();
332 let config = config.clone();
333 let metrics = metrics.clone();
334 let cursors = cursors.clone();
335
336 let res = tokio::task::spawn_blocking(move || {
339 let mut guard = cursors.lock().unwrap_or_else(|p| p.into_inner());
340 prune_once(store.db(), store.schema(), &mut guard, &config, &metrics)
341 })
342 .await;
343
344 match res {
345 Ok(Ok(())) => {}
346 Ok(Err(e)) => {
347 warn!("rpc-store pruner pass failed (will retry next interval): {e:#}")
348 }
349 Err(e) => {
350 if e.is_panic() {
351 std::panic::resume_unwind(e.into_panic());
352 } else {
353 panic!("rpc-store pruner task failed: {e}");
354 }
355 }
356 }
357 }
358 });
359
360 Ok(service)
361}
362
363fn prune_once(
366 db: &Db,
367 schema: &RpcStoreSchema,
368 retraction_cursors: &mut RetractionCursors,
369 config: &PrunerConfig,
370 metrics: &PrunerMetrics,
371) -> anyhow::Result<()> {
372 let Some(current_epoch) = current_committed_epoch(db)? else {
373 debug!("rpc-store pruner: no committed watermark yet; nothing to prune");
374 return Ok(());
375 };
376
377 let Some(retention_lo) =
378 retention_checkpoint_floor(schema, current_epoch, config.retention_epochs)?
379 else {
380 debug!(
381 current_epoch,
382 "rpc-store pruner: retention floor not yet reached; nothing to prune"
383 );
384 return Ok(());
385 };
386
387 let target_lo = clamp_to_snapshot(retention_lo, db.snapshot_range().map(|r| *r.start()));
389
390 let mut cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
391 if target_lo <= cursor.checkpoint_lo {
392 debug!(
393 target_lo,
394 current_lo = cursor.checkpoint_lo,
395 "rpc-store pruner: floor already at or beyond target"
396 );
397 return Ok(());
398 }
399
400 let tick_target = target_lo.min(cursor.checkpoint_lo + config.max_checkpoints_per_tick);
405
406 info!(
407 from = cursor.checkpoint_lo,
408 to = tick_target,
409 target = target_lo,
410 current_epoch,
411 "rpc-store pruner: advancing floor"
412 );
413
414 while cursor.checkpoint_lo < tick_target {
415 let chunk_ckpt_hi = (cursor.checkpoint_lo + config.max_chunk_checkpoints).min(tick_target);
416 cursor = prune_chunk(
417 db,
418 schema,
419 retraction_cursors,
420 cursor,
421 chunk_ckpt_hi,
422 metrics,
423 )?;
424 metrics.checkpoint_lo.set(cursor.checkpoint_lo as i64);
425 metrics.tx_seq_lo.set(cursor.tx_seq_lo as i64);
426 metrics.chunks_committed.inc();
427 }
428
429 if cursor.checkpoint_lo >= target_lo {
436 db.compact_range_cf(transaction_bitmap::NAME, None, None)
437 .context("Compacting transaction_bitmap after prune")?;
438 db.compact_range_cf(event_bitmap::NAME, None, None)
439 .context("Compacting event_bitmap after prune")?;
440 }
441
442 Ok(())
443}
444
445fn prune_chunk(
448 db: &Db,
449 schema: &RpcStoreSchema,
450 retraction_cursors: &mut RetractionCursors,
451 cursor: Watermarks,
452 chunk_ckpt_hi: u64,
453 metrics: &PrunerMetrics,
454) -> anyhow::Result<Watermarks> {
455 let ckpt_lo = cursor.checkpoint_lo;
456 let tx_lo = cursor.tx_seq_lo;
457
458 let last_ckpt = chunk_ckpt_hi - 1;
465 let tx_hi = schema
466 .get_checkpoint_summary(last_ckpt)?
467 .with_context(|| format!("checkpoint_summary missing for checkpoint {last_ckpt}"))?
468 .data()
469 .network_total_transactions;
470
471 let mut batch = db.batch();
472 let mut retractions = Retractions::default();
473 let mut objects_deleted: u64 = 0;
474 let mut tx_cursor = tx_lo;
483 for seq in ckpt_lo..chunk_ckpt_hi {
484 let summary = schema
489 .get_checkpoint_summary(seq)?
490 .with_context(|| format!("checkpoint_summary missing for checkpoint {seq}"))?;
491 let ckpt_tx_hi = summary.data().network_total_transactions;
492
493 for tx_seq in tx_cursor..ckpt_tx_hi {
494 let Some((effects, _unchanged)) = schema.get_effects(tx_seq)? else {
495 continue;
496 };
497 for (id, version) in effects.modified_at_versions() {
498 batch.delete(&schema.objects, &objects::Key { id, version })?;
499 retractions.record(id, seq, false);
504 objects_deleted += 1;
505 }
506 for (id, version) in effects.all_tombstones() {
507 batch.delete(&schema.objects, &objects::Key { id, version })?;
508 retractions.record(id, seq, true);
511 objects_deleted += 1;
512 }
513 batch.delete(
514 &schema.tx_seq_by_digest,
515 &tx_seq_by_digest::Key(*effects.transaction_digest()),
516 )?;
517 }
518 tx_cursor = ckpt_tx_hi;
519
520 batch.delete(
522 &schema.checkpoint_seq_by_digest,
523 &checkpoint_seq_by_digest::Key(summary.data().digest()),
524 )?;
525 }
526
527 batch.delete_range(&schema.transactions, &U64Be(tx_lo), &U64Be(tx_hi))?;
531 batch.delete_range(&schema.effects, &U64Be(tx_lo), &U64Be(tx_hi))?;
532 batch.delete_range(&schema.events, &U64Be(tx_lo), &U64Be(tx_hi))?;
533 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
534 batch.delete_range(
535 &schema.checkpoint_summary,
536 &U64Be(ckpt_lo),
537 &U64Be(chunk_ckpt_hi),
538 )?;
539 batch.delete_range(
540 &schema.checkpoint_contents,
541 &U64Be(ckpt_lo),
542 &U64Be(chunk_ckpt_hi),
543 )?;
544
545 retractions.stage(&mut batch, schema, retraction_cursors)?;
546
547 let new = Watermarks {
549 tx_seq_lo: tx_hi,
550 checkpoint_lo: chunk_ckpt_hi,
551 };
552 let (k, v) = pruning_watermark::store(&new);
553 batch.put(&schema.pruning_watermark, &k, &v)?;
554
555 batch.commit()?;
556
557 retractions.commit_to_cursors(retraction_cursors);
558
559 schema.set_pruning_floor(new.tx_seq_lo);
562 metrics.objects_deleted.inc_by(objects_deleted);
563
564 Ok(new)
565}
566
567fn retract_object_version_by_checkpoint(
610 batch: &mut Batch,
611 schema: &RpcStoreSchema,
612 id: ObjectID,
613 lo_cp: u64,
614 cp: u64,
615 removed: bool,
616) -> anyhow::Result<()> {
617 if lo_cp < cp {
618 let lo = object_version_by_checkpoint::Key {
619 id,
620 checkpoint: lo_cp,
621 };
622 let hi = object_version_by_checkpoint::Key { id, checkpoint: cp };
623 for entry in schema
624 .object_version_by_checkpoint
625 .iter((Bound::Included(lo), Bound::Excluded(hi)))?
626 {
627 let (key, _value) = entry?;
628 batch.delete(&schema.object_version_by_checkpoint, &key)?;
629 }
630 }
631 if removed {
632 let hi = object_version_by_checkpoint::Key { id, checkpoint: cp };
633 batch.delete(&schema.object_version_by_checkpoint, &hi)?;
634 }
635 Ok(())
636}
637
638pub fn prune_history_cohort(
686 db: &Db,
687 schema: &RpcStoreSchema,
688 retraction_cursors: &mut RetractionCursors,
689 pruned_checkpoint_watermark: u64,
690 pruned_tx_seq_exclusive: u64,
691 effects: &[(u64, TransactionEffects)],
692) -> anyhow::Result<()> {
693 let cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
694 let tx_lo = cursor.tx_seq_lo;
695 let tx_hi = pruned_tx_seq_exclusive;
696 let checkpoint_lo = pruned_checkpoint_watermark.saturating_add(1);
699
700 if tx_hi <= tx_lo && checkpoint_lo <= cursor.checkpoint_lo {
703 return Ok(());
704 }
705
706 let mut batch = db.batch();
707 let mut retractions = Retractions::default();
708 for entry in schema.iter_tx_seq_digests(tx_lo, tx_hi)? {
714 let (_tx_seq, digest) = entry?;
715 batch.delete(&schema.tx_seq_by_digest, &tx_seq_by_digest::Key(digest))?;
716 }
717 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
718
719 for (checkpoint, effects) in effects {
729 for (id, _version) in effects.modified_at_versions() {
730 retractions.record(id, *checkpoint, false);
731 }
732 for (id, _version) in effects.all_tombstones() {
733 retractions.record(id, *checkpoint, true);
734 }
735 }
736 retractions.stage(&mut batch, schema, retraction_cursors)?;
737
738 let new = Watermarks {
742 tx_seq_lo: tx_hi.max(tx_lo),
743 checkpoint_lo: checkpoint_lo.max(cursor.checkpoint_lo),
744 };
745 let (k, v) = pruning_watermark::store(&new);
746 batch.put(&schema.pruning_watermark, &k, &v)?;
747 batch.commit()?;
748
749 retractions.commit_to_cursors(retraction_cursors);
750
751 schema.set_pruning_floor(new.tx_seq_lo);
757
758 Ok(())
759}
760
761pub fn embedded_prunable_checkpoint(db: &Db) -> anyhow::Result<Option<u64>> {
783 let framework = db.framework();
784 let mut min_hi: Option<u64> = None;
785 for name in LIVE_COHORT.iter().chain(HISTORY_COHORT) {
786 let key = PipelineTaskKey::new(*name);
787 let Some(watermark) = framework
788 .watermarks
789 .get(&key)
790 .with_context(|| format!("reading watermark for {name}"))?
791 else {
792 return Ok(None);
793 };
794 let hi = watermark.checkpoint_hi_inclusive;
795 min_hi = Some(min_hi.map_or(hi, |m| m.min(hi)));
796 }
797 Ok(min_hi)
798}
799
800fn current_committed_epoch(db: &Db) -> anyhow::Result<Option<u64>> {
808 let framework = FrameworkSchema::new(db.clone());
809 let mut min_epoch: Option<u64> = None;
810 for entry in framework.watermarks.iter(..)? {
811 let (_, watermark) = entry?;
812 let epoch = watermark.epoch_hi_inclusive;
813 min_epoch = Some(min_epoch.map_or(epoch, |m| m.min(epoch)));
814 }
815 Ok(min_epoch)
816}
817
818fn retention_checkpoint_floor(
825 schema: &RpcStoreSchema,
826 current_epoch: u64,
827 retention_epochs: u64,
828) -> anyhow::Result<Option<u64>> {
829 debug_assert!(retention_epochs >= 1, "validated in start_pruner");
830
831 let oldest_retained = current_epoch.saturating_sub(retention_epochs - 1);
833 if oldest_retained == 0 {
834 return Ok(None);
836 }
837
838 let Some(info) = schema.get_epoch(oldest_retained)? else {
839 return Ok(None);
840 };
841 Ok(info.start_checkpoint)
842}
843
844fn clamp_to_snapshot(retention_lo: u64, oldest_snapshot: Option<u64>) -> u64 {
851 match oldest_snapshot {
852 Some(snap) => retention_lo.min(snap),
853 None => retention_lo,
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use std::sync::Arc;
860
861 use prometheus::Registry;
862 use sui_consistent_store::Db;
863 use sui_consistent_store::DbOptions;
864 use sui_consistent_store::PipelineTaskKey;
865 use sui_consistent_store::Watermark;
866 use sui_indexer_alt_framework::pipeline::Processor;
867 use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
868
869 use super::*;
870 use crate::schema::epochs;
871 use crate::schema::primitives::U64Varint;
872
873 fn fresh_db() -> (tempfile::TempDir, Db, RpcStoreSchema) {
874 let dir = tempfile::tempdir().unwrap();
875 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
876 (dir, db, schema)
877 }
878
879 fn stamp_watermarks(db: &Db, names: &[&str], hi: u64) {
881 let framework = FrameworkSchema::new(db.clone());
882 let mut batch = db.batch();
883 for name in names {
884 batch
885 .put(
886 &framework.watermarks,
887 &PipelineTaskKey::new(*name),
888 &Watermark::for_checkpoint(hi),
889 )
890 .unwrap();
891 }
892 batch.commit().unwrap();
893 }
894
895 #[test]
899 fn embedded_prunable_checkpoint_is_min_across_cohorts() {
900 let (_dir, db, _schema) = fresh_db();
901
902 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
904
905 stamp_watermarks(&db, LIVE_COHORT, 1_000);
909 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
910
911 stamp_watermarks(&db, HISTORY_COHORT, 40);
914 stamp_watermarks(&db, &[HISTORY_COHORT[0]], 25);
915 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(25));
916
917 stamp_watermarks(&db, HISTORY_COHORT, 2_000);
920 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(1_000));
921 }
922
923 async fn seed(
930 db: &Db,
931 schema: &RpcStoreSchema,
932 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
933 ) {
934 let mut batch = db.batch();
935 for row in crate::indexer::objects::Objects
936 .process(checkpoint)
937 .await
938 .unwrap()
939 {
940 batch
941 .put(
942 &schema.objects,
943 &objects::Key {
944 id: row.id,
945 version: row.version,
946 },
947 &row.value,
948 )
949 .unwrap();
950 }
951 for row in crate::indexer::effects::Effects
952 .process(checkpoint)
953 .await
954 .unwrap()
955 {
956 batch
957 .put(&schema.effects, &U64Be(row.tx_seq), &row.value)
958 .unwrap();
959 }
960 for row in crate::indexer::checkpoint_summary::CheckpointSummary
961 .process(checkpoint)
962 .await
963 .unwrap()
964 {
965 batch
966 .put(&schema.checkpoint_summary, &U64Be(row.seq), &row.value)
967 .unwrap();
968 }
969 for row in crate::indexer::tx_seq_by_digest::TxSeqByDigest
970 .process(checkpoint)
971 .await
972 .unwrap()
973 {
974 batch
975 .put(
976 &schema.tx_seq_by_digest,
977 &tx_seq_by_digest::Key(row.digest),
978 &U64Varint(row.tx_seq),
979 )
980 .unwrap();
981 }
982 for row in crate::indexer::checkpoint_seq_by_digest::CheckpointSeqByDigest
983 .process(checkpoint)
984 .await
985 .unwrap()
986 {
987 batch
988 .put(
989 &schema.checkpoint_seq_by_digest,
990 &checkpoint_seq_by_digest::Key(row.digest),
991 &U64Varint(row.seq),
992 )
993 .unwrap();
994 }
995 batch.commit().unwrap();
996 }
997
998 fn seed_checkpoint_versions(
999 db: &Db,
1000 schema: &RpcStoreSchema,
1001 id: ObjectID,
1002 rows: &[(u64, u64)],
1003 ) {
1004 let mut batch = db.batch();
1005 for &(checkpoint, version) in rows {
1006 let (k, v) = object_version_by_checkpoint::store(
1007 id,
1008 checkpoint,
1009 sui_types::base_types::SequenceNumber::from_u64(version),
1010 );
1011 batch
1012 .put(&schema.object_version_by_checkpoint, &k, &v)
1013 .unwrap();
1014 }
1015 batch.commit().unwrap();
1016 }
1017
1018 #[test]
1019 fn retractions_stage_widest_range_per_object() {
1020 let (_dir, db, schema) = fresh_db();
1021 let obj = TestCheckpointBuilder::derive_object_id(0);
1022 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (1, 2), (2, 3)]);
1023
1024 let mut retractions = Retractions::default();
1025 retractions.record(obj, 1, true);
1026 retractions.record(obj, 2, false);
1027 let mut batch = db.batch();
1028 retractions
1029 .stage(&mut batch, &schema, &RetractionCursors::default())
1030 .unwrap();
1031 batch.commit().unwrap();
1032
1033 assert_eq!(
1034 schema.get_object_version_at_checkpoint(obj, 1).unwrap(),
1035 None,
1036 "the widest range must cover lower-checkpoint removals",
1037 );
1038 assert_eq!(
1039 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1040 Some(sui_types::base_types::SequenceNumber::from_u64(3)),
1041 "a lower removed=true retraction must not delete the latest anchor",
1042 );
1043 }
1044
1045 #[test]
1046 fn retractions_or_removed_on_equal_checkpoint() {
1047 let (_dir, db, schema) = fresh_db();
1048 let obj = TestCheckpointBuilder::derive_object_id(0);
1049 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (2, 3)]);
1050
1051 let mut retractions = Retractions::default();
1052 retractions.record(obj, 2, false);
1053 retractions.record(obj, 2, true);
1054 let mut batch = db.batch();
1055 retractions
1056 .stage(&mut batch, &schema, &RetractionCursors::default())
1057 .unwrap();
1058 batch.commit().unwrap();
1059
1060 assert_eq!(
1061 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
1062 None,
1063 "same-checkpoint removals must drop the checkpoint row",
1064 );
1065 }
1066
1067 #[test]
1074 fn retraction_point_deletes_deep_history() {
1075 let (_dir, db, schema) = fresh_db();
1076 let obj = TestCheckpointBuilder::derive_object_id(0);
1077 let neighbor = TestCheckpointBuilder::derive_object_id(1);
1078
1079 let rows: Vec<(u64, u64)> = (0..1_000u64).map(|c| (c, c + 1)).collect();
1083 seed_checkpoint_versions(&db, &schema, obj, &rows);
1084 seed_checkpoint_versions(&db, &schema, neighbor, &[(10, 42)]);
1085
1086 let mut retractions = Retractions::default();
1090 for (checkpoint, _) in &rows {
1091 retractions.record(obj, *checkpoint, false);
1092 }
1093 let mut batch = db.batch();
1094 retractions
1095 .stage(&mut batch, &schema, &RetractionCursors::default())
1096 .unwrap();
1097 batch.commit().unwrap();
1098
1099 assert_eq!(
1102 schema.get_object_version_at_checkpoint(obj, 998).unwrap(),
1103 None,
1104 "history below the coalesced checkpoint must be fully point-deleted",
1105 );
1106 assert_eq!(
1107 schema.get_object_version_at_checkpoint(obj, 999).unwrap(),
1108 Some(sui_types::base_types::SequenceNumber::from_u64(1_000)),
1109 "the anchor at the coalesced checkpoint must survive",
1110 );
1111 let remaining: Vec<u64> = schema
1112 .iter_object_versions_by_checkpoint(obj)
1113 .unwrap()
1114 .map(|r| r.unwrap().0.checkpoint)
1115 .collect();
1116 assert_eq!(remaining, vec![999], "only the anchor row remains");
1117
1118 assert_eq!(
1120 schema
1121 .get_object_version_at_checkpoint(neighbor, 10)
1122 .unwrap(),
1123 Some(sui_types::base_types::SequenceNumber::from_u64(42)),
1124 "the bounded scan must not delete a neighboring object's rows",
1125 );
1126 }
1127
1128 #[test]
1129 fn clamp_to_snapshot_holds_floor_at_or_below_oldest_snapshot() {
1130 assert_eq!(clamp_to_snapshot(100, None), 100);
1132 assert_eq!(clamp_to_snapshot(100, Some(250)), 100);
1134 assert_eq!(clamp_to_snapshot(300, Some(250)), 250);
1136 assert_eq!(clamp_to_snapshot(250, Some(250)), 250);
1138 }
1139
1140 #[test]
1141 fn retention_floor_none_when_chain_younger_than_window() {
1142 let (_dir, _db, schema) = fresh_db();
1143 assert!(retention_checkpoint_floor(&schema, 2, 5).unwrap().is_none());
1146 }
1147
1148 #[test]
1149 fn retention_floor_is_start_checkpoint_of_oldest_retained_epoch() {
1150 let (_dir, db, schema) = fresh_db();
1151 let mut batch = db.batch();
1153 batch
1154 .merge(
1155 &schema.epochs,
1156 &U64Be(3),
1157 &epochs::start(1, 1, 0, Some(300), None),
1158 )
1159 .unwrap();
1160 batch.commit().unwrap();
1161 assert_eq!(
1164 retention_checkpoint_floor(&schema, 5, 3).unwrap(),
1165 Some(300)
1166 );
1167 }
1168
1169 #[test]
1170 fn retention_floor_none_when_oldest_epoch_row_missing() {
1171 let (_dir, _db, schema) = fresh_db();
1172 assert!(
1174 retention_checkpoint_floor(&schema, 10, 2)
1175 .unwrap()
1176 .is_none()
1177 );
1178 }
1179
1180 #[test]
1181 fn current_committed_epoch_takes_min_across_watermarks() {
1182 let (_dir, db, _schema) = fresh_db();
1183 let framework = FrameworkSchema::new(db.clone());
1184 let mut batch = db.batch();
1185 batch
1186 .put(
1187 &framework.watermarks,
1188 &PipelineTaskKey::new("a"),
1189 &Watermark {
1190 epoch_hi_inclusive: 7,
1191 ..Default::default()
1192 },
1193 )
1194 .unwrap();
1195 batch
1196 .put(
1197 &framework.watermarks,
1198 &PipelineTaskKey::new("b"),
1199 &Watermark {
1200 epoch_hi_inclusive: 5,
1201 ..Default::default()
1202 },
1203 )
1204 .unwrap();
1205 batch.commit().unwrap();
1206 assert_eq!(current_committed_epoch(&db).unwrap(), Some(5));
1207 }
1208
1209 #[test]
1210 fn current_committed_epoch_none_when_no_watermarks() {
1211 let (_dir, db, _schema) = fresh_db();
1212 assert!(current_committed_epoch(&db).unwrap().is_none());
1213 }
1214
1215 #[test]
1219 fn merge_written_bitmap_buckets_require_two_compactions_for_reclamation() {
1220 let (_dir, db, schema) = fresh_db();
1221 let dimension = b"sender:alice".to_vec();
1222 let floor = transaction_bitmap::TX_BUCKET_SIZE;
1223 let retained_tx_seq = floor + 5;
1224
1225 let (tx_low_key, tx_low_value) = transaction_bitmap::store_match(dimension.clone(), 5);
1226 let (tx_high_key, tx_high_value) =
1227 transaction_bitmap::store_match(dimension.clone(), retained_tx_seq);
1228 let (event_low_key, event_low_value) = event_bitmap::store_match(dimension.clone(), 5, 0);
1229 let (event_high_key, event_high_value) =
1230 event_bitmap::store_match(dimension.clone(), retained_tx_seq, 0);
1231
1232 let mut batch = db.batch();
1233 batch
1234 .merge(&schema.transaction_bitmap, &tx_low_key, &tx_low_value)
1235 .unwrap();
1236 batch
1237 .merge(&schema.transaction_bitmap, &tx_high_key, &tx_high_value)
1238 .unwrap();
1239 batch
1240 .merge(&schema.event_bitmap, &event_low_key, &event_low_value)
1241 .unwrap();
1242 batch
1243 .merge(&schema.event_bitmap, &event_high_key, &event_high_value)
1244 .unwrap();
1245 batch.commit().unwrap();
1246 db.flush().unwrap();
1247
1248 let (watermark_key, watermark_value) = pruning_watermark::store(&Watermarks {
1249 tx_seq_lo: floor,
1250 checkpoint_lo: 1,
1251 });
1252 let mut batch = db.batch();
1253 batch
1254 .put(&schema.pruning_watermark, &watermark_key, &watermark_value)
1255 .unwrap();
1256 batch.commit().unwrap();
1257 schema.set_pruning_floor(floor);
1258
1259 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1260 .unwrap();
1261 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1262
1263 assert!(
1264 schema
1265 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1266 .unwrap()
1267 .is_some()
1268 );
1269 assert!(
1270 schema
1271 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1272 .unwrap()
1273 .is_some()
1274 );
1275 assert!(
1276 schema
1277 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1278 .unwrap()
1279 .is_some()
1280 );
1281 assert!(
1282 schema
1283 .get_event_bitmap(dimension.clone(), event_high_key.bucket)
1284 .unwrap()
1285 .is_some()
1286 );
1287
1288 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1289 .unwrap();
1290 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1291
1292 assert!(
1293 schema
1294 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1295 .unwrap()
1296 .is_none()
1297 );
1298 assert!(
1299 schema
1300 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1301 .unwrap()
1302 .is_some()
1303 );
1304 assert!(
1305 schema
1306 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1307 .unwrap()
1308 .is_none()
1309 );
1310 assert!(
1311 schema
1312 .get_event_bitmap(dimension, event_high_key.bucket)
1313 .unwrap()
1314 .is_some()
1315 );
1316 }
1317
1318 #[tokio::test]
1321 async fn prune_chunk_publishes_the_db_local_bitmap_floor() {
1322 let (_dir, db, schema) = fresh_db();
1323 let checkpoint = Arc::new(
1324 TestCheckpointBuilder::new(0)
1325 .start_transaction(0)
1326 .create_owned_object(0)
1327 .finish_transaction()
1328 .start_transaction(0)
1329 .transfer_object(0, 1)
1330 .finish_transaction()
1331 .build_checkpoint(),
1332 );
1333 seed(&db, &schema, &checkpoint).await;
1334
1335 let metrics = PrunerMetrics::new(None, &Registry::new());
1336 let new = prune_chunk(
1337 &db,
1338 &schema,
1339 &mut RetractionCursors::default(),
1340 Watermarks::default(),
1341 1,
1342 &metrics,
1343 )
1344 .unwrap();
1345 assert_eq!(
1346 schema.current_pruning_floor(),
1347 new.tx_seq_lo,
1348 "the chunk must publish its committed tx_seq floor",
1349 );
1350 }
1351
1352 #[test]
1353 fn start_pruner_rejects_zero_retention() {
1354 let (_dir, db, schema) = fresh_db();
1355 let store = Store::new(db, Arc::new(schema));
1356 let config = PrunerConfig {
1357 retention_epochs: 0,
1358 ..PrunerConfig::default()
1359 };
1360 let err =
1361 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1362 assert!(
1363 format!("{err:#}").contains("retention_epochs"),
1364 "expected a retention_epochs validation error, got: {err:#}",
1365 );
1366 }
1367
1368 #[test]
1369 fn start_pruner_rejects_zero_checkpoints_per_tick() {
1370 let (_dir, db, schema) = fresh_db();
1371 let store = Store::new(db, Arc::new(schema));
1372 let config = PrunerConfig {
1373 max_checkpoints_per_tick: 0,
1374 ..PrunerConfig::default()
1375 };
1376 let err =
1377 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1378 assert!(
1379 format!("{err:#}").contains("max_checkpoints_per_tick"),
1380 "expected a max_checkpoints_per_tick validation error, got: {err:#}",
1381 );
1382 }
1383
1384 #[tokio::test]
1392 async fn prune_once_advances_at_most_the_per_tick_budget() {
1393 let (_dir, db, schema) = fresh_db();
1394
1395 let mut builder = TestCheckpointBuilder::new(0);
1399 let mut checkpoints = Vec::new();
1400 for i in 0..5u64 {
1401 builder = builder
1402 .start_transaction(0)
1403 .create_owned_object(i)
1404 .finish_transaction();
1405 checkpoints.push(Arc::new(builder.build_checkpoint()));
1406 }
1407 for cp in &checkpoints {
1408 seed(&db, &schema, cp).await;
1409 }
1410
1411 let framework = FrameworkSchema::new(db.clone());
1416 let mut batch = db.batch();
1417 batch
1418 .put(
1419 &framework.watermarks,
1420 &PipelineTaskKey::new("p"),
1421 &Watermark {
1422 epoch_hi_inclusive: 2,
1423 ..Default::default()
1424 },
1425 )
1426 .unwrap();
1427 batch
1428 .merge(
1429 &schema.epochs,
1430 &U64Be(2),
1431 &epochs::start(1, 1, 0, Some(5), None),
1432 )
1433 .unwrap();
1434 batch.commit().unwrap();
1435
1436 let config = PrunerConfig {
1437 retention_epochs: 1,
1438 interval_ms: 1,
1439 max_chunk_checkpoints: 2,
1440 max_checkpoints_per_tick: 2,
1441 };
1442 let metrics = PrunerMetrics::new(None, &Registry::new());
1443
1444 let floor = |schema: &RpcStoreSchema| {
1445 schema
1446 .get_pruning_watermarks()
1447 .unwrap()
1448 .unwrap_or_default()
1449 .checkpoint_lo
1450 };
1451
1452 let mut cursors = RetractionCursors::default();
1454 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1455 assert_eq!(floor(&schema), 2, "first tick advances by the budget");
1456 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1457 assert_eq!(floor(&schema), 4, "second tick advances by the budget");
1458 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1459 assert_eq!(floor(&schema), 5, "third tick reaches the target");
1460 assert!(schema.get_effects(4).unwrap().is_none());
1463 assert!(schema.get_checkpoint_summary(4).unwrap().is_none());
1464 prune_once(&db, &schema, &mut cursors, &config, &metrics).unwrap();
1465 assert_eq!(floor(&schema), 5, "a pass at the target is a no-op");
1466 }
1467
1468 #[tokio::test]
1475 async fn prune_chunk_deletes_history_and_preserves_live_object() {
1476 let (_dir, db, schema) = fresh_db();
1477
1478 let checkpoint = Arc::new(
1479 TestCheckpointBuilder::new(0)
1480 .start_transaction(0)
1481 .create_owned_object(0)
1482 .finish_transaction()
1483 .start_transaction(0)
1484 .transfer_object(0, 1)
1485 .finish_transaction()
1486 .build_checkpoint(),
1487 );
1488
1489 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1490 let v_a = checkpoint.transactions[0].effects.lamport_version();
1491 let v_b = checkpoint.transactions[1].effects.lamport_version();
1492 assert_ne!(v_a, v_b, "the transfer must bump the object's version");
1493 let digest0 = *checkpoint.transactions[0].effects.transaction_digest();
1494 let digest1 = *checkpoint.transactions[1].effects.transaction_digest();
1495 let ckpt_digest = checkpoint.summary.data().digest();
1496
1497 seed(&db, &schema, &checkpoint).await;
1498
1499 assert!(schema.get_object_by_key(obj0, v_a).unwrap().is_some());
1501 assert!(schema.get_object_by_key(obj0, v_b).unwrap().is_some());
1502 assert!(schema.get_effects(0).unwrap().is_some());
1503 assert!(schema.get_effects(1).unwrap().is_some());
1504 assert!(schema.get_checkpoint_summary(0).unwrap().is_some());
1505
1506 let metrics = PrunerMetrics::new(None, &Registry::new());
1508 let new = prune_chunk(
1509 &db,
1510 &schema,
1511 &mut RetractionCursors::default(),
1512 Watermarks::default(),
1513 1,
1514 &metrics,
1515 )
1516 .unwrap();
1517 assert_eq!(
1518 new,
1519 Watermarks {
1520 tx_seq_lo: 2,
1521 checkpoint_lo: 1,
1522 },
1523 );
1524
1525 assert!(
1527 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1528 "superseded version v_a should be pruned",
1529 );
1530 assert!(
1531 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1532 "live version v_b must be preserved",
1533 );
1534
1535 assert!(schema.get_effects(0).unwrap().is_none());
1537 assert!(schema.get_effects(1).unwrap().is_none());
1538 assert!(schema.get_checkpoint_summary(0).unwrap().is_none());
1539
1540 assert!(
1542 schema
1543 .tx_seq_by_digest
1544 .get(&tx_seq_by_digest::Key(digest0))
1545 .unwrap()
1546 .is_none()
1547 );
1548 assert!(
1549 schema
1550 .tx_seq_by_digest
1551 .get(&tx_seq_by_digest::Key(digest1))
1552 .unwrap()
1553 .is_none()
1554 );
1555 assert!(
1556 schema
1557 .checkpoint_seq_by_digest
1558 .get(&checkpoint_seq_by_digest::Key(ckpt_digest))
1559 .unwrap()
1560 .is_none()
1561 );
1562
1563 assert_eq!(
1565 schema.get_pruning_watermarks().unwrap().unwrap(),
1566 Watermarks {
1567 tx_seq_lo: 2,
1568 checkpoint_lo: 1,
1569 },
1570 );
1571 }
1572
1573 #[tokio::test]
1582 async fn prune_chunk_retracts_version_only_when_superseding_tx_is_pruned() {
1583 let (_dir, db, schema) = fresh_db();
1584
1585 let mut builder = TestCheckpointBuilder::new(0)
1588 .start_transaction(0)
1589 .create_owned_object(0)
1590 .finish_transaction();
1591 let cp0 = Arc::new(builder.build_checkpoint());
1592 builder = builder
1593 .start_transaction(0)
1594 .transfer_object(0, 1)
1595 .finish_transaction();
1596 let cp1 = Arc::new(builder.build_checkpoint());
1597
1598 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1599 let v_a = cp0.transactions[0].effects.lamport_version();
1600 let v_b = cp1.transactions[0].effects.lamport_version();
1601 assert_ne!(v_a, v_b);
1602
1603 seed(&db, &schema, &cp0).await;
1604 seed(&db, &schema, &cp1).await;
1605 let metrics = PrunerMetrics::new(None, &Registry::new());
1606 let mut cursors = RetractionCursors::default();
1607
1608 let after_first = prune_chunk(
1611 &db,
1612 &schema,
1613 &mut cursors,
1614 Watermarks::default(),
1615 1,
1616 &metrics,
1617 )
1618 .unwrap();
1619 assert_eq!(
1620 after_first,
1621 Watermarks {
1622 tx_seq_lo: 1,
1623 checkpoint_lo: 1,
1624 },
1625 );
1626 assert!(schema.get_effects(0).unwrap().is_none());
1627 assert!(schema.get_effects(1).unwrap().is_some());
1628 assert!(
1629 schema.get_object_by_key(obj0, v_a).unwrap().is_some(),
1630 "v_a must survive while its superseding tx is still retained",
1631 );
1632
1633 let after_second =
1636 prune_chunk(&db, &schema, &mut cursors, after_first, 2, &metrics).unwrap();
1637 assert_eq!(
1638 after_second,
1639 Watermarks {
1640 tx_seq_lo: 2,
1641 checkpoint_lo: 2,
1642 },
1643 );
1644 assert!(schema.get_effects(1).unwrap().is_none());
1645 assert!(
1646 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1647 "v_a must be retracted once its superseding tx is pruned",
1648 );
1649 assert!(
1650 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1651 "live v_b must be preserved",
1652 );
1653 }
1654
1655 #[tokio::test]
1666 async fn prune_chunk_retracts_object_version_by_checkpoint() {
1667 use crate::indexer::object_version_by_checkpoint::ObjectVersionByCheckpoint;
1668
1669 let (_dir, db, schema) = fresh_db();
1670
1671 let mut builder = TestCheckpointBuilder::new(0)
1672 .start_transaction(0)
1673 .create_owned_object(0)
1674 .finish_transaction();
1675 let cp0 = Arc::new(builder.build_checkpoint());
1676 builder = builder
1677 .start_transaction(0)
1678 .transfer_object(0, 1)
1679 .finish_transaction();
1680 let cp1 = Arc::new(builder.build_checkpoint());
1681
1682 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1683 let v_a = cp0.transactions[0].effects.lamport_version();
1684 let v_b = cp1.transactions[0].effects.lamport_version();
1685 assert_ne!(v_a, v_b);
1686
1687 for cp in [&cp0, &cp1] {
1692 seed(&db, &schema, cp).await;
1693 let mut batch = db.batch();
1694 for row in ObjectVersionByCheckpoint::default()
1695 .process(cp)
1696 .await
1697 .unwrap()
1698 {
1699 let crate::indexer::object_version_by_checkpoint::Row::Change {
1702 id,
1703 checkpoint,
1704 version,
1705 } = row
1706 else {
1707 continue;
1708 };
1709 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, version);
1710 batch
1711 .put(&schema.object_version_by_checkpoint, &k, &v)
1712 .unwrap();
1713 }
1714 batch.commit().unwrap();
1715 }
1716
1717 assert_eq!(
1719 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1720 Some(v_a),
1721 );
1722 assert_eq!(
1723 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1724 Some(v_b),
1725 );
1726
1727 let metrics = PrunerMetrics::new(None, &Registry::new());
1728 let mut cursors = RetractionCursors::default();
1729
1730 let after_first = prune_chunk(
1733 &db,
1734 &schema,
1735 &mut cursors,
1736 Watermarks::default(),
1737 1,
1738 &metrics,
1739 )
1740 .unwrap();
1741 assert_eq!(
1742 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1743 Some(v_a),
1744 "cp0-pinned entry must survive while its superseding tx is retained",
1745 );
1746
1747 prune_chunk(&db, &schema, &mut cursors, after_first, 2, &metrics).unwrap();
1750 assert_eq!(
1751 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1752 None,
1753 "cp0-pinned entry must be retracted once its superseding tx is pruned",
1754 );
1755 assert_eq!(
1756 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1757 Some(v_b),
1758 "cp1-pinned floor entry must be preserved",
1759 );
1760 }
1761
1762 #[test]
1767 fn prune_history_cohort_retracts_object_version_by_checkpoint() {
1768 let (_dir, db, schema) = fresh_db();
1769
1770 let mut builder = TestCheckpointBuilder::new(0)
1773 .start_transaction(0)
1774 .create_owned_object(0)
1775 .create_owned_object(1)
1776 .finish_transaction();
1777 let cp0 = Arc::new(builder.build_checkpoint());
1778 builder = builder
1779 .start_transaction(0)
1780 .transfer_object(0, 1)
1781 .finish_transaction()
1782 .start_transaction(0)
1783 .delete_object(1)
1784 .finish_transaction();
1785 let cp1 = Arc::new(builder.build_checkpoint());
1786
1787 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1788 let obj1 = TestCheckpointBuilder::derive_object_id(1);
1789
1790 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
1794 let mut batch = db.batch();
1795 for (id, checkpoint, version) in [(obj0, 0, 1), (obj0, 1, 2), (obj1, 0, 1), (obj1, 1, 2)] {
1796 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, ver(version));
1797 batch
1798 .put(&schema.object_version_by_checkpoint, &k, &v)
1799 .unwrap();
1800 }
1801 batch.commit().unwrap();
1802
1803 assert_eq!(
1805 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1806 Some(ver(1)),
1807 );
1808 assert_eq!(
1809 schema.get_object_version_at_checkpoint(obj1, 0).unwrap(),
1810 Some(ver(1)),
1811 );
1812
1813 let effects: Vec<(u64, TransactionEffects)> = cp0
1818 .transactions
1819 .iter()
1820 .map(|tx| (0u64, tx.effects.clone()))
1821 .chain(cp1.transactions.iter().map(|tx| (1u64, tx.effects.clone())))
1822 .collect();
1823 prune_history_cohort(
1824 &db,
1825 &schema,
1826 &mut RetractionCursors::default(),
1827 1,
1828 0,
1829 &effects,
1830 )
1831 .unwrap();
1832 assert_eq!(
1835 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1836 None,
1837 "a superseded object's pre-anchor row must be retracted",
1838 );
1839 assert_eq!(
1840 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
1841 Some(ver(2)),
1842 "the anchor a read at the floor resolves to must survive",
1843 );
1844
1845 assert_eq!(
1847 schema.get_object_version_at_checkpoint(obj1, 2).unwrap(),
1848 None,
1849 "a removed object's rows must be dropped entirely",
1850 );
1851
1852 assert_eq!(
1854 schema
1855 .get_pruning_watermarks()
1856 .unwrap()
1857 .unwrap()
1858 .checkpoint_lo,
1859 2,
1860 );
1861 }
1862
1863 #[test]
1869 fn prune_history_cohort_deletes_tx_metadata_and_advances_floor() {
1870 use sui_types::digests::TransactionDigest;
1871
1872 use crate::schema::tx_metadata_by_seq;
1873
1874 let (_dir, db, schema) = fresh_db();
1875
1876 let digests: Vec<TransactionDigest> =
1879 (0u8..6).map(|i| TransactionDigest::new([i; 32])).collect();
1880 let mut batch = db.batch();
1881 for (tx_seq, digest) in digests.iter().enumerate() {
1882 let tx_seq = tx_seq as u64;
1883 batch
1884 .put(
1885 &schema.tx_metadata_by_seq,
1886 &U64Be(tx_seq),
1887 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
1888 digest: *digest,
1889 checkpoint_seq: tx_seq,
1890 ckpt_position: 0,
1891 event_count: 0,
1892 timestamp_ms: 0,
1893 }),
1894 )
1895 .unwrap();
1896 batch
1897 .put(
1898 &schema.tx_seq_by_digest,
1899 &tx_seq_by_digest::Key(*digest),
1900 &U64Varint(tx_seq),
1901 )
1902 .unwrap();
1903 }
1904 batch.commit().unwrap();
1905
1906 prune_history_cohort(&db, &schema, &mut RetractionCursors::default(), 2, 3, &[]).unwrap();
1909 for tx_seq in 0..3 {
1911 assert!(
1912 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_none(),
1913 "tx_metadata {tx_seq} should be pruned",
1914 );
1915 }
1916 for tx_seq in 3..6 {
1917 assert!(
1918 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_some(),
1919 "tx_metadata {tx_seq} should be retained",
1920 );
1921 }
1922
1923 for digest in &digests[0..3] {
1925 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_none());
1926 }
1927 for digest in &digests[3..6] {
1928 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_some());
1929 }
1930
1931 assert_eq!(
1933 schema.get_pruning_watermarks().unwrap(),
1934 Some(Watermarks {
1935 tx_seq_lo: 3,
1936 checkpoint_lo: 3,
1937 }),
1938 );
1939
1940 prune_history_cohort(&db, &schema, &mut RetractionCursors::default(), 2, 3, &[]).unwrap();
1942 assert_eq!(
1943 schema.get_pruning_watermarks().unwrap(),
1944 Some(Watermarks {
1945 tx_seq_lo: 3,
1946 checkpoint_lo: 3,
1947 }),
1948 );
1949 }
1950
1951 #[test]
1956 fn prune_history_cohort_handles_sparse_tx_seqs() {
1957 use sui_types::digests::TransactionDigest;
1958
1959 use crate::schema::tx_metadata_by_seq;
1960
1961 let (_dir, db, schema) = fresh_db();
1962
1963 let entries = [
1965 (0u64, [10u8; 32]),
1966 (500_000u64, [11u8; 32]),
1967 (999_999u64, [12u8; 32]),
1968 ];
1969 let mut batch = db.batch();
1970 for (tx_seq, digest_bytes) in entries {
1971 let digest = TransactionDigest::new(digest_bytes);
1972 batch
1973 .put(
1974 &schema.tx_metadata_by_seq,
1975 &U64Be(tx_seq),
1976 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
1977 digest,
1978 checkpoint_seq: tx_seq,
1979 ckpt_position: 0,
1980 event_count: 0,
1981 timestamp_ms: 0,
1982 }),
1983 )
1984 .unwrap();
1985 batch
1986 .put(
1987 &schema.tx_seq_by_digest,
1988 &tx_seq_by_digest::Key(digest),
1989 &U64Varint(tx_seq),
1990 )
1991 .unwrap();
1992 }
1993 batch.commit().unwrap();
1994
1995 prune_history_cohort(
1999 &db,
2000 &schema,
2001 &mut RetractionCursors::default(),
2002 0,
2003 600_000,
2004 &[],
2005 )
2006 .unwrap();
2007 assert!(schema.get_tx_metadata_by_seq(0).unwrap().is_none());
2008 assert!(schema.get_tx_metadata_by_seq(500_000).unwrap().is_none());
2009 assert!(schema.get_tx_metadata_by_seq(999_999).unwrap().is_some());
2010 assert!(
2011 schema
2012 .get_tx_seq_by_digest(&TransactionDigest::new([10u8; 32]))
2013 .unwrap()
2014 .is_none()
2015 );
2016 assert!(
2017 schema
2018 .get_tx_seq_by_digest(&TransactionDigest::new([11u8; 32]))
2019 .unwrap()
2020 .is_none()
2021 );
2022 assert!(
2023 schema
2024 .get_tx_seq_by_digest(&TransactionDigest::new([12u8; 32]))
2025 .unwrap()
2026 .is_some()
2027 );
2028 assert_eq!(
2029 schema.get_pruning_watermarks().unwrap(),
2030 Some(Watermarks {
2031 tx_seq_lo: 600_000,
2032 checkpoint_lo: 1,
2033 }),
2034 );
2035 }
2036 fn all_object_version_by_checkpoint_rows(
2037 schema: &RpcStoreSchema,
2038 ) -> Vec<(
2039 object_version_by_checkpoint::Key,
2040 sui_types::base_types::SequenceNumber,
2041 )> {
2042 schema
2043 .object_version_by_checkpoint
2044 .iter(..)
2045 .unwrap()
2046 .map(|res| {
2047 let (key, value) = res.unwrap();
2048 (
2049 key,
2050 sui_types::base_types::SequenceNumber::from_u64(value.into_inner().version),
2051 )
2052 })
2053 .collect()
2054 }
2055 #[test]
2059 fn prune_history_cohort_differential_with_warm_and_fresh_cursors() {
2060 let (_dir_warm, db_warm, schema_warm) = fresh_db();
2061 let (_dir_fresh, db_fresh, schema_fresh) = fresh_db();
2062
2063 let mut builder = TestCheckpointBuilder::new(0)
2064 .start_transaction(0)
2065 .create_owned_object(0)
2066 .create_owned_object(1)
2067 .create_owned_object(2)
2068 .finish_transaction();
2069 let cp0 = Arc::new(builder.build_checkpoint());
2070
2071 builder = builder
2072 .start_transaction(0)
2073 .transfer_object(0, 1)
2074 .transfer_object(1, 2)
2075 .create_owned_object(3)
2076 .finish_transaction();
2077 let cp1 = Arc::new(builder.build_checkpoint());
2078
2079 builder = builder
2080 .start_transaction(0)
2081 .transfer_object(0, 2)
2082 .delete_object(2)
2083 .create_owned_object(4)
2084 .finish_transaction();
2085 let cp2 = Arc::new(builder.build_checkpoint());
2086
2087 builder = builder
2088 .start_transaction(0)
2089 .transfer_object(0, 3)
2090 .transfer_object(3, 4)
2091 .delete_object(4)
2092 .finish_transaction();
2093 let cp3 = Arc::new(builder.build_checkpoint());
2094
2095 builder = builder
2096 .start_transaction(0)
2097 .transfer_object(0, 4)
2098 .delete_object(1)
2099 .transfer_object(3, 5)
2100 .finish_transaction();
2101 let cp4 = Arc::new(builder.build_checkpoint());
2102
2103 builder = builder
2104 .start_transaction(0)
2105 .transfer_object(0, 5)
2106 .finish_transaction();
2107 let cp5 = Arc::new(builder.build_checkpoint());
2108
2109 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2110 let obj1 = TestCheckpointBuilder::derive_object_id(1);
2111 let obj2 = TestCheckpointBuilder::derive_object_id(2);
2112 let obj3 = TestCheckpointBuilder::derive_object_id(3);
2113 let obj4 = TestCheckpointBuilder::derive_object_id(4);
2114
2115 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2116 let rows = [
2117 (obj0, 0, 1),
2118 (obj0, 1, 2),
2119 (obj0, 2, 3),
2120 (obj0, 3, 4),
2121 (obj0, 4, 5),
2122 (obj0, 5, 6),
2123 (obj1, 0, 1),
2124 (obj1, 1, 2),
2125 (obj1, 4, 3),
2126 (obj2, 0, 1),
2127 (obj2, 2, 2),
2128 (obj3, 1, 1),
2129 (obj3, 3, 2),
2130 (obj3, 4, 3),
2131 (obj4, 2, 1),
2132 (obj4, 3, 2),
2133 ];
2134
2135 for (db, schema) in [(&db_warm, &schema_warm), (&db_fresh, &schema_fresh)] {
2136 let mut batch = db.batch();
2137 for (id, cp, version) in rows {
2138 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(version));
2139 batch
2140 .put(&schema.object_version_by_checkpoint, &k, &v)
2141 .unwrap();
2142 }
2143 batch.commit().unwrap();
2144 }
2145
2146 let checkpoints = [
2147 (&cp0, 0u64),
2148 (&cp1, 1),
2149 (&cp2, 2),
2150 (&cp3, 3),
2151 (&cp4, 4),
2152 (&cp5, 5),
2153 ];
2154
2155 let effects_b1: Vec<(u64, TransactionEffects)> = checkpoints[0..=1]
2157 .iter()
2158 .flat_map(|(cp, seq)| {
2159 cp.transactions
2160 .iter()
2161 .map(move |tx| (*seq, tx.effects.clone()))
2162 })
2163 .collect();
2164 let effects_b2: Vec<(u64, TransactionEffects)> = checkpoints[2..=3]
2166 .iter()
2167 .flat_map(|(cp, seq)| {
2168 cp.transactions
2169 .iter()
2170 .map(move |tx| (*seq, tx.effects.clone()))
2171 })
2172 .collect();
2173 let effects_b3: Vec<(u64, TransactionEffects)> = checkpoints[4..=5]
2175 .iter()
2176 .flat_map(|(cp, seq)| {
2177 cp.transactions
2178 .iter()
2179 .map(move |tx| (*seq, tx.effects.clone()))
2180 })
2181 .collect();
2182
2183 let mut warm_cursors = RetractionCursors::default();
2185 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 1, 0, &effects_b1).unwrap();
2186 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 3, 0, &effects_b2).unwrap();
2187 prune_history_cohort(&db_warm, &schema_warm, &mut warm_cursors, 5, 0, &effects_b3).unwrap();
2188
2189 prune_history_cohort(
2191 &db_fresh,
2192 &schema_fresh,
2193 &mut RetractionCursors::default(),
2194 1,
2195 0,
2196 &effects_b1,
2197 )
2198 .unwrap();
2199 prune_history_cohort(
2200 &db_fresh,
2201 &schema_fresh,
2202 &mut RetractionCursors::default(),
2203 3,
2204 0,
2205 &effects_b2,
2206 )
2207 .unwrap();
2208 prune_history_cohort(
2209 &db_fresh,
2210 &schema_fresh,
2211 &mut RetractionCursors::default(),
2212 5,
2213 0,
2214 &effects_b3,
2215 )
2216 .unwrap();
2217
2218 let rows_warm = all_object_version_by_checkpoint_rows(&schema_warm);
2219 let rows_fresh = all_object_version_by_checkpoint_rows(&schema_fresh);
2220 assert_eq!(rows_warm, rows_fresh);
2221 let obj0_checkpoints: Vec<u64> = rows_warm
2225 .iter()
2226 .filter(|(k, _)| k.id == obj0)
2227 .map(|(k, _)| k.checkpoint)
2228 .collect();
2229 assert_eq!(obj0_checkpoints, vec![5], "obj0 keeps only its cp5 anchor");
2230 }
2231
2232 #[test]
2235 fn prune_history_cohort_simulated_restart() {
2236 let (_dir_uninterrupted, db_uninterrupted, schema_uninterrupted) = fresh_db();
2237 let (_dir_restarted, db_restarted, schema_restarted) = fresh_db();
2238
2239 let mut builder = TestCheckpointBuilder::new(0)
2240 .start_transaction(0)
2241 .create_owned_object(0)
2242 .create_owned_object(1)
2243 .finish_transaction();
2244 let cp0 = Arc::new(builder.build_checkpoint());
2245
2246 builder = builder
2247 .start_transaction(0)
2248 .transfer_object(0, 1)
2249 .transfer_object(1, 2)
2250 .finish_transaction();
2251 let cp1 = Arc::new(builder.build_checkpoint());
2252
2253 builder = builder
2254 .start_transaction(0)
2255 .transfer_object(0, 2)
2256 .delete_object(1)
2257 .finish_transaction();
2258 let cp2 = Arc::new(builder.build_checkpoint());
2259
2260 builder = builder
2261 .start_transaction(0)
2262 .transfer_object(0, 3)
2263 .finish_transaction();
2264 let cp3 = Arc::new(builder.build_checkpoint());
2265
2266 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2267 let obj1 = TestCheckpointBuilder::derive_object_id(1);
2268
2269 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2270 let rows = [
2271 (obj0, 0, 1),
2272 (obj0, 1, 2),
2273 (obj0, 2, 3),
2274 (obj0, 3, 4),
2275 (obj1, 0, 1),
2276 (obj1, 1, 2),
2277 (obj1, 2, 3),
2278 ];
2279
2280 for (db, schema) in [
2281 (&db_uninterrupted, &schema_uninterrupted),
2282 (&db_restarted, &schema_restarted),
2283 ] {
2284 let mut batch = db.batch();
2285 for (id, cp, version) in rows {
2286 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(version));
2287 batch
2288 .put(&schema.object_version_by_checkpoint, &k, &v)
2289 .unwrap();
2290 }
2291 batch.commit().unwrap();
2292 }
2293
2294 let checkpoints = [(&cp0, 0u64), (&cp1, 1), (&cp2, 2), (&cp3, 3)];
2295
2296 let effects_b1: Vec<(u64, TransactionEffects)> = checkpoints[0..=1]
2297 .iter()
2298 .flat_map(|(cp, seq)| {
2299 cp.transactions
2300 .iter()
2301 .map(move |tx| (*seq, tx.effects.clone()))
2302 })
2303 .collect();
2304 let effects_b2: Vec<(u64, TransactionEffects)> = checkpoints[2..=3]
2305 .iter()
2306 .flat_map(|(cp, seq)| {
2307 cp.transactions
2308 .iter()
2309 .map(move |tx| (*seq, tx.effects.clone()))
2310 })
2311 .collect();
2312
2313 let mut uninterrupted_cursors = RetractionCursors::default();
2315 prune_history_cohort(
2316 &db_uninterrupted,
2317 &schema_uninterrupted,
2318 &mut uninterrupted_cursors,
2319 1,
2320 0,
2321 &effects_b1,
2322 )
2323 .unwrap();
2324 prune_history_cohort(
2325 &db_uninterrupted,
2326 &schema_uninterrupted,
2327 &mut uninterrupted_cursors,
2328 3,
2329 0,
2330 &effects_b2,
2331 )
2332 .unwrap();
2333
2334 let mut restarted_cursors = RetractionCursors::default();
2336 prune_history_cohort(
2337 &db_restarted,
2338 &schema_restarted,
2339 &mut restarted_cursors,
2340 1,
2341 0,
2342 &effects_b1,
2343 )
2344 .unwrap();
2345 restarted_cursors = RetractionCursors::default();
2347 prune_history_cohort(
2348 &db_restarted,
2349 &schema_restarted,
2350 &mut restarted_cursors,
2351 3,
2352 0,
2353 &effects_b2,
2354 )
2355 .unwrap();
2356
2357 let rows_uninterrupted = all_object_version_by_checkpoint_rows(&schema_uninterrupted);
2358 let rows_restarted = all_object_version_by_checkpoint_rows(&schema_restarted);
2359 assert_eq!(rows_uninterrupted, rows_restarted);
2360 }
2361
2362 #[test]
2365 fn prune_history_cohort_hot_object_across_batches() {
2366 let (_dir, db, schema) = fresh_db();
2367
2368 let mut builder = TestCheckpointBuilder::new(0)
2369 .start_transaction(0)
2370 .create_owned_object(0)
2371 .finish_transaction();
2372 let cp0 = Arc::new(builder.build_checkpoint());
2373
2374 builder = builder
2375 .start_transaction(0)
2376 .transfer_object(0, 1)
2377 .finish_transaction();
2378 let cp1 = Arc::new(builder.build_checkpoint());
2379
2380 builder = builder
2381 .start_transaction(0)
2382 .transfer_object(0, 2)
2383 .finish_transaction();
2384 let cp2 = Arc::new(builder.build_checkpoint());
2385
2386 builder = builder
2387 .start_transaction(0)
2388 .transfer_object(0, 3)
2389 .finish_transaction();
2390 let cp3 = Arc::new(builder.build_checkpoint());
2391
2392 builder = builder
2393 .start_transaction(0)
2394 .transfer_object(0, 4)
2395 .finish_transaction();
2396 let cp4 = Arc::new(builder.build_checkpoint());
2397
2398 builder = builder
2399 .start_transaction(0)
2400 .transfer_object(0, 5)
2401 .finish_transaction();
2402 let cp5 = Arc::new(builder.build_checkpoint());
2403
2404 let hot_obj = TestCheckpointBuilder::derive_object_id(0);
2405 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2406
2407 let mut batch = db.batch();
2408 for (checkpoint, version) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)] {
2409 let (k, v) = object_version_by_checkpoint::store(hot_obj, checkpoint, ver(version));
2410 batch
2411 .put(&schema.object_version_by_checkpoint, &k, &v)
2412 .unwrap();
2413 }
2414 batch.commit().unwrap();
2415
2416 let mut cursors = RetractionCursors::default();
2417
2418 let effects_b1 = vec![
2420 (0u64, cp0.transactions[0].effects.clone()),
2421 (1u64, cp1.transactions[0].effects.clone()),
2422 ];
2423 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_b1).unwrap();
2424 assert_eq!(cursors.lower_bound(&hot_obj), 1);
2425 assert_eq!(
2426 schema.get_object_version_at_checkpoint(hot_obj, 0).unwrap(),
2427 None
2428 );
2429 assert_eq!(
2430 schema.get_object_version_at_checkpoint(hot_obj, 1).unwrap(),
2431 Some(ver(2))
2432 );
2433
2434 let effects_b2 = vec![
2436 (2u64, cp2.transactions[0].effects.clone()),
2437 (3u64, cp3.transactions[0].effects.clone()),
2438 ];
2439 prune_history_cohort(&db, &schema, &mut cursors, 3, 0, &effects_b2).unwrap();
2440 assert_eq!(cursors.lower_bound(&hot_obj), 3);
2441 assert_eq!(
2442 schema.get_object_version_at_checkpoint(hot_obj, 0).unwrap(),
2443 None
2444 );
2445 assert_eq!(
2446 schema.get_object_version_at_checkpoint(hot_obj, 1).unwrap(),
2447 None
2448 );
2449 assert_eq!(
2450 schema.get_object_version_at_checkpoint(hot_obj, 2).unwrap(),
2451 None
2452 );
2453 assert_eq!(
2454 schema.get_object_version_at_checkpoint(hot_obj, 3).unwrap(),
2455 Some(ver(4))
2456 );
2457
2458 let effects_b3 = vec![
2460 (4u64, cp4.transactions[0].effects.clone()),
2461 (5u64, cp5.transactions[0].effects.clone()),
2462 ];
2463 prune_history_cohort(&db, &schema, &mut cursors, 5, 0, &effects_b3).unwrap();
2464 assert_eq!(cursors.lower_bound(&hot_obj), 5);
2465 assert_eq!(
2466 schema.get_object_version_at_checkpoint(hot_obj, 3).unwrap(),
2467 None
2468 );
2469 assert_eq!(
2470 schema.get_object_version_at_checkpoint(hot_obj, 4).unwrap(),
2471 None
2472 );
2473 assert_eq!(
2474 schema.get_object_version_at_checkpoint(hot_obj, 6).unwrap(),
2475 Some(ver(6))
2476 );
2477 }
2478
2479 #[test]
2486 fn prune_history_cohort_eviction_fallback() {
2487 let (_dir_tiny, db_tiny, schema_tiny) = fresh_db();
2488 let (_dir_default, db_default, schema_default) = fresh_db();
2489
2490 let mut builder = TestCheckpointBuilder::new(0)
2494 .start_transaction(0)
2495 .create_owned_object(0)
2496 .create_owned_object(1)
2497 .create_owned_object(2)
2498 .finish_transaction();
2499 let cp0 = builder.build_checkpoint();
2500 builder = builder
2501 .start_transaction(0)
2502 .transfer_object(0, 1)
2503 .transfer_object(1, 1)
2504 .transfer_object(2, 1)
2505 .finish_transaction();
2506 let cp1 = builder.build_checkpoint();
2507 builder = builder
2508 .start_transaction(0)
2509 .transfer_object(0, 2)
2510 .transfer_object(1, 2)
2511 .transfer_object(2, 2)
2512 .finish_transaction();
2513 let cp2 = builder.build_checkpoint();
2514 builder = builder
2515 .start_transaction(0)
2516 .transfer_object(0, 3)
2517 .transfer_object(1, 3)
2518 .transfer_object(2, 3)
2519 .finish_transaction();
2520 let cp3 = builder.build_checkpoint();
2521
2522 let objs: Vec<ObjectID> = (0..3u64)
2523 .map(TestCheckpointBuilder::derive_object_id)
2524 .collect();
2525 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2526
2527 for (db, schema) in [(&db_tiny, &schema_tiny), (&db_default, &schema_default)] {
2528 let mut batch = db.batch();
2529 for &id in &objs {
2530 for cp in 0..4u64 {
2531 let (k, v) = object_version_by_checkpoint::store(id, cp, ver(cp + 1));
2532 batch
2533 .put(&schema.object_version_by_checkpoint, &k, &v)
2534 .unwrap();
2535 }
2536 }
2537 batch.commit().unwrap();
2538 }
2539
2540 let effects_b1: Vec<(u64, TransactionEffects)> = [(0u64, &cp0), (1, &cp1)]
2541 .into_iter()
2542 .flat_map(|(seq, cp)| {
2543 cp.transactions
2544 .iter()
2545 .map(move |tx| (seq, tx.effects.clone()))
2546 })
2547 .collect();
2548 let effects_b2: Vec<(u64, TransactionEffects)> = [(2u64, &cp2), (3, &cp3)]
2549 .into_iter()
2550 .flat_map(|(seq, cp)| {
2551 cp.transactions
2552 .iter()
2553 .map(move |tx| (seq, tx.effects.clone()))
2554 })
2555 .collect();
2556
2557 let mut tiny_cursors = RetractionCursors::with_capacity(1);
2560 prune_history_cohort(&db_tiny, &schema_tiny, &mut tiny_cursors, 1, 0, &effects_b1).unwrap();
2561 assert!(
2562 objs.iter()
2563 .filter(|id| tiny_cursors.lower_bound(id) > 0)
2564 .count()
2565 <= 1,
2566 "capacity 1 must have evicted all but at most one cursor"
2567 );
2568 prune_history_cohort(&db_tiny, &schema_tiny, &mut tiny_cursors, 3, 0, &effects_b2).unwrap();
2569
2570 let mut default_cursors = RetractionCursors::default();
2572 prune_history_cohort(
2573 &db_default,
2574 &schema_default,
2575 &mut default_cursors,
2576 1,
2577 0,
2578 &effects_b1,
2579 )
2580 .unwrap();
2581 for id in &objs {
2582 assert_eq!(
2583 default_cursors.lower_bound(id),
2584 1,
2585 "default capacity keeps every cursor warm"
2586 );
2587 }
2588 prune_history_cohort(
2589 &db_default,
2590 &schema_default,
2591 &mut default_cursors,
2592 3,
2593 0,
2594 &effects_b2,
2595 )
2596 .unwrap();
2597
2598 assert_eq!(tiny_cursors.len(), 1);
2601 assert!(default_cursors.len() > tiny_cursors.len());
2602
2603 let rows_tiny = all_object_version_by_checkpoint_rows(&schema_tiny);
2606 let rows_default = all_object_version_by_checkpoint_rows(&schema_default);
2607 assert_eq!(rows_tiny, rows_default);
2608 let mut expected: Vec<_> = objs
2609 .iter()
2610 .map(|&id| {
2611 (
2612 object_version_by_checkpoint::Key { id, checkpoint: 3 },
2613 ver(4),
2614 )
2615 })
2616 .collect();
2617 expected.sort_by_key(|(k, _)| k.id);
2618 assert_eq!(rows_tiny, expected);
2619 }
2620
2621 #[test]
2625 fn prune_history_cohort_removed_at_or_below_cursor() {
2626 let (_dir, db, schema) = fresh_db();
2627
2628 let mut builder = TestCheckpointBuilder::new(0)
2629 .start_transaction(0)
2630 .create_owned_object(0)
2631 .finish_transaction();
2632 let _cp0 = Arc::new(builder.build_checkpoint());
2633
2634 builder = builder
2635 .start_transaction(0)
2636 .transfer_object(0, 1)
2637 .finish_transaction()
2638 .start_transaction(0)
2639 .delete_object(0)
2640 .finish_transaction();
2641 let cp1 = Arc::new(builder.build_checkpoint());
2642
2643 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2644 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2645
2646 let mut batch = db.batch();
2647 let (k0, v0) = object_version_by_checkpoint::store(obj0, 0, ver(1));
2648 let (k1, v1) = object_version_by_checkpoint::store(obj0, 1, ver(2));
2649 batch
2650 .put(&schema.object_version_by_checkpoint, &k0, &v0)
2651 .unwrap();
2652 batch
2653 .put(&schema.object_version_by_checkpoint, &k1, &v1)
2654 .unwrap();
2655 batch.commit().unwrap();
2656
2657 let mut cursors = RetractionCursors::default();
2658
2659 let effects_supersede: Vec<(u64, TransactionEffects)> =
2661 vec![(1u64, cp1.transactions[0].effects.clone())];
2662 prune_history_cohort(&db, &schema, &mut cursors, 0, 0, &effects_supersede).unwrap();
2663 assert_eq!(cursors.lower_bound(&obj0), 1);
2664 assert_eq!(
2665 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2666 None
2667 );
2668 assert_eq!(
2669 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2670 Some(ver(2))
2671 );
2672
2673 let effects_remove: Vec<(u64, TransactionEffects)> =
2675 vec![(1u64, cp1.transactions[1].effects.clone())];
2676 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_remove).unwrap();
2677 assert_eq!(cursors.lower_bound(&obj0), 1);
2678
2679 assert_eq!(
2681 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2682 None
2683 );
2684 assert_eq!(
2685 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2686 None
2687 );
2688 assert_eq!(
2689 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
2690 None
2691 );
2692
2693 prune_history_cohort(&db, &schema, &mut cursors, 1, 0, &effects_remove).unwrap();
2695 assert_eq!(
2696 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
2697 None
2698 );
2699 assert_eq!(
2700 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
2701 None
2702 );
2703 assert_eq!(
2704 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
2705 None
2706 );
2707 }
2708
2709 #[test]
2715 fn retraction_cursors_hot_object_survives_lru_churn() {
2716 let hot = TestCheckpointBuilder::derive_object_id(0);
2717 let mut cursors = RetractionCursors::with_capacity(4);
2718 for batch in 1..=5u64 {
2719 cursors.advance(hot, batch);
2720 for j in 0..2u64 {
2721 cursors.advance(
2722 TestCheckpointBuilder::derive_object_id(100 + batch * 10 + j),
2723 batch,
2724 );
2725 }
2726 assert_eq!(
2727 cursors.lower_bound(&hot),
2728 batch,
2729 "hot object must survive batch {batch}'s cold churn"
2730 );
2731 }
2732 assert_eq!(
2735 cursors.lower_bound(&TestCheckpointBuilder::derive_object_id(110)),
2736 0
2737 );
2738 assert_eq!(cursors.len(), 4);
2739 }
2740
2741 #[test]
2746 fn prune_history_cohort_stale_floor_redelivery_is_noop() {
2747 let (_dir, db, schema) = fresh_db();
2748
2749 let mut builder = TestCheckpointBuilder::new(0)
2750 .start_transaction(0)
2751 .create_owned_object(0)
2752 .finish_transaction();
2753 let cp0 = builder.build_checkpoint();
2754 builder = builder
2755 .start_transaction(0)
2756 .transfer_object(0, 1)
2757 .finish_transaction();
2758 let cp1 = builder.build_checkpoint();
2759 builder = builder
2760 .start_transaction(0)
2761 .transfer_object(0, 2)
2762 .finish_transaction();
2763 let cp2 = builder.build_checkpoint();
2764 builder = builder
2765 .start_transaction(0)
2766 .transfer_object(0, 3)
2767 .finish_transaction();
2768 let cp3 = builder.build_checkpoint();
2769
2770 let obj0 = TestCheckpointBuilder::derive_object_id(0);
2771 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
2772
2773 let mut batch = db.batch();
2774 for cp in 0..4u64 {
2775 let (k, v) = object_version_by_checkpoint::store(obj0, cp, ver(cp + 1));
2776 batch
2777 .put(&schema.object_version_by_checkpoint, &k, &v)
2778 .unwrap();
2779 }
2780 batch.commit().unwrap();
2781
2782 let effects_all: Vec<(u64, TransactionEffects)> =
2785 [(0u64, &cp0), (1, &cp1), (2, &cp2), (3, &cp3)]
2786 .into_iter()
2787 .flat_map(|(seq, cp)| {
2788 cp.transactions
2789 .iter()
2790 .map(move |tx| (seq, tx.effects.clone()))
2791 })
2792 .collect();
2793 let mut cursors = RetractionCursors::default();
2794 prune_history_cohort(&db, &schema, &mut cursors, 3, 0, &effects_all).unwrap();
2795 assert_eq!(cursors.lower_bound(&obj0), 3);
2796 assert_eq!(
2797 schema.get_object_version_at_checkpoint(obj0, 3).unwrap(),
2798 Some(ver(4))
2799 );
2800
2801 let redelivered: Vec<(u64, TransactionEffects)> =
2805 vec![(1, cp1.transactions[0].effects.clone())];
2806 prune_history_cohort(&db, &schema, &mut cursors, 1, 5, &redelivered).unwrap();
2807
2808 assert_eq!(cursors.lower_bound(&obj0), 3);
2811 assert_eq!(
2812 schema.get_object_version_at_checkpoint(obj0, 3).unwrap(),
2813 Some(ver(4))
2814 );
2815 let rows = all_object_version_by_checkpoint_rows(&schema);
2816 assert_eq!(
2817 rows,
2818 vec![(
2819 object_version_by_checkpoint::Key {
2820 id: obj0,
2821 checkpoint: 3
2822 },
2823 ver(4)
2824 )]
2825 );
2826 }
2827
2828 #[tokio::test]
2831 async fn prune_chunk_standalone_retraction_cursors_and_restart() {
2832 use crate::indexer::object_version_by_checkpoint::ObjectVersionByCheckpoint;
2833
2834 let (_dir1, db1, schema1) = fresh_db();
2835 let (_dir2, db2, schema2) = fresh_db();
2836 let (_dir3, db3, schema3) = fresh_db();
2837
2838 let mut builder = TestCheckpointBuilder::new(0)
2839 .start_transaction(0)
2840 .create_owned_object(0)
2841 .create_owned_object(1)
2842 .finish_transaction();
2843 let cp0 = Arc::new(builder.build_checkpoint());
2844
2845 builder = builder
2846 .start_transaction(0)
2847 .transfer_object(0, 1)
2848 .transfer_object(1, 2)
2849 .finish_transaction();
2850 let cp1 = Arc::new(builder.build_checkpoint());
2851
2852 builder = builder
2853 .start_transaction(0)
2854 .transfer_object(0, 2)
2855 .delete_object(1)
2856 .finish_transaction();
2857 let cp2 = Arc::new(builder.build_checkpoint());
2858
2859 builder = builder
2860 .start_transaction(0)
2861 .transfer_object(0, 3)
2862 .finish_transaction();
2863 let cp3 = Arc::new(builder.build_checkpoint());
2864
2865 let checkpoints = [&cp0, &cp1, &cp2, &cp3];
2866
2867 for (db, schema) in [(&db1, &schema1), (&db2, &schema2), (&db3, &schema3)] {
2868 for cp in &checkpoints {
2869 seed(db, schema, cp).await;
2870 let mut batch = db.batch();
2871 for row in ObjectVersionByCheckpoint::default()
2872 .process(cp)
2873 .await
2874 .unwrap()
2875 {
2876 let crate::indexer::object_version_by_checkpoint::Row::Change {
2877 id,
2878 checkpoint,
2879 version,
2880 } = row
2881 else {
2882 continue;
2883 };
2884 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, version);
2885 batch
2886 .put(&schema.object_version_by_checkpoint, &k, &v)
2887 .unwrap();
2888 }
2889 batch.commit().unwrap();
2890 }
2891 }
2892
2893 let metrics = PrunerMetrics::new(None, &Registry::new());
2894
2895 let mut cursors1 = RetractionCursors::default();
2897 let w1 = prune_chunk(
2898 &db1,
2899 &schema1,
2900 &mut cursors1,
2901 Watermarks::default(),
2902 2,
2903 &metrics,
2904 )
2905 .unwrap();
2906 prune_chunk(&db1, &schema1, &mut cursors1, w1, 4, &metrics).unwrap();
2907
2908 let w2 = prune_chunk(
2910 &db2,
2911 &schema2,
2912 &mut RetractionCursors::default(),
2913 Watermarks::default(),
2914 2,
2915 &metrics,
2916 )
2917 .unwrap();
2918 prune_chunk(
2919 &db2,
2920 &schema2,
2921 &mut RetractionCursors::default(),
2922 w2,
2923 4,
2924 &metrics,
2925 )
2926 .unwrap();
2927
2928 let mut cursors3 = RetractionCursors::default();
2930 let w3 = prune_chunk(
2931 &db3,
2932 &schema3,
2933 &mut cursors3,
2934 Watermarks::default(),
2935 2,
2936 &metrics,
2937 )
2938 .unwrap();
2939 cursors3 = RetractionCursors::default();
2940 prune_chunk(&db3, &schema3, &mut cursors3, w3, 4, &metrics).unwrap();
2941
2942 let rows1 = all_object_version_by_checkpoint_rows(&schema1);
2943 let rows2 = all_object_version_by_checkpoint_rows(&schema2);
2944 let rows3 = all_object_version_by_checkpoint_rows(&schema3);
2945
2946 assert_eq!(rows1, rows2);
2947 assert_eq!(rows1, rows3);
2948 assert!(!rows1.is_empty());
2949 }
2950}