Skip to main content

forest/cli/subcommands/
index_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::ipld::ChainExportState;
5use crate::rpc::chain::{
6    ApiIndexBackfillStatus, IndexBackfill, IndexBackfillCancel, IndexBackfillParams,
7    IndexBackfillStatus,
8};
9use crate::rpc::{self, prelude::*};
10use crate::shim::clock::ChainEpoch;
11use clap::Subcommand;
12use indicatif::{ProgressBar, ProgressStyle};
13use std::time::Duration;
14
15#[derive(Debug, Subcommand)]
16pub enum IndexCommands {
17    /// Backfill the chain index (Ethereum mappings, events, block blooms) using the running node.
18    ///
19    /// Unlike `forest-tool index backfill`, this does not require the node to be stopped: the
20    /// running daemon performs the backfill through its own database handle.
21    #[command(group(clap::ArgGroup::new("range").required(true).args(["to", "n_tipsets"])))]
22    Backfill {
23        /// Starting tipset epoch for back-filling (inclusive). Defaults to the chain head, unless
24        /// `--resume` is given and a resume checkpoint exists.
25        #[arg(long)]
26        from: Option<ChainEpoch>,
27        /// Ending tipset epoch for back-filling (inclusive).
28        #[arg(long)]
29        to: Option<ChainEpoch>,
30        /// Number of tipsets to back-fill.
31        #[arg(long, conflicts_with = "to")]
32        n_tipsets: Option<u64>,
33        /// Recompute missing tipset state (expensive) instead of skipping it; tipsets that still
34        /// can't be computed are skipped and reported rather than aborting the run.
35        #[arg(long)]
36        recompute: bool,
37        /// Also index revert-prone tipsets newer than the EC-finalized epoch (up to the head). By
38        /// default the walk is clamped to the EC-finalized epoch.
39        #[arg(long)]
40        allow_near_head: bool,
41        /// Resume from the persisted checkpoint of a previous run instead of starting at the chain
42        /// head. Ignored when `--from` is given.
43        #[arg(long)]
44        resume: bool,
45        /// Trigger the backfill and return immediately without waiting for completion.
46        #[arg(long)]
47        no_wait: bool,
48    },
49    /// Show the status of the current (or last) index backfill.
50    BackfillStatus {
51        /// Wait until the backfill completes, showing progress.
52        #[arg(long)]
53        wait: bool,
54    },
55    /// Cancel the in-progress index backfill.
56    BackfillCancel {},
57}
58
59impl IndexCommands {
60    pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
61        match self {
62            Self::Backfill {
63                from,
64                to,
65                n_tipsets,
66                recompute,
67                allow_near_head,
68                resume,
69                no_wait,
70            } => {
71                let params = IndexBackfillParams {
72                    from,
73                    to,
74                    n_tipsets,
75                    recompute,
76                    allow_near_head,
77                    resume,
78                };
79                client
80                    .call(IndexBackfill::request((params,))?.with_timeout(Duration::from_secs(30)))
81                    .await?;
82                println!("Index backfill started.");
83                if no_wait {
84                    println!("Use `forest-cli index backfill-status` to monitor progress.");
85                    return Ok(());
86                }
87                wait_for_backfill(&client).await
88            }
89            Self::BackfillStatus { wait } => {
90                let status = client
91                    .call(IndexBackfillStatus::request(())?.with_timeout(Duration::from_secs(30)))
92                    .await?;
93                if !wait || status.state != ChainExportState::Running {
94                    println!("{status}");
95                    return Ok(());
96                }
97                wait_for_backfill(&client).await
98            }
99            Self::BackfillCancel {} => {
100                let cancelled = client
101                    .call(IndexBackfillCancel::request(())?.with_timeout(Duration::from_secs(30)))
102                    .await?;
103                if cancelled {
104                    println!("Index backfill cancelled.");
105                } else {
106                    println!("No index backfill in progress to cancel.");
107                }
108                Ok(())
109            }
110        }
111    }
112}
113
114/// Polls `Forest.IndexBackfillStatus` until the backfill reaches a terminal state, rendering a
115/// progress bar.
116async fn wait_for_backfill(client: &rpc::Client) -> anyhow::Result<()> {
117    let pb = ProgressBar::new(10000).with_message("Backfilling index");
118    pb.set_style(
119        ProgressStyle::with_template("[{elapsed_precise}] [{wide_bar}] {percent}% {msg}")
120            .expect("indicatif template must be valid")
121            .progress_chars("#>-"),
122    );
123    let last: ApiIndexBackfillStatus = loop {
124        let status = client
125            .call(IndexBackfillStatus::request(())?.with_timeout(Duration::from_secs(30)))
126            .await?;
127        let position = (status.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
128        pb.set_position(position);
129        if status.state != ChainExportState::Running {
130            break status;
131        }
132        tokio::time::sleep(Duration::from_millis(500)).await;
133    };
134    match last.state {
135        ChainExportState::Succeeded => pb.finish_with_message(format!(
136            "Backfill completed (indexed {}, skipped {})",
137            last.indexed, last.skipped
138        )),
139        ChainExportState::Cancelled => pb.abandon_with_message(format!(
140            "Backfill cancelled (indexed {}, skipped {})",
141            last.indexed, last.skipped
142        )),
143        _ => {
144            pb.abandon_with_message("Backfill failed");
145            anyhow::bail!(
146                "index backfill failed: {}",
147                last.error.as_deref().unwrap_or("unknown error")
148            );
149        }
150    }
151    Ok(())
152}