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