1use super::authority_store_tables::AuthorityPerpetualTables;
5use crate::checkpoints::{CheckpointStore, CheckpointWatermark};
6use anyhow::anyhow;
7use mysten_metrics::monitored_scope;
8#[cfg(not(tidehunter))]
9use mysten_metrics::spawn_monitored_task;
10#[cfg(not(tidehunter))]
11use once_cell::sync::Lazy;
12use prometheus::{
13 IntCounter, IntGauge, Registry, register_int_counter_with_registry,
14 register_int_gauge_with_registry,
15};
16#[cfg(tidehunter)]
17use serde::de::DeserializeOwned;
18#[cfg(not(tidehunter))]
19use std::cmp::max;
20use std::cmp::min;
21#[cfg(not(tidehunter))]
22use std::collections::{BTreeSet, HashMap};
23#[cfg(not(tidehunter))]
24use std::sync::Mutex;
25use std::sync::atomic::AtomicU64;
26#[cfg(not(tidehunter))]
27use std::time::{SystemTime, UNIX_EPOCH};
28use std::{sync::Arc, time::Duration};
29use sui_config::node::AuthorityStorePruningConfig;
30pub use sui_rpc_store::RetractionCursors;
31use sui_rpc_store::Store as RpcStore;
32#[cfg(not(tidehunter))]
33use sui_types::base_types::VersionNumber;
34use sui_types::committee::EpochId;
35use sui_types::effects::TransactionEffects;
36use sui_types::effects::TransactionEffectsAPI;
37use sui_types::message_envelope::Message;
38use sui_types::messages_checkpoint::{
39 CheckpointContents, CheckpointDigest, CheckpointSequenceNumber,
40};
41use sui_types::{
42 base_types::{ObjectID, SequenceNumber, TransactionDigest},
43 storage::ObjectKey,
44};
45use tokio::sync::oneshot::{self, Sender};
46use tokio::time::Instant;
47use tracing::{debug, error, info, warn};
48#[cfg(not(tidehunter))]
49use typed_store::rocksdb::LiveFile;
50use typed_store::{Map, TypedStoreError};
51
52#[cfg(not(tidehunter))]
53static PERIODIC_PRUNING_TABLES: Lazy<BTreeSet<String>> = Lazy::new(|| {
54 [
55 "objects",
56 "effects",
57 "transactions",
58 "events",
59 "executed_effects",
60 "executed_transactions_to_checkpoint",
61 ]
62 .into_iter()
63 .map(|cf| cf.to_string())
64 .collect()
65});
66pub const EPOCH_DURATION_MS_FOR_TESTING: u64 = 24 * 60 * 60 * 1000;
67pub struct AuthorityStorePruner {
68 _objects_pruner_cancel_handle: oneshot::Sender<()>,
69}
70
71#[derive(Default)]
72pub struct PrunerWatermarks {
73 pub epoch_id: Arc<AtomicU64>,
74 pub checkpoint_id: Arc<AtomicU64>,
75}
76
77static MIN_PRUNING_TICK_DURATION_MS: u64 = 10 * 1000;
78
79pub struct AuthorityStorePruningMetrics {
80 pub last_pruned_checkpoint: IntGauge,
81 pub num_pruned_objects: IntCounter,
82 pub num_pruned_tombstones: IntCounter,
83 pub last_pruned_effects_checkpoint: IntGauge,
84 pub num_epochs_to_retain_for_objects: IntGauge,
85 pub num_epochs_to_retain_for_checkpoints: IntGauge,
86 pub last_pruned_executed_tx_digests_epoch: IntGauge,
87}
88
89impl AuthorityStorePruningMetrics {
90 pub fn new(registry: &Registry) -> Arc<Self> {
91 let this = Self {
92 last_pruned_checkpoint: register_int_gauge_with_registry!(
93 "last_pruned_checkpoint",
94 "Last pruned checkpoint",
95 registry
96 )
97 .unwrap(),
98 num_pruned_objects: register_int_counter_with_registry!(
99 "num_pruned_objects",
100 "Number of pruned objects",
101 registry
102 )
103 .unwrap(),
104 num_pruned_tombstones: register_int_counter_with_registry!(
105 "num_pruned_tombstones",
106 "Number of pruned tombstones",
107 registry
108 )
109 .unwrap(),
110 last_pruned_effects_checkpoint: register_int_gauge_with_registry!(
111 "last_pruned_effects_checkpoint",
112 "Last pruned effects checkpoint",
113 registry
114 )
115 .unwrap(),
116 num_epochs_to_retain_for_objects: register_int_gauge_with_registry!(
117 "num_epochs_to_retain_for_objects",
118 "Number of epochs to retain for objects",
119 registry
120 )
121 .unwrap(),
122 num_epochs_to_retain_for_checkpoints: register_int_gauge_with_registry!(
123 "num_epochs_to_retain_for_checkpoints",
124 "Number of epochs to retain for checkpoints",
125 registry
126 )
127 .unwrap(),
128 last_pruned_executed_tx_digests_epoch: register_int_gauge_with_registry!(
129 "last_pruned_executed_tx_digests_epoch",
130 "Highest epoch whose executed_transaction_digests entries have been pruned",
131 registry
132 )
133 .unwrap(),
134 };
135 Arc::new(this)
136 }
137
138 pub fn new_for_test() -> Arc<Self> {
139 Self::new(&Registry::new())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
144pub enum PruningMode {
145 Objects,
146 Checkpoints,
147}
148
149impl AuthorityStorePruner {
150 #[cfg(not(tidehunter))]
152 async fn prune_objects_and_indexes(
153 transaction_effects: Vec<(CheckpointSequenceNumber, TransactionEffects)>,
154 perpetual_db: &Arc<AuthorityPerpetualTables>,
155 checkpoint_number: CheckpointSequenceNumber,
156 metrics: Arc<AuthorityStorePruningMetrics>,
157 pruned_tx_seq_exclusive: u64,
158 rpc_store: Option<&RpcStore>,
159 retraction_cursors: &mut RetractionCursors,
160 enable_pruning_tombstones: bool,
161 ) -> anyhow::Result<()> {
162 let _scope = monitored_scope("ObjectsLivePruner");
163 let mut wb = perpetual_db.objects.batch();
164
165 let mut live_object_keys_to_prune = vec![];
167 let mut object_tombstones_to_prune = vec![];
168 for (_checkpoint, effects) in &transaction_effects {
169 for (object_id, seq_number) in effects.modified_at_versions() {
170 live_object_keys_to_prune.push(ObjectKey(object_id, seq_number));
171 }
172
173 if enable_pruning_tombstones {
174 for deleted_object_key in effects.all_tombstones() {
175 object_tombstones_to_prune
176 .push(ObjectKey(deleted_object_key.0, deleted_object_key.1));
177 }
178 }
179 }
180
181 metrics
182 .num_pruned_objects
183 .inc_by(live_object_keys_to_prune.len() as u64);
184 metrics
185 .num_pruned_tombstones
186 .inc_by(object_tombstones_to_prune.len() as u64);
187
188 let mut updates: HashMap<ObjectID, (VersionNumber, VersionNumber)> = HashMap::new();
189 for ObjectKey(object_id, seq_number) in live_object_keys_to_prune {
190 updates
191 .entry(object_id)
192 .and_modify(|range| *range = (min(range.0, seq_number), max(range.1, seq_number)))
193 .or_insert((seq_number, seq_number));
194 }
195
196 for (object_id, (min_version, max_version)) in updates {
197 debug!(
198 "Pruning object {:?} versions {:?} - {:?}",
199 object_id, min_version, max_version
200 );
201 let start_range = ObjectKey(object_id, min_version);
202 let end_range = ObjectKey(object_id, (max_version.value() + 1).into());
203 wb.schedule_delete_range(&perpetual_db.objects, &start_range, &end_range)?;
204 }
205
206 if !object_tombstones_to_prune.is_empty() {
212 let mut object_keys_to_delete = vec![];
213 for ObjectKey(object_id, seq_number) in object_tombstones_to_prune {
214 for result in perpetual_db.objects.safe_iter_with_bounds(
215 Some(ObjectKey(object_id, VersionNumber::MIN)),
216 Some(ObjectKey(object_id, seq_number.next())),
217 ) {
218 let (object_key, _) = result?;
219 assert_eq!(object_key.0, object_id);
220 object_keys_to_delete.push(object_key);
221 }
222 }
223
224 wb.delete_batch(&perpetual_db.objects, object_keys_to_delete)?;
225 }
226
227 perpetual_db.set_highest_pruned_checkpoint(&mut wb, checkpoint_number)?;
228 metrics.last_pruned_checkpoint.set(checkpoint_number as i64);
229
230 if let Some(rpc_store) = rpc_store {
241 sui_rpc_store::prune_history_cohort(
242 rpc_store.db(),
243 rpc_store.schema(),
244 retraction_cursors,
245 checkpoint_number,
246 pruned_tx_seq_exclusive,
247 &transaction_effects,
248 )?;
249 }
250
251 wb.write()?;
252
253 Ok(())
254 }
255
256 #[cfg(tidehunter)]
257 async fn prune_objects_and_indexes(
258 transaction_effects: Vec<(CheckpointSequenceNumber, TransactionEffects)>,
259 perpetual_db: &Arc<AuthorityPerpetualTables>,
260 checkpoint_number: CheckpointSequenceNumber,
261 metrics: Arc<AuthorityStorePruningMetrics>,
262 pruned_tx_seq_exclusive: u64,
263 rpc_store: Option<&RpcStore>,
264 retraction_cursors: &mut RetractionCursors,
265 _: bool,
266 ) -> anyhow::Result<()> {
267 let _scope = monitored_scope("ObjectsLivePruner");
268 let mut wb = perpetual_db.objects.batch();
269 let mut objects_to_prune = vec![];
270
271 for (_checkpoint, effects) in &transaction_effects {
272 for (object_id, version) in effects
273 .modified_at_versions()
274 .into_iter()
275 .chain(effects.all_tombstones())
276 {
277 debug!("Pruning object {:?} version {:?}", object_id, version);
278 objects_to_prune.push(ObjectKey(object_id, version));
279 }
280 }
281 metrics
282 .num_pruned_objects
283 .inc_by(objects_to_prune.len() as u64);
284 wb.delete_batch(&perpetual_db.objects, &objects_to_prune)?;
285
286 perpetual_db.set_highest_pruned_checkpoint(&mut wb, checkpoint_number)?;
287 metrics.last_pruned_checkpoint.set(checkpoint_number as i64);
288
289 if let Some(rpc_store) = rpc_store {
293 sui_rpc_store::prune_history_cohort(
294 rpc_store.db(),
295 rpc_store.schema(),
296 retraction_cursors,
297 checkpoint_number,
298 pruned_tx_seq_exclusive,
299 &transaction_effects,
300 )?;
301 }
302
303 wb.write()?;
304 Ok(())
305 }
306
307 fn prune_checkpoints(
308 perpetual_db: &Arc<AuthorityPerpetualTables>,
309 checkpoint_db: &Arc<CheckpointStore>,
310 checkpoint_number: CheckpointSequenceNumber,
311 checkpoints_to_prune: Vec<CheckpointDigest>,
312 checkpoint_content_to_prune: Vec<CheckpointContents>,
313 effects_to_prune: &Vec<(CheckpointSequenceNumber, TransactionEffects)>,
314 metrics: Arc<AuthorityStorePruningMetrics>,
315 ) -> anyhow::Result<()> {
316 let _scope = monitored_scope("EffectsLivePruner");
317
318 let mut perpetual_batch = perpetual_db.objects.batch();
319 let transactions: Vec<_> = checkpoint_content_to_prune
320 .iter()
321 .flat_map(|content| content.iter().map(|tx| tx.transaction))
322 .collect();
323
324 perpetual_batch.delete_batch(&perpetual_db.transactions, transactions.iter())?;
325 perpetual_batch.delete_batch(&perpetual_db.executed_effects, transactions.iter())?;
326 perpetual_batch.delete_batch(
327 &perpetual_db.executed_transactions_to_checkpoint,
328 transactions.iter(),
329 )?;
330
331 let mut effect_digests = vec![];
332 for (_checkpoint, effects) in effects_to_prune {
333 let effects_digest = effects.digest();
334 debug!("Pruning effects {:?}", effects_digest);
335 effect_digests.push(effects_digest);
336
337 if effects.events_digest().is_some() {
338 perpetual_batch
339 .delete_batch(&perpetual_db.events_2, [effects.transaction_digest()])?;
340 }
341 }
342 perpetual_batch.delete_batch(
343 &perpetual_db.unchanged_loaded_runtime_objects,
344 transactions.iter(),
345 )?;
346 perpetual_batch.delete_batch(&perpetual_db.effects, effect_digests)?;
347
348 let mut checkpoints_batch = checkpoint_db.tables.certified_checkpoints.batch();
349
350 let checkpoint_content_digests =
351 checkpoint_content_to_prune.iter().map(|ckpt| ckpt.digest());
352 checkpoints_batch.delete_batch(
353 &checkpoint_db.tables.checkpoint_content,
354 checkpoint_content_digests.clone(),
355 )?;
356 checkpoints_batch.delete_batch(
357 &checkpoint_db.tables.checkpoint_sequence_by_contents_digest,
358 checkpoint_content_digests,
359 )?;
360
361 checkpoints_batch.delete_batch(
362 &checkpoint_db.tables.checkpoint_by_digest,
363 checkpoints_to_prune,
364 )?;
365
366 checkpoints_batch.insert_batch(
367 &checkpoint_db.tables.watermarks,
368 [(
369 &CheckpointWatermark::HighestPruned,
370 &(checkpoint_number, CheckpointDigest::random()),
371 )],
372 )?;
373
374 perpetual_batch.write()?;
375 checkpoints_batch.write()?;
376 metrics
377 .last_pruned_effects_checkpoint
378 .set(checkpoint_number as i64);
379
380 Ok(())
381 }
382
383 fn rpc_store_max_eligible_checkpoint(
396 rpc_store: Option<&RpcStore>,
397 ) -> anyhow::Result<CheckpointSequenceNumber> {
398 let Some(rpc_store) = rpc_store else {
399 return Ok(u64::MAX);
400 };
401 let indexed = sui_rpc_store::embedded_prunable_checkpoint(rpc_store.db())?;
402 Ok(indexed.map_or(0, |c| c.saturating_add(1)))
405 }
406
407 pub async fn prune_objects_for_eligible_epochs(
409 perpetual_db: &Arc<AuthorityPerpetualTables>,
410 checkpoint_store: &Arc<CheckpointStore>,
411 rpc_store: Option<&RpcStore>,
412 retraction_cursors: &mut RetractionCursors,
413 config: AuthorityStorePruningConfig,
414 metrics: Arc<AuthorityStorePruningMetrics>,
415 epoch_duration_ms: u64,
416 ) -> anyhow::Result<()> {
417 let _scope = monitored_scope("PruneObjectsForEligibleEpochs");
418 let (mut max_eligible_checkpoint_number, epoch_id) = checkpoint_store
419 .get_highest_executed_checkpoint()?
420 .map(|c| (*c.sequence_number(), c.epoch))
421 .unwrap_or_default();
422 let pruned_checkpoint_number = perpetual_db
423 .get_highest_pruned_checkpoint()?
424 .unwrap_or_default();
425 if config.smooth && config.num_epochs_to_retain > 0 {
426 max_eligible_checkpoint_number = Self::smoothed_max_eligible_checkpoint_number(
427 checkpoint_store,
428 max_eligible_checkpoint_number,
429 pruned_checkpoint_number,
430 epoch_id,
431 epoch_duration_ms,
432 config.num_epochs_to_retain,
433 )?;
434 }
435 let rpc_store_bound = Self::rpc_store_max_eligible_checkpoint(rpc_store)?;
436 if rpc_store_bound < max_eligible_checkpoint_number {
437 info!(
438 "objects pruning gated by the embedded rpc-store indexer: \
439 max eligible checkpoint {} -> {}",
440 max_eligible_checkpoint_number, rpc_store_bound,
441 );
442 max_eligible_checkpoint_number = rpc_store_bound;
443 }
444 Self::prune_for_eligible_epochs(
445 perpetual_db,
446 checkpoint_store,
447 rpc_store,
448 retraction_cursors,
449 PruningMode::Objects,
450 config.num_epochs_to_retain,
451 pruned_checkpoint_number,
452 max_eligible_checkpoint_number,
453 config,
454 metrics.clone(),
455 )
456 .await
457 }
458
459 pub async fn prune_checkpoints_for_eligible_epochs(
460 perpetual_db: &Arc<AuthorityPerpetualTables>,
461 checkpoint_store: &Arc<CheckpointStore>,
462 rpc_store: Option<&RpcStore>,
463 config: AuthorityStorePruningConfig,
464 metrics: Arc<AuthorityStorePruningMetrics>,
465 epoch_duration_ms: u64,
466 pruner_watermarks: &Arc<PrunerWatermarks>,
467 ) -> anyhow::Result<()> {
468 let _scope = monitored_scope("PruneCheckpointsForEligibleEpochs");
469 let pruned_checkpoint_number = checkpoint_store
470 .get_highest_pruned_checkpoint_seq_number()?
471 .unwrap_or(0);
472 let (mut max_eligible_checkpoint, epoch_id) = checkpoint_store
473 .get_highest_executed_checkpoint()?
474 .map(|c| (*c.sequence_number(), c.epoch))
475 .unwrap_or_default();
476 if config.num_epochs_to_retain != u64::MAX {
477 max_eligible_checkpoint = min(
478 max_eligible_checkpoint,
479 perpetual_db
480 .get_highest_pruned_checkpoint()?
481 .unwrap_or_default(),
482 );
483 }
484 if config.smooth
485 && let Some(num_epochs_to_retain) = config.num_epochs_to_retain_for_checkpoints
486 {
487 max_eligible_checkpoint = Self::smoothed_max_eligible_checkpoint_number(
488 checkpoint_store,
489 max_eligible_checkpoint,
490 pruned_checkpoint_number,
491 epoch_id,
492 epoch_duration_ms,
493 num_epochs_to_retain,
494 )?;
495 }
496 let rpc_store_bound = Self::rpc_store_max_eligible_checkpoint(rpc_store)?;
501 if rpc_store_bound < max_eligible_checkpoint {
502 info!(
503 "checkpoint pruning gated by the embedded rpc-store indexer: \
504 max eligible checkpoint {} -> {}",
505 max_eligible_checkpoint, rpc_store_bound,
506 );
507 max_eligible_checkpoint = rpc_store_bound;
508 }
509 debug!("Max eligible checkpoint {}", max_eligible_checkpoint);
510 Self::prune_for_eligible_epochs(
511 perpetual_db,
512 checkpoint_store,
513 rpc_store,
514 &mut RetractionCursors::default(),
517 PruningMode::Checkpoints,
518 config
519 .num_epochs_to_retain_for_checkpoints()
520 .ok_or_else(|| anyhow!("config value not set"))?,
521 pruned_checkpoint_number,
522 max_eligible_checkpoint,
523 config.clone(),
524 metrics.clone(),
525 )
526 .await?;
527
528 if let Some(num_epochs_to_retain) = config.num_epochs_to_retain_for_checkpoints() {
529 Self::update_pruning_watermarks(
530 perpetual_db,
531 checkpoint_store,
532 num_epochs_to_retain,
533 pruner_watermarks,
534 false,
535 )?;
536 }
537 Ok(())
538 }
539
540 pub async fn prune_for_eligible_epochs(
542 perpetual_db: &Arc<AuthorityPerpetualTables>,
543 checkpoint_store: &Arc<CheckpointStore>,
544 rpc_store: Option<&RpcStore>,
545 retraction_cursors: &mut RetractionCursors,
546 mode: PruningMode,
547 num_epochs_to_retain: u64,
548 starting_checkpoint_number: CheckpointSequenceNumber,
549 max_eligible_checkpoint: CheckpointSequenceNumber,
550 config: AuthorityStorePruningConfig,
551 metrics: Arc<AuthorityStorePruningMetrics>,
552 ) -> anyhow::Result<()> {
553 let _scope = monitored_scope("PruneForEligibleEpochs");
554
555 let mut checkpoint_number = starting_checkpoint_number;
556 let current_epoch = checkpoint_store
557 .get_highest_executed_checkpoint()?
558 .map(|c| c.epoch())
559 .unwrap_or_default();
560
561 let mut checkpoints_to_prune = vec![];
562 let mut checkpoint_content_to_prune = vec![];
563 let mut effects_to_prune: Vec<(CheckpointSequenceNumber, TransactionEffects)> = vec![];
567 let mut pruned_tx_seq_exclusive = 0u64;
572
573 while let Some(ckpt) = checkpoint_store
574 .tables
575 .certified_checkpoints
576 .get(&(checkpoint_number + 1))?
577 {
578 let checkpoint = ckpt.into_inner();
579 if (current_epoch < checkpoint.epoch() + num_epochs_to_retain)
584 || (*checkpoint.sequence_number() >= max_eligible_checkpoint)
585 {
586 break;
587 }
588 checkpoint_number = *checkpoint.sequence_number();
589 pruned_tx_seq_exclusive = checkpoint.network_total_transactions;
590
591 let content = checkpoint_store
592 .get_checkpoint_contents(&checkpoint.content_digest)?
593 .ok_or_else(|| {
594 anyhow::anyhow!(
595 "checkpoint content data is missing: {}",
596 checkpoint.sequence_number
597 )
598 })?;
599 let effects = perpetual_db
600 .effects
601 .multi_get(content.iter().map(|tx| tx.effects))?;
602
603 info!("scheduling pruning for checkpoint {:?}", checkpoint_number);
604 checkpoints_to_prune.push(*checkpoint.digest());
605 checkpoint_content_to_prune.push(content);
606 effects_to_prune.extend(
607 effects
608 .into_iter()
609 .flatten()
610 .map(|effects| (checkpoint_number, effects)),
611 );
612
613 if effects_to_prune.len() >= config.max_transactions_in_batch
614 || checkpoints_to_prune.len() >= config.max_checkpoints_in_batch
615 {
616 match mode {
617 PruningMode::Objects => {
618 Self::prune_objects_and_indexes(
619 effects_to_prune,
620 perpetual_db,
621 checkpoint_number,
622 metrics.clone(),
623 pruned_tx_seq_exclusive,
624 rpc_store,
625 retraction_cursors,
626 !config.killswitch_tombstone_pruning,
627 )
628 .await?
629 }
630 PruningMode::Checkpoints => Self::prune_checkpoints(
631 perpetual_db,
632 checkpoint_store,
633 checkpoint_number,
634 checkpoints_to_prune,
635 checkpoint_content_to_prune,
636 &effects_to_prune,
637 metrics.clone(),
638 )?,
639 };
640 checkpoints_to_prune = vec![];
641 checkpoint_content_to_prune = vec![];
642 effects_to_prune = vec![];
643 tokio::task::yield_now().await;
645 }
646 }
647
648 if !checkpoints_to_prune.is_empty() {
649 match mode {
650 PruningMode::Objects => {
651 Self::prune_objects_and_indexes(
652 effects_to_prune,
653 perpetual_db,
654 checkpoint_number,
655 metrics.clone(),
656 pruned_tx_seq_exclusive,
657 rpc_store,
658 retraction_cursors,
659 !config.killswitch_tombstone_pruning,
660 )
661 .await?
662 }
663 PruningMode::Checkpoints => Self::prune_checkpoints(
664 perpetual_db,
665 checkpoint_store,
666 checkpoint_number,
667 checkpoints_to_prune,
668 checkpoint_content_to_prune,
669 &effects_to_prune,
670 metrics.clone(),
671 )?,
672 };
673 }
674 Ok(())
675 }
676
677 fn prune_executed_tx_digests(
698 perpetual_db: &Arc<AuthorityPerpetualTables>,
699 checkpoint_store: &Arc<CheckpointStore>,
700 metrics: &AuthorityStorePruningMetrics,
701 last_pruned_epoch: &mut Option<EpochId>,
702 ) -> anyhow::Result<()> {
703 let current_epoch = checkpoint_store
704 .get_highest_executed_checkpoint()?
705 .map(|c| c.epoch)
706 .unwrap_or_default();
707
708 if current_epoch < 2 || last_pruned_epoch.is_some_and(|e| e >= current_epoch) {
709 return Ok(());
710 }
711
712 let retain_from_epoch = current_epoch - 1;
714 let from_epoch = last_pruned_epoch.map_or(0, |e| e - 1);
717 info!(
718 "Pruning executed_transaction_digests for epochs {}..{} (current epoch: {})",
719 from_epoch, retain_from_epoch, current_epoch
720 );
721
722 #[cfg(tidehunter)]
723 {
724 let from_key = (from_epoch, TransactionDigest::ZERO);
725 let to_key = (retain_from_epoch - 1, TransactionDigest::new([0xff; 32]));
726 perpetual_db
727 .executed_transaction_digests
728 .drop_cells_in_range(&from_key, &to_key)?;
729 }
730 #[cfg(not(tidehunter))]
731 {
732 let start_key = (from_epoch, TransactionDigest::ZERO);
733 let end_key = (retain_from_epoch, TransactionDigest::ZERO);
735 let mut batch = perpetual_db.executed_transaction_digests.batch();
736 batch.schedule_delete_range(
737 &perpetual_db.executed_transaction_digests,
738 &start_key,
739 &end_key,
740 )?;
741 batch.write()?;
742 }
743
744 metrics
745 .last_pruned_executed_tx_digests_epoch
746 .set((retain_from_epoch - 1) as i64);
747 *last_pruned_epoch = Some(current_epoch);
748 Ok(())
749 }
750
751 fn update_pruning_watermarks(
752 perpetual_db: &Arc<AuthorityPerpetualTables>,
753 checkpoint_store: &Arc<CheckpointStore>,
754 num_epochs_to_retain: u64,
755 pruning_watermark: &Arc<PrunerWatermarks>,
756 objects_compactor_active: bool,
757 ) -> anyhow::Result<bool> {
758 use std::sync::atomic::Ordering;
759 let objects_pruning_checkpoint_id = perpetual_db
760 .get_highest_pruned_checkpoint()?
761 .unwrap_or_default();
762 let objects_pruning_epoch_id = checkpoint_store
763 .get_checkpoint_by_sequence_number(objects_pruning_checkpoint_id)?
764 .map(|chk| chk.epoch)
765 .unwrap_or_default();
766
767 let current_watermark = pruning_watermark.epoch_id.load(Ordering::Relaxed);
768 let current_epoch_id = checkpoint_store
769 .get_highest_executed_checkpoint()?
770 .map(|c| c.epoch)
771 .unwrap_or_default();
772 if current_epoch_id < num_epochs_to_retain {
773 return Ok(false);
774 }
775 let target_epoch_id = current_epoch_id - num_epochs_to_retain;
776 let checkpoint_id =
777 checkpoint_store.get_epoch_last_checkpoint_seq_number(target_epoch_id)?;
778
779 let new_watermark = if objects_compactor_active {
782 target_epoch_id + 1
783 } else {
784 min(target_epoch_id + 1, objects_pruning_epoch_id)
785 };
786 if current_watermark == new_watermark {
787 return Ok(false);
788 }
789 info!("relocation: setting epoch watermark to {}", new_watermark);
790 pruning_watermark
791 .epoch_id
792 .store(new_watermark, Ordering::Relaxed);
793 if let Some(checkpoint_id) = checkpoint_id {
794 let watermark = if objects_compactor_active {
795 checkpoint_id
796 } else {
797 min(checkpoint_id, objects_pruning_checkpoint_id)
798 };
799 info!("relocation: setting checkpoint watermark to {}", watermark);
800 pruning_watermark
801 .checkpoint_id
802 .store(watermark, Ordering::Relaxed);
803 }
804 Ok(true)
805 }
806
807 #[cfg(tidehunter)]
808 fn prune_th(
809 perpetual_db: &Arc<AuthorityPerpetualTables>,
810 checkpoint_store: &Arc<CheckpointStore>,
811 num_epochs_to_retain: u64,
812 pruning_watermark: Arc<PrunerWatermarks>,
813 objects_compactor_active: bool,
814 ) -> anyhow::Result<()> {
815 let watermark_updated = Self::update_pruning_watermarks(
816 perpetual_db,
817 checkpoint_store,
818 num_epochs_to_retain,
819 &pruning_watermark,
820 objects_compactor_active,
821 )?;
822 if !watermark_updated {
823 info!("skip relocation. Watermark hasn't changed");
824 return Ok(());
825 }
826 perpetual_db.objects.db.start_relocation()?;
827 checkpoint_store.tables.watermarks.db.start_relocation()?;
828 Ok(())
829 }
830
831 #[cfg(not(tidehunter))]
832 fn compact_next_sst_file(
833 perpetual_db: Arc<AuthorityPerpetualTables>,
834 delay_days: usize,
835 last_processed: Arc<Mutex<HashMap<String, SystemTime>>>,
836 ) -> anyhow::Result<Option<LiveFile>> {
837 let db_path = perpetual_db.objects.db.path_for_pruning();
838 let mut state = last_processed
839 .lock()
840 .expect("failed to obtain a lock for last processed SST files");
841 let mut sst_file_for_compaction: Option<LiveFile> = None;
842 let time_threshold =
843 SystemTime::now() - Duration::from_secs(delay_days as u64 * 24 * 60 * 60);
844 for sst_file in perpetual_db.objects.db.live_files()? {
845 let file_path = db_path.join(sst_file.name.clone().trim_matches('/'));
846 let last_modified = std::fs::metadata(file_path)?.modified()?;
847 if !PERIODIC_PRUNING_TABLES.contains(&sst_file.column_family_name)
848 || sst_file.level < 1
849 || sst_file.start_key.is_none()
850 || sst_file.end_key.is_none()
851 || last_modified > time_threshold
852 || state.get(&sst_file.name).unwrap_or(&UNIX_EPOCH) > &time_threshold
853 {
854 continue;
855 }
856 if let Some(candidate) = &sst_file_for_compaction
857 && candidate.size > sst_file.size
858 {
859 continue;
860 }
861 sst_file_for_compaction = Some(sst_file);
862 }
863 let Some(sst_file) = sst_file_for_compaction else {
864 return Ok(None);
865 };
866 info!(
867 "Manual compaction of sst file {:?}. Size: {:?}, level: {:?}",
868 sst_file.name, sst_file.size, sst_file.level
869 );
870 perpetual_db.objects.compact_range_raw(
871 &sst_file.column_family_name,
872 sst_file.start_key.clone().unwrap(),
873 sst_file.end_key.clone().unwrap(),
874 )?;
875 state.insert(sst_file.name.clone(), SystemTime::now());
876 Ok(Some(sst_file))
877 }
878
879 fn pruning_tick_duration_ms(epoch_duration_ms: u64) -> u64 {
880 min(epoch_duration_ms / 2, MIN_PRUNING_TICK_DURATION_MS)
881 }
882
883 fn smoothed_max_eligible_checkpoint_number(
884 checkpoint_store: &Arc<CheckpointStore>,
885 mut max_eligible_checkpoint: CheckpointSequenceNumber,
886 pruned_checkpoint: CheckpointSequenceNumber,
887 epoch_id: EpochId,
888 epoch_duration_ms: u64,
889 num_epochs_to_retain: u64,
890 ) -> anyhow::Result<CheckpointSequenceNumber> {
891 if epoch_id < num_epochs_to_retain {
892 return Ok(0);
893 }
894 let last_checkpoint_in_epoch = checkpoint_store
895 .get_epoch_last_checkpoint(epoch_id - num_epochs_to_retain)?
896 .map(|checkpoint| checkpoint.sequence_number)
897 .unwrap_or_default();
898 max_eligible_checkpoint = max_eligible_checkpoint.min(last_checkpoint_in_epoch);
899 if max_eligible_checkpoint == 0 {
900 return Ok(max_eligible_checkpoint);
901 }
902 let num_intervals = epoch_duration_ms
903 .checked_div(Self::pruning_tick_duration_ms(epoch_duration_ms))
904 .unwrap_or(1);
905 let delta = max_eligible_checkpoint
906 .saturating_sub(pruned_checkpoint)
907 .checked_div(num_intervals)
908 .unwrap_or(1);
909 Ok(pruned_checkpoint + delta)
910 }
911
912 fn setup_pruning(
913 config: AuthorityStorePruningConfig,
914 epoch_duration_ms: u64,
915 perpetual_db: Arc<AuthorityPerpetualTables>,
916 checkpoint_store: Arc<CheckpointStore>,
917 rpc_store: Option<RpcStore>,
918 metrics: Arc<AuthorityStorePruningMetrics>,
919 pruner_watermarks: Arc<PrunerWatermarks>,
920 ) -> Sender<()> {
921 let (sender, mut recv) = tokio::sync::oneshot::channel();
922 debug!(
923 "Starting object pruning service with num_epochs_to_retain={}",
924 config.num_epochs_to_retain
925 );
926
927 let tick_duration =
928 Duration::from_millis(Self::pruning_tick_duration_ms(epoch_duration_ms));
929 let pruning_initial_delay = if cfg!(msim) {
930 Duration::from_millis(1)
931 } else {
932 Duration::from_secs(config.pruning_run_delay_seconds.unwrap_or(60 * 60))
933 };
934
935 metrics
936 .num_epochs_to_retain_for_objects
937 .set(config.num_epochs_to_retain as i64);
938 metrics.num_epochs_to_retain_for_checkpoints.set(
939 config
940 .num_epochs_to_retain_for_checkpoints
941 .unwrap_or_default() as i64,
942 );
943
944 #[cfg(tidehunter)]
945 {
946 let checkpoint_retention = config.num_epochs_to_retain_for_checkpoints();
950 let prune_objects =
951 checkpoint_retention.is_some() && config.num_epochs_to_retain != u64::MAX;
952 let prune_loop = async move {
953 let mut retraction_cursors = RetractionCursors::default();
954 let mut last_pruned_tx_digests_epoch = None;
955 let mut objects_prune_interval =
956 tokio::time::interval_at(Instant::now() + pruning_initial_delay, tick_duration);
957 let mut checkpoints_prune_interval =
958 tokio::time::interval_at(Instant::now() + pruning_initial_delay, tick_duration);
959 let mut tx_digests_prune_interval =
962 tokio::time::interval_at(Instant::now(), tick_duration);
963 loop {
964 tokio::select! {
965 _ = objects_prune_interval.tick(), if prune_objects => {
966 if let Err(err) = Self::prune_objects_for_eligible_epochs(&perpetual_db, &checkpoint_store, rpc_store.as_ref(), &mut retraction_cursors, config.clone(), metrics.clone(), epoch_duration_ms).await {
967 error!("Failed to prune objects: {:?}", err);
968 }
969 },
970 _ = checkpoints_prune_interval.tick(), if checkpoint_retention.is_some() => {
971 let Some(num_epochs_to_retain) = checkpoint_retention else { continue };
972 if let Err(err) = Self::prune_th(&perpetual_db, &checkpoint_store, num_epochs_to_retain, pruner_watermarks.clone(), !prune_objects) {
973 error!("Failed to prune checkpoints: {:?}", err);
974 }
975 },
976 _ = tx_digests_prune_interval.tick() => {
977 if let Err(err) = Self::prune_executed_tx_digests(&perpetual_db, &checkpoint_store, &metrics, &mut last_pruned_tx_digests_epoch) {
978 error!("Failed to prune executed_transaction_digests: {:?}", err);
979 }
980 },
981 _ = &mut recv => break,
982 }
983 }
984 };
985
986 #[cfg(not(msim))]
987 std::thread::Builder::new()
988 .name("authority-store-pruner".to_string())
989 .spawn(move || {
990 let runtime = tokio::runtime::Builder::new_current_thread()
991 .enable_all()
992 .build()
993 .expect("Failed to build pruner tokio runtime");
994 runtime.block_on(prune_loop);
995 })
996 .expect("Failed to spawn authority store pruner thread");
997
998 #[cfg(msim)]
999 tokio::task::spawn(prune_loop);
1000 }
1001 #[cfg(not(tidehunter))]
1002 {
1003 let perpetual_db_for_compaction = perpetual_db.clone();
1004 if let Some(delay_days) = config.periodic_compaction_threshold_days {
1005 spawn_monitored_task!(async move {
1006 let last_processed = Arc::new(Mutex::new(HashMap::new()));
1007 loop {
1008 let db = perpetual_db_for_compaction.clone();
1009 let state = Arc::clone(&last_processed);
1010 let result = tokio::task::spawn_blocking(move || {
1011 Self::compact_next_sst_file(db, delay_days, state)
1012 })
1013 .await;
1014 let mut sleep_interval_secs = 1;
1015 match result {
1016 Err(err) => error!("Failed to compact sst file: {:?}", err),
1017 Ok(Err(err)) => error!("Failed to compact sst file: {:?}", err),
1018 Ok(Ok(None)) => {
1019 sleep_interval_secs = 3600;
1020 }
1021 _ => {}
1022 }
1023 tokio::time::sleep(Duration::from_secs(sleep_interval_secs)).await;
1024 }
1025 });
1026 }
1027
1028 let prune_loop = async move {
1029 let mut retraction_cursors = RetractionCursors::default();
1030 let mut last_pruned_tx_digests_epoch = None;
1031 let mut objects_prune_interval =
1032 tokio::time::interval_at(Instant::now() + pruning_initial_delay, tick_duration);
1033 let mut checkpoints_prune_interval =
1034 tokio::time::interval_at(Instant::now() + pruning_initial_delay, tick_duration);
1035 let mut tx_digests_prune_interval =
1038 tokio::time::interval_at(Instant::now(), tick_duration);
1039 loop {
1040 tokio::select! {
1041 _ = objects_prune_interval.tick(), if config.num_epochs_to_retain != u64::MAX => {
1042 if let Err(err) = Self::prune_objects_for_eligible_epochs(&perpetual_db, &checkpoint_store, rpc_store.as_ref(), &mut retraction_cursors, config.clone(), metrics.clone(), epoch_duration_ms).await {
1043 error!("Failed to prune objects: {:?}", err);
1044 }
1045 },
1046 _ = tx_digests_prune_interval.tick() => {
1047 if let Err(err) = Self::prune_executed_tx_digests(&perpetual_db, &checkpoint_store, &metrics, &mut last_pruned_tx_digests_epoch) {
1048 error!("Failed to prune executed_transaction_digests: {:?}", err);
1049 }
1050 },
1051 _ = checkpoints_prune_interval.tick(), if !matches!(config.num_epochs_to_retain_for_checkpoints(), None | Some(u64::MAX) | Some(0)) => {
1052 if let Err(err) = Self::prune_checkpoints_for_eligible_epochs(&perpetual_db, &checkpoint_store, rpc_store.as_ref(), config.clone(), metrics.clone(), epoch_duration_ms, &pruner_watermarks).await {
1053 error!("Failed to prune checkpoints: {:?}", err);
1054 }
1055 },
1056 _ = &mut recv => break,
1057 }
1058 }
1059 };
1060
1061 #[cfg(not(msim))]
1062 std::thread::Builder::new()
1063 .name("authority-store-pruner".to_string())
1064 .spawn(move || {
1065 let runtime = tokio::runtime::Builder::new_current_thread()
1066 .enable_all()
1067 .build()
1068 .expect("Failed to build pruner tokio runtime");
1069 runtime.block_on(prune_loop);
1070 })
1071 .expect("Failed to spawn authority store pruner thread");
1072
1073 #[cfg(msim)]
1074 tokio::task::spawn(prune_loop);
1075 }
1076 sender
1077 }
1078
1079 pub fn new(
1080 perpetual_db: Arc<AuthorityPerpetualTables>,
1081 checkpoint_store: Arc<CheckpointStore>,
1082 rpc_store: Option<RpcStore>,
1083 mut pruning_config: AuthorityStorePruningConfig,
1084 is_validator: bool,
1085 epoch_duration_ms: u64,
1086 registry: &Registry,
1087 pruner_watermarks: Arc<PrunerWatermarks>, ) -> Self {
1089 #[cfg(tidehunter)]
1097 {
1098 let objects_compactor_enabled =
1099 is_validator || pruning_config.num_epochs_to_retain == 0;
1100 if objects_compactor_enabled && pruning_config.num_epochs_to_retain != u64::MAX {
1101 info!(
1102 "Tidehunter: disabling object pruner (was num_epochs_to_retain={}). The objects compactor performs equivalent compaction.",
1103 pruning_config.num_epochs_to_retain
1104 );
1105 pruning_config.num_epochs_to_retain = u64::MAX;
1106 }
1107 }
1108
1109 if pruning_config.num_epochs_to_retain > 0 && pruning_config.num_epochs_to_retain < u64::MAX
1110 {
1111 warn!(
1112 "Using objects pruner with num_epochs_to_retain = {} can lead to performance issues",
1113 pruning_config.num_epochs_to_retain
1114 );
1115 if is_validator {
1116 warn!("Resetting to aggressive pruner.");
1117 pruning_config.num_epochs_to_retain = 0;
1118 } else {
1119 warn!("Consider using an aggressive pruner (num_epochs_to_retain = 0)");
1120 }
1121 }
1122 AuthorityStorePruner {
1123 _objects_pruner_cancel_handle: Self::setup_pruning(
1124 pruning_config,
1125 epoch_duration_ms,
1126 perpetual_db,
1127 checkpoint_store,
1128 rpc_store,
1129 AuthorityStorePruningMetrics::new(registry),
1130 pruner_watermarks,
1131 ),
1132 }
1133 }
1134
1135 pub fn compact(perpetual_db: &Arc<AuthorityPerpetualTables>) -> Result<(), TypedStoreError> {
1136 perpetual_db.objects.compact_range(
1137 &ObjectKey(ObjectID::ZERO, SequenceNumber::MIN),
1138 &ObjectKey(ObjectID::MAX, SequenceNumber::MAX),
1139 )
1140 }
1141}
1142
1143#[cfg(tidehunter)]
1144pub(crate) fn apply_relocation_filter<T: DeserializeOwned>(
1145 config: typed_store::tidehunter_util::KeySpaceConfig,
1146 pruner_watermark: Arc<AtomicU64>,
1147 extractor: impl Fn(T) -> u64 + Send + Sync + 'static,
1148 by_key: bool,
1149) -> typed_store::tidehunter_util::KeySpaceConfig {
1150 use bincode::Options;
1151 use std::sync::atomic::Ordering;
1152 use typed_store::tidehunter_util::Decision;
1153 config.with_relocation_filter(move |key, value| {
1154 let data = if by_key {
1155 bincode::DefaultOptions::new()
1156 .with_big_endian()
1157 .with_fixint_encoding()
1158 .deserialize(key)
1159 .expect("relocation filter deserialization error")
1160 } else {
1161 bcs::from_bytes(value).expect("relocation filter deserialization error")
1162 };
1163 if extractor(data) < pruner_watermark.load(Ordering::Relaxed) {
1164 Decision::Remove
1165 } else {
1166 Decision::StopRelocation
1167 }
1168 })
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use more_asserts as ma;
1174 #[cfg(not(tidehunter))]
1175 use std::collections::HashSet;
1176 use std::path::Path;
1177 use std::sync::Arc;
1178 #[cfg(not(tidehunter))]
1179 use std::time::Duration;
1180 use tracing::log::info;
1181
1182 use crate::authority::authority_store_pruner::AuthorityStorePruningMetrics;
1183 use crate::authority::authority_store_tables::AuthorityPerpetualTables;
1184 use crate::authority::authority_store_types::get_store_object;
1185 #[cfg(not(tidehunter))]
1186 use crate::authority::authority_store_types::{StoreObject, StoreObjectWrapper};
1187 use prometheus::Registry;
1188 use sui_types::base_types::ObjectDigest;
1189 use sui_types::effects::TransactionEffects;
1190 use sui_types::effects::TransactionEffectsAPI;
1191 use sui_types::{
1192 base_types::{ObjectID, SequenceNumber},
1193 object::Object,
1194 storage::ObjectKey,
1195 };
1196 use typed_store::Map;
1197 #[cfg(not(tidehunter))]
1198 use typed_store::rocks::{DBMap, MetricConf, ReadWriteOptions, default_db_options};
1199
1200 use super::AuthorityStorePruner;
1201 use sui_rpc_store::RetractionCursors;
1202 #[test]
1206 fn rpc_store_gate_bounds_eligible_checkpoints() {
1207 use sui_consistent_store::Db;
1208 use sui_consistent_store::DbOptions;
1209 use sui_consistent_store::FrameworkSchema;
1210 use sui_consistent_store::PipelineTaskKey;
1211 use sui_consistent_store::Watermark;
1212 use sui_rpc_store::HISTORY_COHORT;
1213 use sui_rpc_store::LIVE_COHORT;
1214 use sui_rpc_store::RpcStoreSchema;
1215
1216 assert_eq!(
1218 AuthorityStorePruner::rpc_store_max_eligible_checkpoint(None).unwrap(),
1219 u64::MAX,
1220 );
1221
1222 let dir = tempfile::tempdir().unwrap();
1223 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
1224 let store = sui_rpc_store::Store::new(db.clone(), Arc::new(schema));
1225
1226 assert_eq!(
1229 AuthorityStorePruner::rpc_store_max_eligible_checkpoint(Some(&store)).unwrap(),
1230 0,
1231 );
1232
1233 let framework = FrameworkSchema::new(db.clone());
1237 let mut batch = db.batch();
1238 for name in LIVE_COHORT.iter().chain(HISTORY_COHORT) {
1239 batch
1240 .put(
1241 &framework.watermarks,
1242 &PipelineTaskKey::new(*name),
1243 &Watermark::for_checkpoint(41),
1244 )
1245 .unwrap();
1246 }
1247 batch
1248 .put(
1249 &framework.watermarks,
1250 &PipelineTaskKey::new(HISTORY_COHORT[0]),
1251 &Watermark::for_checkpoint(7),
1252 )
1253 .unwrap();
1254 batch.commit().unwrap();
1255 assert_eq!(
1256 AuthorityStorePruner::rpc_store_max_eligible_checkpoint(Some(&store)).unwrap(),
1257 8,
1258 );
1259 }
1260
1261 #[cfg(not(tidehunter))]
1262 fn get_keys_after_pruning(path: &Path) -> anyhow::Result<HashSet<ObjectKey>> {
1263 let perpetual_db_path = path.join(Path::new("perpetual"));
1264 let cf_names = AuthorityPerpetualTables::describe_tables();
1265 let cfs: Vec<_> = cf_names
1266 .keys()
1267 .map(|x| (x.as_str(), default_db_options().options))
1268 .collect();
1269 let perpetual_db = typed_store::rocks::open_cf_opts(
1270 perpetual_db_path,
1271 None,
1272 MetricConf::new("perpetual_pruning"),
1273 &cfs,
1274 );
1275
1276 let mut after_pruning = HashSet::new();
1277 let objects = DBMap::<ObjectKey, StoreObjectWrapper>::reopen(
1278 &perpetual_db?,
1279 Some("objects"),
1280 &ReadWriteOptions::default(),
1283 false,
1284 )?;
1285 let iter = objects.safe_iter();
1286 for item in iter {
1287 after_pruning.insert(item?.0);
1288 }
1289 Ok(after_pruning)
1290 }
1291
1292 #[cfg(not(tidehunter))]
1293 type GenerateTestDataResult = (Vec<ObjectKey>, Vec<ObjectKey>, Vec<ObjectKey>);
1294
1295 #[cfg(not(tidehunter))]
1296 fn generate_test_data(
1297 db: Arc<AuthorityPerpetualTables>,
1298 num_versions_per_object: u64,
1299 num_object_versions_to_retain: u64,
1300 total_unique_object_ids: u32,
1301 ) -> Result<GenerateTestDataResult, anyhow::Error> {
1302 assert!(num_versions_per_object >= num_object_versions_to_retain);
1303
1304 let (mut to_keep, mut to_delete, mut tombstones) = (vec![], vec![], vec![]);
1305 let mut batch = db.objects.batch();
1306
1307 let ids = ObjectID::in_range(ObjectID::ZERO, total_unique_object_ids.into())?;
1308 for id in ids {
1309 for (counter, seq) in (0..num_versions_per_object).rev().enumerate() {
1310 let object_key = ObjectKey(id, SequenceNumber::from_u64(seq));
1311 if counter < num_object_versions_to_retain.try_into().unwrap() {
1312 to_keep.push(object_key);
1314 } else {
1315 to_delete.push(object_key);
1316 }
1317 let obj = get_store_object(Object::immutable_with_id_for_testing(id));
1318 batch.insert_batch(
1319 &db.objects,
1320 [(ObjectKey(id, SequenceNumber::from(seq)), obj.clone())],
1321 )?;
1322 }
1323
1324 if num_object_versions_to_retain == 0 {
1326 let tombstone_key = ObjectKey(id, SequenceNumber::from(num_versions_per_object));
1327 println!("Adding tombstone object {:?}", tombstone_key);
1328 batch.insert_batch(
1329 &db.objects,
1330 [(tombstone_key, StoreObjectWrapper::V1(StoreObject::Deleted))],
1331 )?;
1332 tombstones.push(tombstone_key);
1333 }
1334 }
1335 batch.write().unwrap();
1336 assert_eq!(
1337 to_keep.len() as u64,
1338 std::cmp::min(num_object_versions_to_retain, num_versions_per_object)
1339 * total_unique_object_ids as u64
1340 );
1341 assert_eq!(
1342 tombstones.len() as u64,
1343 if num_object_versions_to_retain == 0 {
1344 total_unique_object_ids as u64
1345 } else {
1346 0
1347 }
1348 );
1349 Ok((to_keep, to_delete, tombstones))
1350 }
1351
1352 #[cfg(not(tidehunter))]
1353 async fn run_pruner(
1354 path: &Path,
1355 num_versions_per_object: u64,
1356 num_object_versions_to_retain: u64,
1357 total_unique_object_ids: u32,
1358 ) -> Vec<ObjectKey> {
1359 let registry = Registry::default();
1360 let metrics = AuthorityStorePruningMetrics::new(®istry);
1361 let to_keep = {
1362 let db = Arc::new(AuthorityPerpetualTables::open(path, None, None));
1363 let (to_keep, to_delete, tombstones) = generate_test_data(
1364 db.clone(),
1365 num_versions_per_object,
1366 num_object_versions_to_retain,
1367 total_unique_object_ids,
1368 )
1369 .unwrap();
1370 let mut effects = TransactionEffects::default();
1371 for object in to_delete {
1372 effects.unsafe_add_deleted_live_object_for_testing((
1373 object.0,
1374 object.1,
1375 ObjectDigest::MIN,
1376 ));
1377 }
1378 for object in tombstones {
1379 effects.unsafe_add_object_tombstone_for_testing((
1380 object.0,
1381 object.1,
1382 ObjectDigest::MIN,
1383 ));
1384 }
1385 AuthorityStorePruner::prune_objects_and_indexes(
1386 vec![(0, effects)],
1387 &db,
1388 0,
1389 metrics,
1390 0,
1391 None,
1392 &mut RetractionCursors::default(),
1393 true,
1394 )
1395 .await
1396 .unwrap();
1397 to_keep
1398 };
1399 tokio::time::sleep(Duration::from_secs(3)).await;
1400 to_keep
1401 }
1402
1403 #[cfg(not(tidehunter))]
1405 #[tokio::test]
1406 async fn test_pruning_objects() {
1407 let path = tempfile::tempdir().unwrap().keep();
1408 let to_keep = run_pruner(&path, 3, 2, 1000).await;
1409 assert_eq!(
1410 HashSet::from_iter(to_keep),
1411 get_keys_after_pruning(&path).unwrap()
1412 );
1413 run_pruner(&tempfile::tempdir().unwrap().keep(), 3, 2, 1000).await;
1414 }
1415
1416 #[cfg(not(tidehunter))]
1418 #[tokio::test]
1419 async fn test_pruning_tombstones() {
1420 let path = tempfile::tempdir().unwrap().keep();
1421 let to_keep = run_pruner(&path, 0, 0, 1000).await;
1422 assert_eq!(to_keep.len(), 0);
1423 assert_eq!(get_keys_after_pruning(&path).unwrap().len(), 0);
1424
1425 let path = tempfile::tempdir().unwrap().keep();
1426 let to_keep = run_pruner(&path, 3, 0, 1000).await;
1427 assert_eq!(to_keep.len(), 0);
1428 assert_eq!(get_keys_after_pruning(&path).unwrap().len(), 0);
1429 }
1430
1431 #[cfg(not(target_env = "msvc"))]
1432 #[tokio::test]
1433 async fn test_db_size_after_compaction() -> Result<(), anyhow::Error> {
1434 let primary_path = tempfile::tempdir()?.keep();
1435 let perpetual_db = Arc::new(AuthorityPerpetualTables::open(&primary_path, None, None));
1436 let total_unique_object_ids = 10_000;
1437 let num_versions_per_object = 10;
1438 let ids = ObjectID::in_range(ObjectID::ZERO, total_unique_object_ids)?;
1439 let mut to_delete = vec![];
1440 for id in ids {
1441 for i in (0..num_versions_per_object).rev() {
1442 if i < num_versions_per_object - 2 {
1443 to_delete.push((id, SequenceNumber::from(i)));
1444 }
1445 let obj = get_store_object(Object::immutable_with_id_for_testing(id));
1446 perpetual_db
1447 .objects
1448 .insert(&ObjectKey(id, SequenceNumber::from(i)), &obj)?;
1449 }
1450 }
1451
1452 fn get_sst_size(path: &Path) -> u64 {
1453 let mut size = 0;
1454 for entry in std::fs::read_dir(path).unwrap() {
1455 let entry = entry.unwrap();
1456 let path = entry.path();
1457 if let Some(ext) = path.extension() {
1458 if ext != "sst" {
1459 continue;
1460 }
1461 size += std::fs::metadata(path).unwrap().len();
1462 }
1463 }
1464 size
1465 }
1466
1467 let db_path = primary_path.clone().join("perpetual");
1468 let start = ObjectKey(ObjectID::ZERO, SequenceNumber::MIN);
1469 let end = ObjectKey(ObjectID::MAX, SequenceNumber::MAX);
1470
1471 perpetual_db.objects.compact_range(&start, &end)?;
1472 let before_compaction_size = get_sst_size(&db_path);
1473
1474 let mut effects = TransactionEffects::default();
1475 for object in to_delete {
1476 effects.unsafe_add_deleted_live_object_for_testing((
1477 object.0,
1478 object.1,
1479 ObjectDigest::MIN,
1480 ));
1481 }
1482 let registry = Registry::default();
1483 let metrics = AuthorityStorePruningMetrics::new(®istry);
1484 AuthorityStorePruner::prune_objects_and_indexes(
1485 vec![(0, effects)],
1486 &perpetual_db,
1487 0,
1488 metrics,
1489 0,
1490 None,
1491 &mut RetractionCursors::default(),
1492 true,
1493 )
1494 .await
1495 .unwrap();
1496 perpetual_db.objects.compact_range(&start, &end)?;
1497 let after_compaction_size = get_sst_size(&db_path);
1498
1499 info!(
1500 "Before compaction disk size = {:?}, after compaction disk size = {:?}",
1501 before_compaction_size, after_compaction_size
1502 );
1503 ma::assert_le!(after_compaction_size, before_compaction_size);
1504 Ok(())
1505 }
1506}