1use consensus_config::Epoch;
5use mysten_metrics::spawn_logged_monitored_task;
6use prometheus::{
7 IntCounter, IntCounterVec, IntGauge, Registry, register_int_counter_vec_with_registry,
8 register_int_counter_with_registry, register_int_gauge_with_registry,
9};
10use std::fs;
11use std::path::PathBuf;
12use std::time::Duration;
13use tokio::{sync::mpsc, time::Instant};
14#[cfg(not(tidehunter))]
15use tracing::warn;
16use tracing::{error, info};
17use typed_store::rocks::safe_drop_db;
18
19struct Metrics {
20 last_pruned_consensus_db_epoch: IntGauge,
21 successfully_pruned_consensus_dbs: IntCounter,
22 error_pruning_consensus_dbs: IntCounterVec,
23}
24
25impl Metrics {
26 fn new(registry: &Registry) -> Self {
27 Self {
28 last_pruned_consensus_db_epoch: register_int_gauge_with_registry!(
29 "last_pruned_consensus_db_epoch",
30 "The last epoch for which the consensus store was pruned",
31 registry
32 )
33 .unwrap(),
34 successfully_pruned_consensus_dbs: register_int_counter_with_registry!(
35 "successfully_pruned_consensus_dbs",
36 "The number of consensus dbs successfully pruned",
37 registry
38 )
39 .unwrap(),
40 error_pruning_consensus_dbs: register_int_counter_vec_with_registry!(
41 "error_pruning_consensus_dbs",
42 "The number of errors encountered while pruning consensus dbs",
43 &["mode"],
44 registry
45 )
46 .unwrap(),
47 }
48 }
49}
50
51pub struct ConsensusStorePruner {
52 tx_remove: mpsc::Sender<Epoch>,
53 _handle: tokio::task::JoinHandle<()>,
54}
55
56impl ConsensusStorePruner {
57 pub fn new(
58 base_path: PathBuf,
59 epoch_retention: u64,
60 epoch_prune_period: Duration,
61 registry: &Registry,
62 ) -> Self {
63 let (tx_remove, mut rx_remove) = mpsc::channel(1);
64 let metrics = Metrics::new(registry);
65
66 let _handle = spawn_logged_monitored_task!(async {
67 info!(
68 "Starting consensus store pruner with epoch retention {epoch_retention} and prune period {epoch_prune_period:?}"
69 );
70
71 let mut timeout = tokio::time::interval_at(
72 Instant::now() + Duration::from_secs(60), epoch_prune_period,
74 );
75
76 let mut latest_epoch = 0;
77 loop {
78 tokio::select! {
79 _ = timeout.tick() => {
80 Self::prune_old_epoch_data(&base_path, latest_epoch, epoch_retention, &metrics).await;
81 }
82 result = rx_remove.recv() => {
83 if result.is_none() {
84 info!("Closing consensus store pruner");
85 break;
86 }
87 latest_epoch = result.unwrap();
88 Self::prune_old_epoch_data(&base_path, latest_epoch, epoch_retention, &metrics).await;
89 }
90 }
91 }
92 });
93
94 Self { tx_remove, _handle }
95 }
96
97 pub async fn prune(&self, current_epoch: Epoch) {
100 let result = self.tx_remove.send(current_epoch).await;
101 if result.is_err() {
102 error!(
103 "Error sending message to data removal task for epoch {:?}",
104 current_epoch,
105 );
106 }
107 }
108
109 async fn prune_old_epoch_data(
110 storage_base_path: &PathBuf,
111 current_epoch: Epoch,
112 epoch_retention: u64,
113 metrics: &Metrics,
114 ) {
115 let drop_boundary = current_epoch.saturating_sub(epoch_retention);
116
117 info!(
118 "Consensus store prunning for current epoch {}. Will remove epochs < {:?}",
119 current_epoch, drop_boundary
120 );
121
122 let files = match fs::read_dir(storage_base_path) {
124 Ok(f) => f,
125 Err(e) => {
126 error!(
127 "Can not read the files in the storage path directory for epoch cleanup: {:?}",
128 e
129 );
130 return;
131 }
132 };
133
134 for file_res in files {
136 let f = match file_res {
137 Ok(f) => f,
138 Err(e) => {
139 error!(
140 "Error while cleaning up storage of previous epochs: {:?}",
141 e
142 );
143 continue;
144 }
145 };
146
147 let name = f.file_name();
148 let file_epoch_string = match name.to_str() {
149 Some(f) => f,
150 None => continue,
151 };
152
153 let file_epoch = match file_epoch_string.to_owned().parse::<u64>() {
154 Ok(f) => f,
155 Err(e) => {
156 error!(
157 "Could not parse file \"{file_epoch_string}\" in storage path into epoch for cleanup: {:?}",
158 e
159 );
160 continue;
161 }
162 };
163
164 if file_epoch < drop_boundary {
165 const WAIT_TIMEOUT: Duration = Duration::from_secs(5);
166 match safe_drop_db(f.path(), WAIT_TIMEOUT).await {
167 Ok(()) => {
168 info!(
169 "Successfully pruned consensus epoch storage directory: {:?}",
170 f.path()
171 );
172 let last_epoch = metrics.last_pruned_consensus_db_epoch.get();
173 metrics
174 .last_pruned_consensus_db_epoch
175 .set(last_epoch.max(file_epoch as i64));
176 metrics.successfully_pruned_consensus_dbs.inc();
177 }
178 Err(e) => {
179 #[cfg(not(tidehunter))]
180 {
181 warn!(
182 "Could not prune old consensus storage \"{:?}\" directory with safe approach. Will fallback to force delete: {:?}",
183 f.path(),
184 e
185 );
186 metrics
187 .error_pruning_consensus_dbs
188 .with_label_values(&["safe"])
189 .inc();
190
191 if let Err(err) = fs::remove_dir_all(f.path()) {
192 error!(
193 "Could not prune old consensus storage \"{:?}\" directory with force delete: {:?}",
194 f.path(),
195 err
196 );
197 metrics
198 .error_pruning_consensus_dbs
199 .with_label_values(&["force"])
200 .inc();
201 } else {
202 info!(
203 "Successfully pruned consensus epoch storage directory with force delete: {:?}",
204 f.path()
205 );
206 let last_epoch = metrics.last_pruned_consensus_db_epoch.get();
207 metrics
208 .last_pruned_consensus_db_epoch
209 .set(last_epoch.max(file_epoch as i64));
210 metrics.successfully_pruned_consensus_dbs.inc();
211 }
212 }
213 #[cfg(tidehunter)]
214 {
215 error!(
216 "Could not prune old consensus storage \"{:?}\" directory: {:?}",
217 f.path(),
218 e
219 );
220 metrics
221 .error_pruning_consensus_dbs
222 .with_label_values(&["safe"])
223 .inc();
224 }
225 }
226 }
227 }
228 }
229
230 info!(
231 "Completed old epoch data removal process for epoch {:?}",
232 current_epoch
233 );
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use crate::epoch::consensus_store_pruner::{ConsensusStorePruner, Metrics};
240 use prometheus::Registry;
241 use std::fs;
242 use tokio::time::sleep;
243
244 #[tokio::test]
245 async fn test_remove_old_epoch_data() {
246 telemetry_subscribers::init_for_testing();
247 let metrics = Metrics::new(&Registry::new());
248
249 {
250 let epoch_retention = 0;
252 let current_epoch = 0;
253
254 let base_directory = tempfile::tempdir().unwrap().keep();
255
256 create_epoch_directories(&base_directory, vec!["0", "other"]);
257
258 ConsensusStorePruner::prune_old_epoch_data(
259 &base_directory,
260 current_epoch,
261 epoch_retention,
262 &metrics,
263 )
264 .await;
265
266 let epochs_left = read_epoch_directories(&base_directory);
267
268 assert_eq!(epochs_left.len(), 1);
269 assert_eq!(epochs_left[0], 0);
270 }
271
272 {
273 let epoch_retention = 1;
275 let current_epoch = 100;
276
277 let base_directory = tempfile::tempdir().unwrap().keep();
278
279 create_epoch_directories(&base_directory, vec!["97", "98", "99", "100", "other"]);
280
281 ConsensusStorePruner::prune_old_epoch_data(
282 &base_directory,
283 current_epoch,
284 epoch_retention,
285 &metrics,
286 )
287 .await;
288
289 let epochs_left = read_epoch_directories(&base_directory);
290
291 assert_eq!(epochs_left.len(), 2);
292 assert_eq!(epochs_left[0], 99);
293 assert_eq!(epochs_left[1], 100);
294 }
295
296 {
297 let epoch_retention = 0;
300 let current_epoch = 100;
301
302 let base_directory = tempfile::tempdir().unwrap().keep();
303
304 create_epoch_directories(&base_directory, vec!["97", "98", "99", "100", "other"]);
305
306 ConsensusStorePruner::prune_old_epoch_data(
307 &base_directory,
308 current_epoch,
309 epoch_retention,
310 &metrics,
311 )
312 .await;
313
314 let epochs_left = read_epoch_directories(&base_directory);
315
316 assert_eq!(epochs_left.len(), 1);
317 assert_eq!(epochs_left[0], 100);
318 }
319 }
320
321 #[tokio::test(flavor = "current_thread")]
322 async fn test_consensus_store_pruner() {
323 let epoch_retention = 1;
324 let epoch_prune_period = std::time::Duration::from_millis(500);
325
326 let base_directory = tempfile::tempdir().unwrap().keep();
327
328 create_epoch_directories(&base_directory, vec!["97", "98", "99", "100", "other"]);
330
331 let pruner = ConsensusStorePruner::new(
332 base_directory.clone(),
333 epoch_retention,
334 epoch_prune_period,
335 &Registry::new(),
336 );
337
338 sleep(3 * epoch_prune_period).await;
340
341 let epoch_dirs = read_epoch_directories(&base_directory);
343 assert_eq!(epoch_dirs.len(), 4);
344
345 pruner.prune(100).await;
347
348 sleep(2 * epoch_prune_period).await;
350
351 let epoch_dirs = read_epoch_directories(&base_directory);
352 assert_eq!(epoch_dirs.len(), 2);
353 assert_eq!(epoch_dirs[0], 99);
354 assert_eq!(epoch_dirs[1], 100);
355 }
356
357 fn create_epoch_directories(base_directory: &std::path::Path, epochs: Vec<&str>) {
358 for epoch in epochs {
359 let mut path = base_directory.to_path_buf();
360 path.push(epoch);
361 fs::create_dir(path).unwrap();
362 }
363 }
364
365 fn read_epoch_directories(base_directory: &std::path::Path) -> Vec<u64> {
366 let files = fs::read_dir(base_directory).unwrap();
367
368 let mut epochs = Vec::new();
369 for file_res in files {
370 let file_epoch_string = file_res.unwrap().file_name().to_str().unwrap().to_owned();
371 if let Ok(file_epoch) = file_epoch_string.parse::<u64>() {
372 epochs.push(file_epoch);
373 }
374 }
375
376 epochs.sort();
377 epochs
378 }
379}