Skip to main content

clone_solana_ledger/
bigtable_upload.rs

1use {
2    crate::blockstore::Blockstore,
3    crossbeam_channel::{bounded, unbounded},
4    log::*,
5    clone_solana_measure::measure::Measure,
6    clone_solana_sdk::clock::Slot,
7    std::{
8        cmp::{max, min},
9        collections::HashSet,
10        result::Result,
11        sync::{
12            atomic::{AtomicBool, Ordering},
13            Arc,
14        },
15        time::{Duration, Instant},
16    },
17};
18
19#[derive(Clone)]
20pub struct ConfirmedBlockUploadConfig {
21    pub force_reupload: bool,
22    pub max_num_slots_to_check: usize,
23    pub num_blocks_to_upload_in_parallel: usize,
24    pub block_read_ahead_depth: usize, // should always be >= `num_blocks_to_upload_in_parallel`
25}
26
27impl Default for ConfirmedBlockUploadConfig {
28    fn default() -> Self {
29        let num_blocks_to_upload_in_parallel = num_cpus::get() / 2;
30        ConfirmedBlockUploadConfig {
31            force_reupload: false,
32            max_num_slots_to_check: num_blocks_to_upload_in_parallel * 4,
33            num_blocks_to_upload_in_parallel,
34            block_read_ahead_depth: num_blocks_to_upload_in_parallel * 2,
35        }
36    }
37}
38
39struct BlockstoreLoadStats {
40    pub num_blocks_read: usize,
41    pub elapsed: Duration,
42}
43
44/// Uploads a range of blocks from a Blockstore to bigtable LedgerStorage
45/// Returns the Slot of the last block checked. If no blocks in the range `[staring_slot,
46/// ending_slot]` are found in Blockstore, this value is equal to `ending_slot`.
47pub async fn upload_confirmed_blocks(
48    blockstore: Arc<Blockstore>,
49    bigtable: clone_solana_storage_bigtable::LedgerStorage,
50    starting_slot: Slot,
51    ending_slot: Slot,
52    config: ConfirmedBlockUploadConfig,
53    exit: Arc<AtomicBool>,
54) -> Result<Slot, Box<dyn std::error::Error>> {
55    let mut measure = Measure::start("entire upload");
56
57    info!(
58        "Loading ledger slots from {} to {}",
59        starting_slot, ending_slot
60    );
61    let blockstore_slots: Vec<_> = blockstore
62        .rooted_slot_iterator(starting_slot)
63        .map_err(|err| {
64            format!("Failed to load entries starting from slot {starting_slot}: {err:?}")
65        })?
66        .take_while(|slot| *slot <= ending_slot)
67        .collect();
68
69    if blockstore_slots.is_empty() {
70        warn!("Ledger has no slots from {starting_slot} to {ending_slot:?}");
71        return Ok(ending_slot);
72    }
73
74    let first_blockstore_slot = *blockstore_slots.first().unwrap();
75    let last_blockstore_slot = *blockstore_slots.last().unwrap();
76    info!(
77        "Found {} slots in the range ({}, {})",
78        blockstore_slots.len(),
79        first_blockstore_slot,
80        last_blockstore_slot,
81    );
82
83    // Gather the blocks that are already present in bigtable, by slot
84    let bigtable_slots = if !config.force_reupload {
85        let mut bigtable_slots = vec![];
86        info!(
87            "Loading list of bigtable blocks between slots {} and {}...",
88            first_blockstore_slot, last_blockstore_slot
89        );
90
91        let mut start_slot = first_blockstore_slot;
92        while start_slot <= last_blockstore_slot {
93            let mut next_bigtable_slots = loop {
94                let num_bigtable_blocks = min(1000, config.max_num_slots_to_check * 2);
95                match bigtable
96                    .get_confirmed_blocks(start_slot, num_bigtable_blocks)
97                    .await
98                {
99                    Ok(slots) => break slots,
100                    Err(err) => {
101                        error!("get_confirmed_blocks for {} failed: {:?}", start_slot, err);
102                        // Consider exponential backoff...
103                        tokio::time::sleep(Duration::from_secs(2)).await;
104                    }
105                }
106            };
107            if next_bigtable_slots.is_empty() {
108                break;
109            }
110            bigtable_slots.append(&mut next_bigtable_slots);
111            start_slot = bigtable_slots.last().unwrap() + 1;
112        }
113        bigtable_slots
114            .into_iter()
115            .filter(|slot| *slot <= last_blockstore_slot)
116            .collect::<Vec<_>>()
117    } else {
118        Vec::new()
119    };
120
121    // The blocks that still need to be uploaded is the difference between what's already in the
122    // bigtable and what's in blockstore...
123    let blocks_to_upload = {
124        let blockstore_slots = blockstore_slots.into_iter().collect::<HashSet<_>>();
125        let bigtable_slots = bigtable_slots.into_iter().collect::<HashSet<_>>();
126
127        let mut blocks_to_upload = blockstore_slots
128            .difference(&bigtable_slots)
129            .cloned()
130            .collect::<Vec<_>>();
131        blocks_to_upload.sort_unstable();
132        blocks_to_upload.truncate(config.max_num_slots_to_check);
133        blocks_to_upload
134    };
135
136    if blocks_to_upload.is_empty() {
137        info!(
138            "No blocks between {} and {} need to be uploaded to bigtable",
139            starting_slot, ending_slot
140        );
141        return Ok(ending_slot);
142    }
143    let last_slot = *blocks_to_upload.last().unwrap();
144    info!(
145        "{} blocks to be uploaded to the bucket in the range ({}, {})",
146        blocks_to_upload.len(),
147        blocks_to_upload.first().unwrap(),
148        last_slot
149    );
150
151    // Distribute the blockstore reading across a few background threads to speed up the bigtable uploading
152    let (loader_threads, receiver): (Vec<_>, _) = {
153        let exit = exit.clone();
154
155        let (sender, receiver) = bounded(config.block_read_ahead_depth);
156
157        let (slot_sender, slot_receiver) = unbounded();
158        blocks_to_upload
159            .into_iter()
160            .for_each(|b| slot_sender.send(b).unwrap());
161        drop(slot_sender);
162
163        (
164            (0..config.num_blocks_to_upload_in_parallel)
165                .map(|i| {
166                    let blockstore = blockstore.clone();
167                    let sender = sender.clone();
168                    let slot_receiver = slot_receiver.clone();
169                    let exit = exit.clone();
170                    std::thread::Builder::new()
171                        .name(format!("solBigTGetBlk{i:02}"))
172                        .spawn(move || {
173                            let start = Instant::now();
174                            let mut num_blocks_read = 0;
175
176                            while let Ok(slot) = slot_receiver.recv() {
177                                if exit.load(Ordering::Relaxed) {
178                                    break;
179                                }
180
181                                let _ = match blockstore.get_rooted_block_with_entries(slot, true) {
182                                    Ok(confirmed_block_with_entries) => {
183                                        num_blocks_read += 1;
184                                        sender.send((slot, Some(confirmed_block_with_entries)))
185                                    }
186                                    Err(err) => {
187                                        warn!(
188                                            "Failed to get load confirmed block from slot {}: {:?}",
189                                            slot, err
190                                        );
191                                        sender.send((slot, None))
192                                    }
193                                };
194                            }
195                            BlockstoreLoadStats {
196                                num_blocks_read,
197                                elapsed: start.elapsed(),
198                            }
199                        })
200                        .unwrap()
201                })
202                .collect(),
203            receiver,
204        )
205    };
206
207    let mut failures = 0;
208    use futures::stream::StreamExt;
209
210    let mut stream =
211        tokio_stream::iter(receiver.into_iter()).chunks(config.num_blocks_to_upload_in_parallel);
212
213    while let Some(blocks) = stream.next().await {
214        if exit.load(Ordering::Relaxed) {
215            break;
216        }
217
218        let mut measure_upload = Measure::start("Upload");
219        let mut num_blocks = blocks.len();
220        info!("Preparing the next {} blocks for upload", num_blocks);
221
222        let uploads = blocks.into_iter().filter_map(|(slot, block)| match block {
223            None => {
224                num_blocks -= 1;
225                None
226            }
227            Some(confirmed_block) => {
228                let bt = bigtable.clone();
229                Some(tokio::spawn(async move {
230                    bt.upload_confirmed_block_with_entries(slot, confirmed_block)
231                        .await
232                }))
233            }
234        });
235
236        for result in futures::future::join_all(uploads).await {
237            if let Err(err) = result {
238                error!("upload_confirmed_block() join failed: {:?}", err);
239                failures += 1;
240            } else if let Err(err) = result.unwrap() {
241                error!("upload_confirmed_block() upload failed: {:?}", err);
242                failures += 1;
243            }
244        }
245
246        measure_upload.stop();
247        info!("{} for {} blocks", measure_upload, num_blocks);
248    }
249
250    measure.stop();
251    info!("{}", measure);
252
253    let blockstore_results = loader_threads.into_iter().map(|t| t.join());
254
255    let mut blockstore_num_blocks_read = 0;
256    let mut blockstore_load_wallclock = Duration::default();
257    let mut blockstore_errors = 0;
258
259    for r in blockstore_results {
260        match r {
261            Ok(stats) => {
262                blockstore_num_blocks_read += stats.num_blocks_read;
263                blockstore_load_wallclock = max(stats.elapsed, blockstore_load_wallclock);
264            }
265            Err(e) => {
266                error!("error joining blockstore thread: {:?}", e);
267                blockstore_errors += 1;
268            }
269        }
270    }
271
272    info!(
273        "blockstore upload took {:?} for {} blocks ({:.2} blocks/s) errors: {}",
274        blockstore_load_wallclock,
275        blockstore_num_blocks_read,
276        blockstore_num_blocks_read as f64 / blockstore_load_wallclock.as_secs_f64(),
277        blockstore_errors
278    );
279
280    if failures > 0 {
281        Err(format!("Incomplete upload, {failures} operations failed").into())
282    } else {
283        Ok(last_slot)
284    }
285}