1use crate::blocks::Tipset;
5use crate::db::SettingsStoreExt;
6use crate::db::car::forest::{
7 FOREST_CAR_FILE_EXTENSION, TEMP_FOREST_CAR_FILE_EXTENSION, new_forest_car_temp_path_in,
8};
9use crate::db::car::{ForestCar, ManyCar};
10use crate::ipld::ChainExportState;
11use crate::message::SignedMessage;
12use crate::networks::ChainConfig;
13use crate::prelude::*;
14use crate::rpc::sync::SnapshotProgressTracker;
15use crate::shim::clock::ChainEpoch;
16use crate::state_manager::StateManager;
17use crate::utils::db::car_stream::CarStream;
18use crate::utils::io::EitherMmapOrRandomAccessFile;
19use crate::utils::net::{DownloadFileOption, download_to};
20use anyhow::{Context, bail};
21use futures::TryStreamExt;
22use serde::{Deserialize, Serialize};
23use std::sync::LazyLock;
24use std::sync::atomic::{AtomicI64, Ordering};
25use std::{
26 ffi::OsStr,
27 fs,
28 path::{Path, PathBuf},
29 time,
30};
31use tokio::io::AsyncWriteExt;
32use tokio_util::sync::CancellationToken;
33use tracing::{debug, info, warn};
34use url::Url;
35use walkdir::WalkDir;
36
37#[cfg(doc)]
38use crate::rpc::eth::types::EthHash;
39
40#[cfg(doc)]
41use crate::blocks::TipsetKey;
42
43#[cfg(doc)]
44use cid::Cid;
45
46pub fn load_all_forest_cars_with_cleanup<T>(
48 store: &ManyCar<T>,
49 forest_car_db_dir: &Path,
50) -> anyhow::Result<()> {
51 load_all_forest_cars_internal(store, forest_car_db_dir, true)
52}
53
54pub fn load_all_forest_cars<T>(store: &ManyCar<T>, forest_car_db_dir: &Path) -> anyhow::Result<()> {
56 load_all_forest_cars_internal(store, forest_car_db_dir, false)
57}
58
59fn load_all_forest_cars_internal<T>(
60 store: &ManyCar<T>,
61 forest_car_db_dir: &Path,
62 cleanup: bool,
63) -> anyhow::Result<()> {
64 if !forest_car_db_dir.is_dir() {
65 fs::create_dir_all(forest_car_db_dir)?;
66 }
67 for file in WalkDir::new(forest_car_db_dir)
68 .max_depth(1)
69 .into_iter()
70 .filter_map(|e| {
71 e.ok().and_then(|e| {
72 if !e.file_type().is_dir() {
73 Some(e.into_path())
74 } else {
75 None
76 }
77 })
78 })
79 {
80 if let Some(filename) = file.file_name().and_then(OsStr::to_str) {
81 if filename.ends_with(FOREST_CAR_FILE_EXTENSION) {
82 let car = ForestCar::try_from(file.as_path())
83 .with_context(|| format!("Error loading car DB at {}", file.display()))?;
84 store.read_only(car.into())?;
85 debug!("Loaded car DB at {}", file.display());
86 } else if cleanup && filename.ends_with(TEMP_FOREST_CAR_FILE_EXTENSION) {
87 match std::fs::remove_file(&file) {
89 Ok(_) => {
90 info!("Deleted temp car DB at {}", file.display());
91 }
92 Err(e) => {
93 warn!("Failed to delete temp car DB at {}: {e}", file.display());
94 }
95 }
96 }
97 }
98 }
99
100 tracing::info!("Loaded {} CARs", store.len());
101
102 Ok(())
103}
104
105#[derive(
106 Default,
107 PartialEq,
108 Eq,
109 Debug,
110 Clone,
111 Copy,
112 strum::Display,
113 strum::EnumString,
114 Serialize,
115 Deserialize,
116)]
117#[strum(serialize_all = "lowercase")]
118#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
119pub enum ImportMode {
120 #[default]
121 Auto,
123 Copy,
125 Move,
127 Symlink,
129 Hardlink,
131}
132
133pub async fn import_chain_as_forest_car(
136 from_path: &Path,
137 forest_car_db_dir: &Path,
138 import_mode: ImportMode,
139 rpc_endpoint: Url,
140 f3_root: &Path,
141 chain_config: &ChainConfig,
142 snapshot_progress_tracker: &SnapshotProgressTracker,
143) -> anyhow::Result<(PathBuf, Tipset)> {
144 info!("Importing chain from snapshot at: {}", from_path.display());
145
146 let stopwatch = time::Instant::now();
147
148 let forest_car_db_path = forest_car_db_dir.join(format!(
149 "{}{FOREST_CAR_FILE_EXTENSION}",
150 chrono::Utc::now().timestamp_millis()
151 ));
152
153 let move_or_copy = |mode: ImportMode| {
154 let forest_car_db_path = forest_car_db_path.clone();
155 async move {
156 let downloaded_car_temp_path = new_forest_car_temp_path_in(forest_car_db_dir)?;
157 if let Ok(url) = Url::parse(&from_path.display().to_string()) {
158 download_to(
159 &url,
160 &downloaded_car_temp_path,
161 DownloadFileOption::Resumable,
162 snapshot_progress_tracker.create_callback(),
163 )
164 .await?;
165
166 snapshot_progress_tracker.completed();
167 } else {
168 snapshot_progress_tracker.not_required();
169 if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
170 move_or_copy_file(from_path, &downloaded_car_temp_path, mode)?;
171 } else {
172 transcode_into_forest_car(from_path, &downloaded_car_temp_path).await?;
174 if mode == ImportMode::Move {
175 std::fs::remove_file(from_path).context("Error removing original file")?;
176 }
177 }
178 }
179
180 if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(
181 &downloaded_car_temp_path,
182 )?) {
183 downloaded_car_temp_path.persist(&forest_car_db_path)?;
184 } else {
185 let forest_car_db_temp_path = new_forest_car_temp_path_in(forest_car_db_dir)?;
187 transcode_into_forest_car(&downloaded_car_temp_path, &forest_car_db_temp_path)
188 .await?;
189 forest_car_db_temp_path.persist(&forest_car_db_path)?;
190 }
191 anyhow::Ok(())
192 }
193 };
194
195 match import_mode {
196 ImportMode::Auto => {
197 if Url::parse(&from_path.display().to_string()).is_ok() {
198 move_or_copy(ImportMode::Move).await?;
200 } else if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
201 tracing::info!(
202 "Hardlinking {} to {}",
203 from_path.display(),
204 forest_car_db_path.display()
205 );
206 if std::fs::hard_link(from_path, &forest_car_db_path).is_err() {
207 tracing::warn!("Error creating hardlink, fallback to copy");
208 move_or_copy(ImportMode::Copy).await?;
209 }
210 } else {
211 tracing::warn!(
212 "Snapshot file is not a valid forest.car.zst file, fallback to copy"
213 );
214 move_or_copy(ImportMode::Copy).await?;
215 }
216 }
217 ImportMode::Copy | ImportMode::Move => {
218 move_or_copy(import_mode).await?;
219 }
220 ImportMode::Symlink => {
221 let from_path = std::path::absolute(from_path)?;
222 if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(&from_path)?) {
223 tracing::info!(
224 "Symlinking {} to {}",
225 from_path.display(),
226 forest_car_db_path.display()
227 );
228 std::os::unix::fs::symlink(from_path, &forest_car_db_path)
229 .context("Error creating symlink")?;
230 } else {
231 bail!("Snapshot file must be a valid forest.car.zst file");
232 }
233 }
234 ImportMode::Hardlink => {
235 if ForestCar::is_valid(&EitherMmapOrRandomAccessFile::open(from_path)?) {
236 tracing::info!(
237 "Hardlinking {} to {}",
238 from_path.display(),
239 forest_car_db_path.display()
240 );
241 std::fs::hard_link(from_path, &forest_car_db_path)
242 .context("Error creating hardlink")?;
243 } else {
244 bail!("Snapshot file must be a valid forest.car.zst file");
245 }
246 }
247 };
248
249 let forest_car = ForestCar::try_from(forest_car_db_path.as_path())?;
250
251 if let Some(f3_cid) = forest_car.metadata().and_then(|m| m.f3_data) {
252 if crate::f3::get_f3_sidecar_params(chain_config)
253 .initial_power_table
254 .is_none()
255 {
256 tracing::warn!(
258 "skipped importing F3 data as the initial power table CID is not set in the current manifest"
259 );
260 } else {
261 let mut f3_data = forest_car
262 .get_reader(f3_cid)?
263 .with_context(|| format!("f3 data not found, cid: {f3_cid}"))?;
264 let mut temp_f3_snap = tempfile::Builder::new()
265 .suffix(".f3snap.bin")
266 .tempfile_in(forest_car_db_dir)?;
267 {
268 let f = temp_f3_snap.as_file_mut();
269 std::io::copy(&mut f3_data, f)?;
270 f.sync_all()?;
271 }
272 if let Err(e) = crate::f3::import_f3_snapshot(
273 chain_config,
274 rpc_endpoint.to_string(),
275 f3_root.display().to_string(),
276 temp_f3_snap.path().display().to_string(),
277 ) {
278 tracing::error!("Failed to import F3 snapshot: {e:#}");
280 }
281 }
282 }
283
284 let ts = forest_car.heaviest_tipset()?;
285 info!(
286 "Imported snapshot in: {}s, heaviest tipset epoch: {}, key: {}",
287 stopwatch.elapsed().as_secs(),
288 ts.epoch(),
289 ts.key()
290 );
291
292 Ok((forest_car_db_path, ts))
293}
294
295fn move_or_copy_file(from: &Path, to: &Path, import_mode: ImportMode) -> anyhow::Result<()> {
296 match import_mode {
297 ImportMode::Move => {
298 tracing::info!("Moving {} to {}", from.display(), to.display());
299 if fs::rename(from, to).is_ok() {
300 Ok(())
301 } else {
302 fs::copy(from, to).context("Error copying file")?;
303 fs::remove_file(from).context("Error removing original file")?;
304 Ok(())
305 }
306 }
307 ImportMode::Copy => {
308 tracing::info!("Copying {} to {}", from.display(), to.display());
309 fs::copy(from, to).map(|_| ()).context("Error copying file")
310 }
311 m => {
312 bail!("{m} must be handled elsewhere");
313 }
314 }
315}
316
317async fn transcode_into_forest_car(from: &Path, to: &Path) -> anyhow::Result<()> {
318 tracing::info!(
319 from = %from.display(),
320 to = %to.display(),
321 "transcoding into forest car"
322 );
323 let car_stream = CarStream::new_from_path(from).await?;
324 let roots = car_stream.header_v1.roots.clone();
325
326 let mut writer = tokio::io::BufWriter::new(tokio::fs::File::create(to).await?);
327 let frames = crate::db::car::forest::Encoder::compress_stream_default(
328 car_stream.map_err(anyhow::Error::from),
329 );
330 crate::db::car::forest::Encoder::write(&mut writer, roots, frames).await?;
331 writer.shutdown().await?;
332
333 Ok(())
334}
335
336pub const BACKFILL_CHECKPOINT_KEY: &str = "/index/backfill/checkpoint";
339
340enum ProcessOutcome {
342 Indexed,
344 Skipped,
347}
348
349#[derive(Debug, Clone, Copy)]
352pub struct BackfillOptions {
353 pub allow_recompute: bool,
356 pub allow_near_head: bool,
359 pub batch_size: usize,
361}
362
363impl Default for BackfillOptions {
364 fn default() -> Self {
365 Self {
366 allow_recompute: true,
367 allow_near_head: true,
368 batch_size: 1000,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Copy, Default)]
375pub struct BackfillReport {
376 pub indexed: u64,
377 pub skipped: u64,
378 pub cancelled: bool,
379}
380
381#[derive(Default)]
383struct BackfillCounters {
384 start_epoch: AtomicI64,
385 current_epoch: AtomicI64,
386 target_epoch: AtomicI64,
387 indexed: AtomicI64,
388 skipped: AtomicI64,
389}
390
391#[derive(Default)]
392struct BackfillStatusInner {
393 outcome: Option<ChainExportState>,
395 error: Option<String>,
396 start_time: Option<chrono::DateTime<chrono::Utc>>,
397 cancellation_token: Option<CancellationToken>,
398 counters: Arc<BackfillCounters>,
399}
400
401impl BackfillStatusInner {
402 fn is_running(&self) -> bool {
403 self.cancellation_token.is_some()
404 }
405}
406
407#[derive(Default)]
410pub struct BackfillStatus {
411 inner: parking_lot::Mutex<BackfillStatusInner>,
412}
413
414#[derive(Debug, Clone)]
416pub struct BackfillStatusSnapshot {
417 pub state: ChainExportState,
418 pub error: Option<String>,
419 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
420 pub start_epoch: ChainEpoch,
421 pub current_epoch: ChainEpoch,
422 pub target_epoch: ChainEpoch,
423 pub indexed: u64,
424 pub skipped: u64,
425}
426
427impl BackfillStatus {
428 pub fn snapshot(&self) -> BackfillStatusSnapshot {
429 let inner = self.inner.lock();
430 let c = &inner.counters;
431 BackfillStatusSnapshot {
432 state: if inner.is_running() {
433 ChainExportState::Running
434 } else {
435 inner.outcome.unwrap_or(ChainExportState::Idle)
436 },
437 error: inner.error.clone(),
438 start_time: inner.start_time,
439 start_epoch: c.start_epoch.load(Ordering::Relaxed),
440 current_epoch: c.current_epoch.load(Ordering::Relaxed),
441 target_epoch: c.target_epoch.load(Ordering::Relaxed),
442 indexed: c.indexed.load(Ordering::Relaxed).max(0) as u64,
443 skipped: c.skipped.load(Ordering::Relaxed).max(0) as u64,
444 }
445 }
446
447 pub fn cancel_running(&self) -> bool {
449 if let Some(token) = &self.inner.lock().cancellation_token {
450 token.cancel();
451 true
452 } else {
453 false
454 }
455 }
456
457 fn try_begin(
458 &self,
459 cancellation_token: CancellationToken,
460 ) -> anyhow::Result<Arc<BackfillCounters>> {
461 let mut inner = self.inner.lock();
462 anyhow::ensure!(
463 !inner.is_running(),
464 "an index backfill is already running; check `forest-cli index backfill-status`",
465 );
466 let counters = Arc::new(BackfillCounters::default());
467 *inner = BackfillStatusInner {
468 outcome: None,
469 error: None,
470 start_time: Some(chrono::Utc::now()),
471 cancellation_token: Some(cancellation_token),
472 counters: counters.clone(),
473 };
474 Ok(counters)
475 }
476
477 fn record_outcome(&self, outcome: ChainExportState, error: Option<String>) {
478 let mut inner = self.inner.lock();
479 if inner.outcome.is_none() {
480 inner.outcome = Some(outcome);
481 inner.error = error;
482 }
483 }
484
485 fn end(&self) {
486 let mut inner = self.inner.lock();
487 if inner.outcome.is_none() {
488 inner.outcome = Some(ChainExportState::Failed);
489 }
490 inner.cancellation_token = None;
491 }
492}
493
494pub static BACKFILL_STATUS: LazyLock<BackfillStatus> = LazyLock::new(BackfillStatus::default);
496
497pub struct BackfillGuard {
501 cancellation_token: CancellationToken,
502 counters: Arc<BackfillCounters>,
503 export_guard: crate::ipld::ChainExportGuard,
505}
506
507impl BackfillGuard {
508 pub fn try_start() -> anyhow::Result<Self> {
509 let export_guard = crate::ipld::ChainExportGuard::try_start_export(
511 crate::ipld::ChainExportKind::IndexBackfill,
512 )?;
513 let cancellation_token = CancellationToken::new();
514 let counters = match BACKFILL_STATUS.try_begin(cancellation_token.clone()) {
515 Ok(counters) => counters,
516 Err(e) => {
517 drop(export_guard);
519 return Err(e);
520 }
521 };
522 Ok(Self {
523 cancellation_token,
524 counters,
525 export_guard,
526 })
527 }
528
529 pub fn cancellation_token(&self) -> CancellationToken {
530 self.cancellation_token.clone()
531 }
532
533 pub fn finish<T>(self, result: anyhow::Result<T>) -> anyhow::Result<T> {
536 match &result {
537 Ok(_) => {
538 BACKFILL_STATUS.record_outcome(ChainExportState::Succeeded, None);
539 }
540 Err(e) => {
541 BACKFILL_STATUS.record_outcome(ChainExportState::Failed, Some(format!("{e:#}")));
542 }
543 }
544 self.export_guard
545 .record_outcome(ChainExportState::Idle, None);
546 result
547 }
548
549 fn reset_counters(&self, start_epoch: ChainEpoch, target_epoch: ChainEpoch) {
550 self.counters
551 .start_epoch
552 .store(start_epoch, Ordering::Relaxed);
553 self.counters
554 .current_epoch
555 .store(start_epoch, Ordering::Relaxed);
556 self.counters
557 .target_epoch
558 .store(target_epoch, Ordering::Relaxed);
559 self.counters.indexed.store(0, Ordering::Relaxed);
560 self.counters.skipped.store(0, Ordering::Relaxed);
561 }
562
563 fn set_current(&self, epoch: ChainEpoch) {
564 self.counters.current_epoch.store(epoch, Ordering::Relaxed);
565 }
566
567 fn inc_indexed(&self) {
568 self.counters.indexed.fetch_add(1, Ordering::Relaxed);
569 }
570
571 fn inc_skipped(&self) {
572 self.counters.skipped.fetch_add(1, Ordering::Relaxed);
573 }
574
575 fn record_cancelled(&self) {
576 BACKFILL_STATUS.record_outcome(ChainExportState::Cancelled, None);
577 self.export_guard
578 .record_outcome(ChainExportState::Idle, None);
579 }
580}
581
582impl Drop for BackfillGuard {
583 fn drop(&mut self) {
584 self.cancellation_token.cancel();
585 BACKFILL_STATUS.end();
586 }
587}
588
589pub fn read_backfill_checkpoint(
592 state_manager: &StateManager,
593) -> anyhow::Result<Option<ChainEpoch>> {
594 Ok(state_manager
595 .db()
596 .read_obj::<ChainEpoch>(BACKFILL_CHECKPOINT_KEY)?
597 .filter(|epoch| *epoch >= 0))
598}
599
600fn write_backfill_checkpoint(
601 state_manager: &StateManager,
602 epoch: ChainEpoch,
603) -> anyhow::Result<()> {
604 state_manager
605 .db()
606 .write_obj(BACKFILL_CHECKPOINT_KEY, &epoch)
607}
608
609fn clear_backfill_checkpoint(state_manager: &StateManager) -> anyhow::Result<()> {
610 write_backfill_checkpoint(state_manager, -1)
612}
613
614async fn process_ts(
615 ts: &Tipset,
616 state_manager: &StateManager,
617 delegated_messages: &mut Vec<(SignedMessage, u64)>,
618 allow_recompute: bool,
619) -> anyhow::Result<ProcessOutcome> {
620 let epoch = ts.epoch();
621 let tsk = ts.key().clone();
622
623 let executed = match state_manager
624 .load_executed_tipset_uncached(ts, allow_recompute)
625 .await
626 {
627 Ok(executed) => executed,
628 Err(e) => {
631 tracing::warn!(
632 "skipping tipset @{epoch} during backfill (state unavailable, recompute={allow_recompute}): {e:#}"
633 );
634 return Ok(ProcessOutcome::Skipped);
635 }
636 };
637 crate::rpc::eth::store_block_logs_bloom(
639 state_manager,
640 ts,
641 &executed.state_root,
642 &executed.executed_messages,
643 )?;
644
645 delegated_messages.append(
646 &mut state_manager
647 .chain_store()
648 .headers_delegated_messages(ts.block_headers().iter())?,
649 );
650 tracing::trace!("Indexing tipset @{}: {}", epoch, &tsk);
651 tsk.save(state_manager.db())?;
652
653 Ok(ProcessOutcome::Indexed)
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
657pub enum RangeSpec {
658 To(ChainEpoch),
659 NumTipsets(usize),
660}
661
662impl RangeSpec {
663 pub fn new(to: Option<ChainEpoch>, n_tipsets: Option<usize>) -> anyhow::Result<Self> {
666 match (to, n_tipsets) {
667 (Some(to), None) => {
668 anyhow::ensure!(to >= 0, "'to' must not be negative, got {to}.");
669 Ok(Self::To(to))
670 }
671 (None, Some(n)) => Ok(Self::NumTipsets(n)),
672 (None, None) => anyhow::bail!("You must provide either 'to' or 'n_tipsets'."),
673 (Some(_), Some(_)) => anyhow::bail!("'to' and 'n_tipsets' are mutually exclusive."),
674 }
675 }
676}
677
678impl std::fmt::Display for RangeSpec {
679 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680 match self {
681 RangeSpec::To(epoch) => write!(f, "To epoch: {epoch}"),
682 RangeSpec::NumTipsets(n) => write!(f, "Tipsets: {n}"),
683 }
684 }
685}
686
687pub async fn backfill_db(
697 state_manager: &StateManager,
698 head_ts: &Tipset,
699 spec: RangeSpec,
700) -> anyhow::Result<()> {
701 let guard = BackfillGuard::try_start()?;
702 let result = run_backfill(
703 state_manager,
704 head_ts,
705 spec,
706 BackfillOptions::default(),
707 &guard,
708 )
709 .await;
710 let report = guard.finish(result)?;
711 tracing::info!(
712 "Total successful backfills: {} (skipped: {})",
713 report.indexed,
714 report.skipped
715 );
716 Ok(())
717}
718
719pub async fn run_backfill(
733 state_manager: &StateManager,
734 from_ts: &Tipset,
735 spec: RangeSpec,
736 options: BackfillOptions,
737 guard: &BackfillGuard,
738) -> anyhow::Result<BackfillReport> {
739 tracing::info!("Starting index backfill...");
740
741 let cancel = guard.cancellation_token();
742
743 let head_rx = state_manager.chain_store().subscribe_head_changes();
745
746 let start_ts = if options.allow_near_head {
748 from_ts.shallow_clone()
749 } else {
750 let safe_epoch = state_manager.chain_store().ec_calculator_finalized_epoch();
751 if from_ts.epoch() > safe_epoch {
752 state_manager
753 .chain_index()
754 .load_required_tipset_by_height(
755 safe_epoch,
756 from_ts.shallow_clone(),
757 crate::chain::index::ResolveNullTipset::TakeOlder,
758 )
759 .await?
760 } else {
761 from_ts.shallow_clone()
762 }
763 };
764
765 let target_epoch = match spec {
766 RangeSpec::To(to_epoch) => to_epoch,
767 RangeSpec::NumTipsets(n) => start_ts.epoch().saturating_sub(n as ChainEpoch),
769 };
770 guard.reset_counters(start_ts.epoch(), target_epoch);
771
772 let mut batch: Vec<(SignedMessage, u64)> = vec![];
773 let mut report = BackfillReport::default();
774 let mut processed_since_flush = 0usize;
775 let mut lowest_epoch = start_ts.epoch();
776
777 for (count, ts) in start_ts
778 .shallow_clone()
779 .chain(&state_manager.chain_store().db())
780 .enumerate()
781 {
782 match spec {
783 RangeSpec::To(to_epoch) if ts.epoch() < to_epoch => break,
784 RangeSpec::NumTipsets(n) if count >= n => break,
785 _ => {}
786 }
787
788 if cancel.is_cancelled() {
789 report.cancelled = true;
790 break;
791 }
792
793 guard.set_current(ts.epoch());
794 lowest_epoch = ts.epoch();
795 match process_ts(&ts, state_manager, &mut batch, options.allow_recompute).await? {
796 ProcessOutcome::Indexed => {
797 report.indexed += 1;
798 guard.inc_indexed();
799 }
800 ProcessOutcome::Skipped => {
801 report.skipped += 1;
802 guard.inc_skipped();
803 }
804 }
805 processed_since_flush += 1;
806
807 if processed_since_flush >= options.batch_size {
808 state_manager
809 .chain_store()
810 .process_signed_messages(&batch, true)?;
811 batch.clear();
812 write_backfill_checkpoint(state_manager, ts.epoch())?;
813 processed_since_flush = 0;
814 }
815 }
816
817 state_manager
819 .chain_store()
820 .process_signed_messages(&batch, true)?;
821 batch.clear();
822
823 if !report.cancelled {
825 let mut extra: Vec<(SignedMessage, u64)> = vec![];
826 for changes in head_rx.try_iter() {
827 for ts in changes.applies {
828 if ts.epoch() >= lowest_epoch && ts.epoch() <= start_ts.epoch() {
829 tracing::debug!("re-indexing tipset @{} applied during backfill", ts.epoch());
830 if let Err(e) =
831 process_ts(&ts, state_manager, &mut extra, options.allow_recompute).await
832 {
833 tracing::warn!("failed to re-index applied tipset @{}: {e:#}", ts.epoch());
834 }
835 }
836 }
837 }
838 if !extra.is_empty() {
839 state_manager
840 .chain_store()
841 .process_signed_messages(&extra, true)?;
842 }
843 }
844
845 if report.cancelled {
846 write_backfill_checkpoint(state_manager, lowest_epoch)?;
848 guard.record_cancelled();
849 tracing::info!(
850 "Index backfill cancelled after {} tipsets (skipped: {})",
851 report.indexed,
852 report.skipped
853 );
854 } else {
855 clear_backfill_checkpoint(state_manager)?;
857 }
858
859 Ok(report)
860}
861
862#[cfg(test)]
863mod test {
864 use super::*;
865
866 #[test]
869 #[serial_test::serial(chain_export)]
870 fn backfill_guard_is_single_flight_and_records_outcomes() {
871 let g = BackfillGuard::try_start().unwrap();
872 assert_eq!(BACKFILL_STATUS.snapshot().state, ChainExportState::Running);
873
874 assert!(BackfillGuard::try_start().is_err());
876
877 g.finish(anyhow::Ok(())).unwrap();
879 assert_eq!(
880 BACKFILL_STATUS.snapshot().state,
881 ChainExportState::Succeeded
882 );
883
884 let g = BackfillGuard::try_start().unwrap();
886 g.finish(anyhow::Result::<()>::Err(anyhow::anyhow!("boom")))
887 .unwrap_err();
888 let snapshot = BACKFILL_STATUS.snapshot();
889 assert_eq!(snapshot.state, ChainExportState::Failed);
890 assert_eq!(snapshot.error.as_deref(), Some("boom"));
891
892 let g = BackfillGuard::try_start().unwrap();
894 drop(g);
895 assert_eq!(BACKFILL_STATUS.snapshot().state, ChainExportState::Failed);
896 }
897
898 #[test]
899 #[serial_test::serial(chain_export)]
900 fn backfill_cancellation_is_observable_and_wins() {
901 let g = BackfillGuard::try_start().unwrap();
902 assert!(BACKFILL_STATUS.cancel_running());
904 assert!(g.cancellation_token().is_cancelled());
905
906 g.record_cancelled();
909 g.finish(anyhow::Ok(())).unwrap();
910 assert_eq!(
911 BACKFILL_STATUS.snapshot().state,
912 ChainExportState::Cancelled
913 );
914
915 assert!(!BACKFILL_STATUS.cancel_running());
917 }
918
919 #[test]
920 #[serial_test::serial(chain_export)]
921 fn backfill_and_snapshot_gc_are_mutually_exclusive() {
922 use crate::ipld::{ChainExportGuard, ChainExportKind};
923
924 let gc = ChainExportGuard::try_start_export(ChainExportKind::SnapshotGc).unwrap();
927 assert!(BackfillGuard::try_start().is_err());
928 drop(gc);
929
930 let bf = BackfillGuard::try_start().unwrap();
932 assert!(ChainExportGuard::try_start_export(ChainExportKind::SnapshotGc).is_err());
933 drop(bf);
934 }
935
936 fn test_state_manager() -> StateManager {
937 use crate::blocks::{CachingBlockHeader, RawBlockHeader};
938 use crate::chain::ChainStore;
939 use crate::networks::ChainConfig;
940 use crate::shim::address::Address;
941
942 let db = Arc::new(crate::db::MemoryDB::default());
943 let genesis = CachingBlockHeader::new(RawBlockHeader {
944 miner_address: Address::new_id(0),
945 timestamp: 7777,
946 ..Default::default()
947 });
948 let cs = ChainStore::new(db, Arc::new(ChainConfig::default()), genesis).unwrap();
949 StateManager::new(cs).unwrap()
950 }
951
952 #[test]
953 fn backfill_checkpoint_roundtrip_and_clear() {
954 let sm = test_state_manager();
955
956 assert_eq!(read_backfill_checkpoint(&sm).unwrap(), None);
958
959 write_backfill_checkpoint(&sm, 4321).unwrap();
960 assert_eq!(read_backfill_checkpoint(&sm).unwrap(), Some(4321));
961
962 clear_backfill_checkpoint(&sm).unwrap();
965 assert_eq!(read_backfill_checkpoint(&sm).unwrap(), None);
966 }
967
968 #[tokio::test]
969 async fn import_snapshot_from_file_valid() {
970 for import_mode in [ImportMode::Auto, ImportMode::Copy, ImportMode::Move] {
971 import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
972 .await
973 .unwrap();
974 }
975
976 for import_mode in [ImportMode::Symlink, ImportMode::Hardlink] {
978 import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
979 .await
980 .unwrap_err();
981 }
982 }
983
984 #[tokio::test]
985 async fn import_snapshot_from_compressed_file_valid() {
986 for import_mode in [ImportMode::Auto, ImportMode::Copy, ImportMode::Move] {
987 import_snapshot_from_file("test-snapshots/chain4.car.zst", import_mode)
988 .await
989 .unwrap();
990 }
991
992 for import_mode in [ImportMode::Symlink, ImportMode::Hardlink] {
994 import_snapshot_from_file("test-snapshots/chain4.car", import_mode)
995 .await
996 .unwrap_err();
997 }
998 }
999
1000 #[tokio::test]
1001 async fn import_snapshot_from_forest_car_valid() {
1002 for import_mode in [
1003 ImportMode::Auto,
1004 ImportMode::Copy,
1005 ImportMode::Move,
1006 ImportMode::Symlink,
1007 ImportMode::Hardlink,
1008 ] {
1009 import_snapshot_from_file("test-snapshots/chain4.forest.car.zst", import_mode)
1010 .await
1011 .unwrap();
1012 }
1013 }
1014
1015 #[tokio::test]
1016 async fn import_snapshot_from_file_invalid() {
1017 for import_mode in &[
1018 ImportMode::Auto,
1019 ImportMode::Copy,
1020 ImportMode::Move,
1021 ImportMode::Symlink,
1022 ImportMode::Hardlink,
1023 ] {
1024 import_snapshot_from_file("Cargo.toml", *import_mode)
1025 .await
1026 .unwrap_err();
1027 }
1028 }
1029
1030 #[tokio::test]
1031 async fn import_snapshot_from_file_not_found() {
1032 for import_mode in &[
1033 ImportMode::Auto,
1034 ImportMode::Copy,
1035 ImportMode::Move,
1036 ImportMode::Symlink,
1037 ImportMode::Hardlink,
1038 ] {
1039 import_snapshot_from_file("dummy.car", *import_mode)
1040 .await
1041 .unwrap_err();
1042 }
1043 }
1044
1045 #[tokio::test]
1046 async fn import_snapshot_from_url_not_found() {
1047 for import_mode in &[
1048 ImportMode::Auto,
1049 ImportMode::Copy,
1050 ImportMode::Move,
1051 ImportMode::Symlink,
1052 ImportMode::Hardlink,
1053 ] {
1054 import_snapshot_from_file("https://forest.chainsafe.io/dummy.car", *import_mode)
1055 .await
1056 .unwrap_err();
1057 }
1058 }
1059
1060 async fn import_snapshot_from_file(
1061 file_path: &str,
1062 import_mode: ImportMode,
1063 ) -> anyhow::Result<()> {
1064 let temp_file = tempfile::Builder::new().tempfile()?;
1066 fs::copy(Path::new(file_path), temp_file.path())?;
1067 let file_path = temp_file.path();
1068
1069 let temp_db_dir = tempfile::Builder::new().tempdir()?;
1070
1071 let (path, ts) = import_chain_as_forest_car(
1072 file_path,
1073 temp_db_dir.path(),
1074 import_mode,
1075 "http://127.0.0.1:2345/rpc/v1".parse().unwrap(),
1076 Path::new("test"),
1077 &ChainConfig::devnet(),
1078 &SnapshotProgressTracker::default(),
1079 )
1080 .await?;
1081 match import_mode {
1082 ImportMode::Symlink => {
1083 assert_eq!(
1084 std::path::absolute(path.read_link()?)?,
1085 std::path::absolute(file_path)?
1086 );
1087 }
1088 ImportMode::Move => {
1089 assert!(!file_path.exists());
1090 assert!(path.is_file());
1091 }
1092 _ => {
1093 assert!(file_path.is_file());
1094 assert!(path.is_file());
1095 }
1096 }
1097 assert!(ts.epoch() > 0);
1098 Ok(())
1099 }
1100}