Skip to main content

ab_farmer/single_disk_farm/
plotting.rs

1use crate::farm::{SectorExpirationDetails, SectorPlottingDetails, SectorUpdate};
2use crate::node_client::NodeClient;
3use crate::plotter::{Plotter, SectorPlottingProgress};
4use crate::single_disk_farm::direct_io_file_wrapper::DirectIoFileWrapper;
5use crate::single_disk_farm::metrics::{SectorState, SingleDiskFarmMetrics};
6use crate::single_disk_farm::{
7    BackgroundTaskError, Handlers, PlotMetadataHeader, RESERVED_PLOT_METADATA,
8};
9use ab_core_primitives::ed25519::Ed25519PublicKey;
10use ab_core_primitives::hashes::Blake3Hash;
11use ab_core_primitives::pieces::PieceOffset;
12use ab_core_primitives::sectors::{SectorId, SectorIndex};
13use ab_core_primitives::segments::{HistorySize, SegmentIndex};
14use ab_farmer_components::file_ext::FileExt;
15use ab_farmer_components::plotting::PlottedSector;
16use ab_farmer_components::sector::SectorMetadataChecksummed;
17use ab_farmer_components::shard_commitment::ShardCommitmentsRootsCache;
18use async_lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock, Semaphore, SemaphoreGuard};
19use futures::channel::{mpsc, oneshot};
20use futures::stream::FuturesOrdered;
21use futures::{FutureExt, SinkExt, StreamExt, select};
22use parity_scale_codec::Encode;
23use parking_lot::RwLock;
24use rand::prelude::*;
25use std::collections::HashSet;
26use std::future::Future;
27use std::io;
28use std::num::NonZeroUsize;
29use std::ops::Range;
30use std::pin::pin;
31use std::sync::Arc as StdArc;
32use std::time::{Duration, Instant};
33use thiserror::Error;
34use tokio::sync::watch;
35use tokio::task;
36use tracing::{Instrument, debug, info, info_span, trace, warn};
37use triomphe::Arc;
38
39const FARMER_APP_INFO_RETRY_INTERVAL: Duration = Duration::from_millis(500);
40const PLOTTING_RETRY_DELAY: Duration = Duration::from_secs(1);
41
42pub(super) struct SectorToPlot {
43    sector_index: SectorIndex,
44    /// Progress so far in % (not including this sector)
45    progress: f32,
46    /// Whether this is the last sector queued so far
47    last_queued: bool,
48    acknowledgement_sender: oneshot::Sender<()>,
49}
50
51/// Errors that happen during plotting
52#[derive(Debug, Error)]
53pub enum PlottingError {
54    /// Failed to retrieve farmer info
55    #[error("Failed to retrieve farmer info: {error}")]
56    FailedToGetFarmerInfo {
57        /// Lower-level error
58        error: anyhow::Error,
59    },
60    /// Failed to get super segment root
61    #[error("Failed to get super segment root: {error}")]
62    FailedToGetSuperSegmentRoot {
63        /// Lower-level error
64        error: anyhow::Error,
65    },
66    /// Missing archived segment header
67    #[error("Missing archived segment header: {segment_index}")]
68    MissingArchivedSegmentHeader {
69        /// Segment index that was missing
70        segment_index: SegmentIndex,
71    },
72    /// Failed to subscribe to archived segments
73    #[error("Failed to subscribe to archived segments: {error}")]
74    FailedToSubscribeArchivedSegments {
75        /// Lower-level error
76        error: anyhow::Error,
77    },
78    /// Low-level plotting error
79    #[error("Low-level plotting error: {0}")]
80    LowLevel(String),
81    /// I/O error occurred
82    #[error("Plotting I/O error: {0}")]
83    Io(#[from] io::Error),
84    /// Background downloading panicked
85    #[error("Background downloading panicked")]
86    BackgroundDownloadingPanicked,
87}
88
89pub(super) struct SectorPlottingOptions<'a, NC> {
90    pub(super) public_key: Ed25519PublicKey,
91    pub(super) shard_commitments_roots_cache: ShardCommitmentsRootsCache,
92    pub(super) node_client: &'a NC,
93    pub(super) pieces_in_sector: u16,
94    pub(super) sector_size: usize,
95    pub(super) plot_file: StdArc<DirectIoFileWrapper>,
96    pub(super) metadata_file: Arc<DirectIoFileWrapper>,
97    pub(super) handlers: &'a Handlers,
98    pub(super) global_mutex: &'a AsyncMutex<()>,
99    pub(super) plotter: StdArc<dyn Plotter>,
100    pub(super) metrics: Option<Arc<SingleDiskFarmMetrics>>,
101}
102
103pub(super) struct PlottingOptions<'a, NC> {
104    pub(super) metadata_header: PlotMetadataHeader,
105    pub(super) sectors_metadata: &'a AsyncRwLock<Vec<SectorMetadataChecksummed>>,
106    pub(super) sectors_being_modified: &'a AsyncRwLock<HashSet<SectorIndex>>,
107    pub(super) sectors_to_plot_receiver: mpsc::Receiver<SectorToPlot>,
108    pub(super) sector_plotting_options: SectorPlottingOptions<'a, NC>,
109    pub(super) max_plotting_sectors_per_farm: NonZeroUsize,
110}
111
112/// Starts plotting process.
113///
114/// NOTE: Returned future is async, but does blocking operations and should be running in dedicated
115/// thread.
116pub(super) async fn plotting<NC>(
117    plotting_options: PlottingOptions<'_, NC>,
118) -> Result<(), PlottingError>
119where
120    NC: NodeClient,
121{
122    let PlottingOptions {
123        mut metadata_header,
124        sectors_metadata,
125        sectors_being_modified,
126        mut sectors_to_plot_receiver,
127        sector_plotting_options,
128        max_plotting_sectors_per_farm,
129    } = plotting_options;
130
131    let sector_plotting_options = &sector_plotting_options;
132    let plotting_semaphore = Semaphore::new(max_plotting_sectors_per_farm.get());
133    let max_used_history_size = RwLock::new(
134        sectors_metadata
135            .read()
136            .await
137            .iter()
138            .map(|sector_metadata| sector_metadata.history_size)
139            .max()
140            .unwrap_or(HistorySize::ONE),
141    );
142    let mut sectors_being_plotted = FuturesOrdered::new();
143    // Channel size is intentionally unbounded for easier analysis, but it is bounded by plotting
144    // semaphore in practice due to permit stored in `SectorPlottingResult`
145    let (sector_plotting_result_sender, mut sector_plotting_result_receiver) = mpsc::unbounded();
146    let process_plotting_result_fut = async move {
147        while let Some(sector_plotting_result) = sector_plotting_result_receiver.next().await {
148            process_plotting_result(
149                sector_plotting_result,
150                sectors_metadata,
151                sectors_being_modified,
152                &mut metadata_header,
153                Arc::clone(&sector_plotting_options.metadata_file),
154            )
155            .await?;
156        }
157
158        unreachable!(
159            "Stream will not end before the rest of the plotting process is shutting down"
160        );
161    };
162    let process_plotting_result_fut = process_plotting_result_fut.fuse();
163    let mut process_plotting_result_fut = pin!(process_plotting_result_fut);
164
165    // Wait for new sectors to plot from `sectors_to_plot_receiver` and wait for sectors that
166    // already started plotting to finish plotting and then update metadata header
167    loop {
168        select! {
169            maybe_sector_to_plot = sectors_to_plot_receiver.next() => {
170                let Some(sector_to_plot) = maybe_sector_to_plot else {
171                    break;
172                };
173
174                let sector_index = sector_to_plot.sector_index;
175                let sector_plotting_init_fut = plot_single_sector(
176                    sector_to_plot,
177                    sector_plotting_options,
178                    sectors_metadata,
179                    sectors_being_modified,
180                    &plotting_semaphore,
181                    &max_used_history_size,
182                )
183                    .instrument(info_span!("", %sector_index))
184                    .fuse();
185                let mut sector_plotting_init_fut = pin!(sector_plotting_init_fut);
186
187                // Wait for plotting of new sector to start (backpressure), while also waiting
188                // for sectors that already started plotting to finish plotting and then update
189                // metadata header
190                loop {
191                    select! {
192                        sector_plotting_init_result = sector_plotting_init_fut => {
193                            let sector_plotting_fut = match sector_plotting_init_result {
194                                PlotSingleSectorResult::Scheduled(future) => future,
195                                PlotSingleSectorResult::Skipped => {
196                                    break;
197                                }
198                                PlotSingleSectorResult::FatalError(error) => {
199                                    return Err(error);
200                                }
201                            };
202                            sectors_being_plotted.push_back(
203                                sector_plotting_fut.instrument(info_span!("", %sector_index))
204                            );
205                            break;
206                        }
207                        maybe_sector_plotting_result = sectors_being_plotted.select_next_some() => {
208                            sector_plotting_result_sender
209                                .unbounded_send(maybe_sector_plotting_result?)
210                                .expect("Sending means receiver is not dropped yet; qed");
211                        }
212                        result = process_plotting_result_fut => {
213                            return result;
214                        }
215                    }
216                }
217            }
218            maybe_sector_plotting_result = sectors_being_plotted.select_next_some() => {
219                sector_plotting_result_sender
220                    .unbounded_send(maybe_sector_plotting_result?)
221                    .expect("Sending means receiver is not dropped yet; qed");
222            }
223            result = process_plotting_result_fut => {
224                return result;
225            }
226        }
227    }
228
229    Ok(())
230}
231
232async fn process_plotting_result(
233    sector_plotting_result: SectorPlottingResult<'_>,
234    sectors_metadata: &AsyncRwLock<Vec<SectorMetadataChecksummed>>,
235    sectors_being_modified: &AsyncRwLock<HashSet<SectorIndex>>,
236    metadata_header: &mut PlotMetadataHeader,
237    metadata_file: Arc<DirectIoFileWrapper>,
238) -> Result<(), PlottingError> {
239    let SectorPlottingResult {
240        sector_metadata,
241        replotting,
242        last_queued,
243        plotting_permit,
244    } = sector_plotting_result;
245
246    let sector_index = sector_metadata.sector_index;
247
248    {
249        let mut sectors_metadata = sectors_metadata.write().await;
250        // If exists then we're replotting, otherwise we create sector for the first time
251        if let Some(existing_sector_metadata) = sectors_metadata.get_mut(usize::from(sector_index))
252        {
253            *existing_sector_metadata = sector_metadata;
254        } else {
255            sectors_metadata.push(sector_metadata);
256        }
257    }
258
259    // Inform others that this sector is no longer being modified
260    sectors_being_modified.write().await.remove(&sector_index);
261
262    if u16::from(sector_index) + 1 > metadata_header.plotted_sector_count {
263        metadata_header.plotted_sector_count = u16::from(sector_index) + 1;
264
265        let encoded_metadata_header = metadata_header.encode();
266        let write_fut =
267            task::spawn_blocking(move || metadata_file.write_all_at(&encoded_metadata_header, 0));
268        write_fut.await.map_err(|error| {
269            PlottingError::LowLevel(format!("Failed to spawn blocking tokio task: {error}"))
270        })??;
271    }
272
273    if last_queued {
274        if replotting {
275            info!("Replotting complete");
276        } else {
277            info!("Initial plotting complete");
278        }
279    }
280
281    drop(plotting_permit);
282
283    Ok(())
284}
285
286enum PlotSingleSectorResult<F> {
287    Scheduled(F),
288    Skipped,
289    FatalError(PlottingError),
290}
291
292struct SectorPlottingResult<'a> {
293    sector_metadata: SectorMetadataChecksummed,
294    replotting: bool,
295    last_queued: bool,
296    plotting_permit: SemaphoreGuard<'a>,
297}
298
299async fn plot_single_sector<'a, NC>(
300    sector_to_plot: SectorToPlot,
301    sector_plotting_options: &'a SectorPlottingOptions<'a, NC>,
302    sectors_metadata: &'a AsyncRwLock<Vec<SectorMetadataChecksummed>>,
303    sectors_being_modified: &'a AsyncRwLock<HashSet<SectorIndex>>,
304    plotting_semaphore: &'a Semaphore,
305    max_used_history_size: &RwLock<HistorySize>,
306) -> PlotSingleSectorResult<
307    impl Future<Output = Result<SectorPlottingResult<'a>, PlottingError>> + 'a,
308>
309where
310    NC: NodeClient,
311{
312    let SectorPlottingOptions {
313        public_key,
314        shard_commitments_roots_cache,
315        node_client,
316        pieces_in_sector,
317        sector_size,
318        plot_file,
319        metadata_file,
320        handlers,
321        global_mutex,
322        plotter,
323        metrics,
324    } = sector_plotting_options;
325
326    let SectorToPlot {
327        sector_index,
328        progress,
329        last_queued,
330        acknowledgement_sender: _acknowledgement_sender,
331    } = sector_to_plot;
332    trace!("Preparing to plot sector");
333
334    // Inform others that this sector is being modified
335    {
336        let mut sectors_being_modified = sectors_being_modified.write().await;
337        if !sectors_being_modified.insert(sector_index) {
338            debug!("Skipped sector plotting, it is already in progress");
339            return PlotSingleSectorResult::Skipped;
340        }
341    }
342
343    let plotting_permit = plotting_semaphore.acquire().await;
344
345    let maybe_old_sector_metadata = sectors_metadata
346        .read()
347        .await
348        .get(usize::from(sector_index))
349        .cloned();
350    let replotting = maybe_old_sector_metadata.is_some();
351
352    if let Some(metrics) = metrics {
353        metrics.sector_plotting.inc();
354    }
355    let sector_state = SectorUpdate::Plotting(SectorPlottingDetails::Starting {
356        progress,
357        replotting,
358        last_queued,
359    });
360    handlers
361        .sector_update
362        .call_simple(&(sector_index, sector_state));
363
364    let start = Instant::now();
365
366    // This `loop` is a workaround for an edge-case in local setup if expiration is configured to 1.
367    // In that scenario we get a replotting notification essentially straight from a block import
368    // pipeline of the node, before the block is imported. This can result in a later request for
369    // farmer app info to return old data, meaning we're replotting exactly the same sector that
370    // just expired.
371    let mut protocol_info = loop {
372        let protocol_info = match node_client.farmer_app_info().await {
373            Ok(farmer_app_info) => farmer_app_info.protocol_info,
374            Err(error) => {
375                return PlotSingleSectorResult::FatalError(PlottingError::FailedToGetFarmerInfo {
376                    error,
377                });
378            }
379        };
380
381        if let Some(old_sector_metadata) = &maybe_old_sector_metadata
382            && protocol_info.history_size <= old_sector_metadata.history_size
383        {
384            if protocol_info.min_sector_lifetime == HistorySize::ONE {
385                debug!(
386                    current_history_size = %protocol_info.history_size,
387                    old_sector_history_size = %old_sector_metadata.history_size,
388                    "Latest protocol history size is not yet newer than old sector history \
389                    size, wait for a bit and try again"
390                );
391                tokio::time::sleep(FARMER_APP_INFO_RETRY_INTERVAL).await;
392                continue;
393            }
394
395            debug!(
396                current_history_size = %protocol_info.history_size,
397                old_sector_history_size = %old_sector_metadata.history_size,
398                "Skipped sector plotting, likely redundant due to redundant archived \
399                segment notification"
400            );
401            return PlotSingleSectorResult::Skipped;
402        }
403
404        break protocol_info;
405    };
406
407    // Maintain the current max history size unless it is too old. This reduces the diversity of
408    // shard assignment, making it easier for farmers to run nodes.
409    {
410        let mut max_used_history_size = max_used_history_size.write();
411        if maybe_old_sector_metadata
412            .as_ref()
413            .map(|sector_metadata| sector_metadata.history_size)
414            == Some(*max_used_history_size)
415        {
416            *max_used_history_size = protocol_info.history_size;
417        } else {
418            // TODO: This may hypothetically result in plotting expired sectors. Add sector
419            //  expiration check in the future if it ends up being a real concern.
420            protocol_info.history_size = *max_used_history_size;
421        }
422    }
423
424    let (progress_sender, mut progress_receiver) = mpsc::channel(10);
425    let shard_commitments_root = shard_commitments_roots_cache.get(protocol_info.history_size);
426
427    // Initiate plotting
428    plotter
429        .plot_sector(
430            *public_key,
431            shard_commitments_root,
432            sector_index,
433            protocol_info,
434            *pieces_in_sector,
435            replotting,
436            progress_sender,
437        )
438        .await;
439
440    if replotting {
441        info!("Replotting sector ({progress:.2}% complete)");
442    } else {
443        info!("Plotting sector ({progress:.2}% complete)");
444    }
445
446    PlotSingleSectorResult::Scheduled(async move {
447        let plotted_sector = loop {
448            match plot_single_sector_internal(
449                sector_index,
450                *sector_size,
451                plot_file,
452                metadata_file,
453                handlers,
454                global_mutex,
455                progress_receiver,
456                metrics.as_ref(),
457            )
458            .await?
459            {
460                Ok(plotted_sector) => {
461                    break plotted_sector;
462                }
463                Err(error) => {
464                    warn!(
465                        %error,
466                        "Failed to plot sector, retrying in {PLOTTING_RETRY_DELAY:?}"
467                    );
468
469                    tokio::time::sleep(PLOTTING_RETRY_DELAY).await;
470                }
471            }
472
473            let (retry_progress_sender, retry_progress_receiver) = mpsc::channel(10);
474            progress_receiver = retry_progress_receiver;
475
476            // Initiate plotting
477            plotter
478                .plot_sector(
479                    *public_key,
480                    shard_commitments_root,
481                    sector_index,
482                    protocol_info,
483                    *pieces_in_sector,
484                    replotting,
485                    retry_progress_sender,
486                )
487                .await;
488
489            if replotting {
490                info!("Replotting sector retry");
491            } else {
492                info!("Plotting sector retry");
493            }
494        };
495
496        let maybe_old_plotted_sector = maybe_old_sector_metadata.map(|old_sector_metadata| {
497            let old_history_size = old_sector_metadata.history_size;
498
499            PlottedSector {
500                sector_id: plotted_sector.sector_id,
501                sector_index: plotted_sector.sector_index,
502                sector_metadata: old_sector_metadata,
503                piece_indexes: {
504                    let mut piece_indexes = Vec::with_capacity(usize::from(*pieces_in_sector));
505                    (PieceOffset::ZERO..)
506                        .take(usize::from(*pieces_in_sector))
507                        .map(|piece_offset| {
508                            plotted_sector.sector_id.derive_piece_index(
509                                piece_offset,
510                                old_history_size,
511                                protocol_info.max_pieces_in_sector,
512                                protocol_info.recent_segments,
513                                protocol_info.recent_history_fraction,
514                            )
515                        })
516                        .collect_into(&mut piece_indexes);
517                    piece_indexes
518                },
519            }
520        });
521
522        if replotting {
523            debug!("Sector replotted successfully");
524        } else {
525            debug!("Sector plotted successfully");
526        }
527
528        let sector_metadata = plotted_sector.sector_metadata.clone();
529
530        let time = start.elapsed();
531        if let Some(metrics) = metrics {
532            metrics.sector_plotting_time.observe(time.as_secs_f64());
533            metrics.sector_plotted.inc();
534            metrics.update_sector_state(SectorState::Plotted);
535        }
536        let sector_state = SectorUpdate::Plotting(SectorPlottingDetails::Finished {
537            plotted_sector,
538            old_plotted_sector: maybe_old_plotted_sector,
539            time,
540        });
541        handlers
542            .sector_update
543            .call_simple(&(sector_index, sector_state));
544
545        Ok(SectorPlottingResult {
546            sector_metadata,
547            replotting,
548            last_queued,
549            plotting_permit,
550        })
551    })
552}
553
554/// Outer error is used to indicate irrecoverable plotting errors, while inner result is for
555/// recoverable errors
556#[expect(clippy::too_many_arguments, reason = "Internal API")]
557async fn plot_single_sector_internal(
558    sector_index: SectorIndex,
559    sector_size: usize,
560    plot_file: &StdArc<DirectIoFileWrapper>,
561    metadata_file: &Arc<DirectIoFileWrapper>,
562    handlers: &Handlers,
563    global_mutex: &AsyncMutex<()>,
564    mut progress_receiver: mpsc::Receiver<SectorPlottingProgress>,
565    metrics: Option<&Arc<SingleDiskFarmMetrics>>,
566) -> Result<Result<PlottedSector, PlottingError>, PlottingError> {
567    // Process plotting progress notifications
568    let progress_processor_fut = async {
569        while let Some(progress) = progress_receiver.next().await {
570            match progress {
571                SectorPlottingProgress::Downloading => {
572                    if let Some(metrics) = metrics {
573                        metrics.sector_downloading.inc();
574                    }
575                    handlers.sector_update.call_simple(&(
576                        sector_index,
577                        SectorUpdate::Plotting(SectorPlottingDetails::Downloading),
578                    ));
579                }
580                SectorPlottingProgress::Downloaded(time) => {
581                    if let Some(metrics) = metrics {
582                        metrics.sector_downloading_time.observe(time.as_secs_f64());
583                        metrics.sector_downloaded.inc();
584                    }
585                    handlers.sector_update.call_simple(&(
586                        sector_index,
587                        SectorUpdate::Plotting(SectorPlottingDetails::Downloaded(time)),
588                    ));
589                }
590                SectorPlottingProgress::Encoding => {
591                    if let Some(metrics) = metrics {
592                        metrics.sector_encoding.inc();
593                    }
594                    handlers.sector_update.call_simple(&(
595                        sector_index,
596                        SectorUpdate::Plotting(SectorPlottingDetails::Encoding),
597                    ));
598                }
599                SectorPlottingProgress::Encoded(time) => {
600                    if let Some(metrics) = metrics {
601                        metrics.sector_encoding_time.observe(time.as_secs_f64());
602                        metrics.sector_encoded.inc();
603                    }
604                    handlers.sector_update.call_simple(&(
605                        sector_index,
606                        SectorUpdate::Plotting(SectorPlottingDetails::Encoded(time)),
607                    ));
608                }
609                SectorPlottingProgress::Finished {
610                    plotted_sector,
611                    time: _,
612                    sector,
613                } => {
614                    return Ok((plotted_sector, sector));
615                }
616                SectorPlottingProgress::Error { error } => {
617                    if let Some(metrics) = metrics {
618                        metrics.sector_plotting_error.inc();
619                    }
620                    handlers.sector_update.call_simple(&(
621                        sector_index,
622                        SectorUpdate::Plotting(SectorPlottingDetails::Error(error.clone())),
623                    ));
624                    return Err(error);
625                }
626            }
627        }
628
629        Err("Plotting progress stream ended before plotting finished".to_string())
630    };
631
632    let (plotted_sector, mut sector) = match progress_processor_fut.await {
633        Ok(result) => result,
634        Err(error) => {
635            return Ok(Err(PlottingError::LowLevel(error)));
636        }
637    };
638
639    {
640        // Take mutex briefly to make sure writing is allowed right now
641        global_mutex.lock().await;
642
643        if let Some(metrics) = metrics {
644            metrics.sector_writing.inc();
645        }
646        handlers.sector_update.call_simple(&(
647            sector_index,
648            SectorUpdate::Plotting(SectorPlottingDetails::Writing),
649        ));
650
651        let start = Instant::now();
652
653        {
654            let sector_write_base_offset = u64::from(sector_index) * sector_size as u64;
655            let mut total_received = 0;
656            let mut sector_write_offset = sector_write_base_offset;
657            while let Some(maybe_sector_chunk) = sector.next().await {
658                let sector_chunk = match maybe_sector_chunk {
659                    Ok(sector_chunk) => sector_chunk,
660                    Err(error) => {
661                        return Ok(Err(PlottingError::LowLevel(format!(
662                            "Sector chunk receive error: {error}"
663                        ))));
664                    }
665                };
666
667                total_received += sector_chunk.len();
668
669                if total_received > sector_size {
670                    return Ok(Err(PlottingError::LowLevel(format!(
671                        "Received too many bytes {total_received} instead of expected \
672                        {sector_size} bytes"
673                    ))));
674                }
675
676                let sector_chunk_size = sector_chunk.len() as u64;
677
678                trace!(sector_chunk_size, "Writing sector chunk to disk");
679                let write_fut = task::spawn_blocking({
680                    let plot_file = StdArc::clone(plot_file);
681
682                    move || plot_file.write_all_at(&sector_chunk, sector_write_offset)
683                });
684                write_fut.await.map_err(|error| {
685                    PlottingError::LowLevel(format!("Failed to spawn blocking tokio task: {error}"))
686                })??;
687
688                sector_write_offset += sector_chunk_size;
689            }
690            drop(sector);
691
692            if total_received != sector_size {
693                return Ok(Err(PlottingError::LowLevel(format!(
694                    "Received only {total_received} sector bytes out of {sector_size} \
695                    expected bytes"
696                ))));
697            }
698        }
699        {
700            let encoded_sector_metadata = plotted_sector.sector_metadata.encode();
701            let write_fut = task::spawn_blocking({
702                let metadata_file = Arc::clone(metadata_file);
703
704                move || {
705                    metadata_file.write_all_at(
706                        &encoded_sector_metadata,
707                        RESERVED_PLOT_METADATA
708                            + (u64::from(sector_index) * encoded_sector_metadata.len() as u64),
709                    )
710                }
711            });
712            write_fut.await.map_err(|error| {
713                PlottingError::LowLevel(format!("Failed to spawn blocking tokio task: {error}"))
714            })??;
715        }
716
717        let time = start.elapsed();
718        if let Some(metrics) = metrics {
719            metrics.sector_writing_time.observe(time.as_secs_f64());
720            metrics.sector_written.inc();
721        }
722        handlers.sector_update.call_simple(&(
723            sector_index,
724            SectorUpdate::Plotting(SectorPlottingDetails::Written(time)),
725        ));
726    }
727
728    Ok(Ok(plotted_sector))
729}
730
731pub(super) struct PlottingSchedulerOptions<NC> {
732    pub(super) public_key_hash: Blake3Hash,
733    pub(super) shard_commitments_roots_cache: ShardCommitmentsRootsCache,
734    pub(super) sectors_indices_left_to_plot: Range<SectorIndex>,
735    pub(super) target_sector_count: u16,
736    pub(super) last_archived_segment_index: SegmentIndex,
737    pub(super) min_sector_lifetime: HistorySize,
738    pub(super) node_client: NC,
739    pub(super) handlers: Arc<Handlers>,
740    pub(super) sectors_metadata: StdArc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
741    pub(super) sectors_to_plot_sender: mpsc::Sender<SectorToPlot>,
742    // Max delay between segment header being acknowledged by farmer and potentially
743    // triggering replotting
744    pub(super) new_segment_processing_delay: Duration,
745    pub(super) metrics: Option<Arc<SingleDiskFarmMetrics>>,
746}
747
748pub(super) async fn plotting_scheduler<NC>(
749    plotting_scheduler_options: PlottingSchedulerOptions<NC>,
750) -> Result<(), BackgroundTaskError>
751where
752    NC: NodeClient,
753{
754    let PlottingSchedulerOptions {
755        public_key_hash,
756        shard_commitments_roots_cache,
757        sectors_indices_left_to_plot,
758        target_sector_count,
759        last_archived_segment_index,
760        min_sector_lifetime,
761        node_client,
762        handlers,
763        sectors_metadata,
764        sectors_to_plot_sender,
765        new_segment_processing_delay,
766        metrics,
767    } = plotting_scheduler_options;
768
769    // Create a proxy channel with atomically updatable last archived segment that allows to not
770    // buffer messages from RPC subscription but also access the most recent value at any time
771    let (new_segment_index_sender, new_segment_index_receiver) =
772        watch::channel(last_archived_segment_index);
773
774    let read_new_super_segments_notifications_fut = read_new_segment_index_notifications(
775        &node_client,
776        new_segment_index_sender,
777        new_segment_processing_delay,
778    );
779
780    let send_plotting_notifications_fut = send_plotting_notifications(
781        public_key_hash,
782        &shard_commitments_roots_cache,
783        sectors_indices_left_to_plot,
784        target_sector_count,
785        min_sector_lifetime,
786        &node_client,
787        &handlers,
788        sectors_metadata,
789        new_segment_index_receiver,
790        sectors_to_plot_sender,
791        metrics.as_ref(),
792    );
793
794    select! {
795        result = read_new_super_segments_notifications_fut.fuse() => {
796            result
797        }
798        result = send_plotting_notifications_fut.fuse() => {
799            result
800        }
801    }
802}
803
804async fn read_new_segment_index_notifications<NC>(
805    node_client: &NC,
806    new_super_segments_sender: watch::Sender<SegmentIndex>,
807    new_segment_processing_delay: Duration,
808) -> Result<(), BackgroundTaskError>
809where
810    NC: NodeClient,
811{
812    info!("Subscribing to new super segments");
813
814    let mut super_segment_headers_notifications = node_client
815        .subscribe_new_super_segment_headers()
816        .await
817        .map_err(|error| PlottingError::FailedToSubscribeArchivedSegments { error })?;
818
819    while let Some(super_segment_header) = super_segment_headers_notifications.next().await {
820        debug!(?super_segment_header, "New super segment");
821
822        // There is no urgent need to rush replotting sectors immediately, and this delay allows for
823        // newly archived pieces to be both cached locally and on other farmers on the network
824        let delay = Duration::from_secs(rand::rng().random_range(
825            new_segment_processing_delay.as_secs() / 10..=new_segment_processing_delay.as_secs(),
826        ));
827        tokio::time::sleep(delay).await;
828
829        if new_super_segments_sender
830            .send(super_segment_header.max_segment_index.as_inner())
831            .is_err()
832        {
833            break;
834        }
835    }
836
837    Ok(())
838}
839
840struct SectorToReplot {
841    sector_index: SectorIndex,
842    expires_at: SegmentIndex,
843}
844
845#[expect(clippy::too_many_arguments, reason = "Internal API")]
846async fn send_plotting_notifications<NC>(
847    public_key_hash: Blake3Hash,
848    shard_commitments_roots_cache: &ShardCommitmentsRootsCache,
849    sectors_indices_left_to_plot: Range<SectorIndex>,
850    target_sector_count: u16,
851    min_sector_lifetime: HistorySize,
852    node_client: &NC,
853    handlers: &Handlers,
854    sectors_metadata: StdArc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
855    mut new_segment_index_receiver: watch::Receiver<SegmentIndex>,
856    mut sectors_to_plot_sender: mpsc::Sender<SectorToPlot>,
857    metrics: Option<&Arc<SingleDiskFarmMetrics>>,
858) -> Result<(), BackgroundTaskError>
859where
860    NC: NodeClient,
861{
862    // Finish initial plotting if some sectors were not plotted fully yet
863    for sector_index in sectors_indices_left_to_plot {
864        let (acknowledgement_sender, acknowledgement_receiver) = oneshot::channel();
865        if let Err(error) = sectors_to_plot_sender
866            .send(SectorToPlot {
867                sector_index,
868                progress: f32::from(u16::from(sector_index)) / f32::from(target_sector_count)
869                    * 100.0,
870                last_queued: u16::from(sector_index) + 1 == target_sector_count,
871                acknowledgement_sender,
872            })
873            .await
874        {
875            warn!(%error, "Failed to send sector index for initial plotting");
876            return Ok(());
877        }
878
879        // We do not care if message was sent back or sender was just dropped
880        let _: Result<(), _> = acknowledgement_receiver.await;
881    }
882
883    let mut sectors_expire_at = vec![None::<SegmentIndex>; usize::from(target_sector_count)];
884    // 10% capacity is generous and should prevent reallocation in most cases
885    let mut sectors_to_replot = Vec::with_capacity(usize::from(target_sector_count) / 10);
886
887    loop {
888        let segment_index = *new_segment_index_receiver.borrow_and_update();
889        trace!(%segment_index, "New archived segment received");
890
891        let sectors_metadata = sectors_metadata.read().await;
892        let sectors_to_check = sectors_metadata
893            .iter()
894            .map(|sector_metadata| (sector_metadata.sector_index, sector_metadata.history_size));
895        for (sector_index, history_size) in sectors_to_check {
896            if let Some(Some(expires_at)) =
897                sectors_expire_at.get(usize::from(sector_index)).copied()
898            {
899                trace!(
900                    %sector_index,
901                    %history_size,
902                    %expires_at,
903                    "Checking sector for expiration"
904                );
905                // +1 means we will start replotting a bit before it actually expires to avoid
906                // storing expired sectors
907                if expires_at <= (segment_index + SegmentIndex::ONE) {
908                    debug!(
909                        %sector_index,
910                        %history_size,
911                        %expires_at,
912                        "Sector expires soon #1, scheduling replotting"
913                    );
914
915                    let expiration_details = if expires_at <= segment_index {
916                        if let Some(metrics) = metrics {
917                            metrics.update_sector_state(SectorState::Expired);
918                        }
919                        SectorExpirationDetails::Expired
920                    } else {
921                        if let Some(metrics) = metrics {
922                            metrics.update_sector_state(SectorState::AboutToExpire);
923                        }
924                        SectorExpirationDetails::AboutToExpire
925                    };
926                    handlers
927                        .sector_update
928                        .call_simple(&(sector_index, SectorUpdate::Expiration(expiration_details)));
929
930                    // Time to replot
931                    sectors_to_replot.push(SectorToReplot {
932                        sector_index,
933                        expires_at,
934                    });
935                }
936                continue;
937            }
938
939            let shard_commitments_root = shard_commitments_roots_cache.get(history_size);
940            if let Some(expiration_check_segment_index) = history_size
941                .sector_expiration_check(min_sector_lifetime)
942                .map(|expiration_check_history_size| expiration_check_history_size.segment_index())
943            {
944                trace!(
945                    %sector_index,
946                    %history_size,
947                    %expiration_check_segment_index,
948                    "Determined sector expiration check segment index"
949                );
950                let maybe_sector_expiration_check_super_segment_root = node_client
951                    .super_segment_root_for_segment_index(expiration_check_segment_index)
952                    .await
953                    .map_err(|error| PlottingError::FailedToGetSuperSegmentRoot { error })?;
954
955                if let Some(sector_expiration_check_super_segment_root) =
956                    maybe_sector_expiration_check_super_segment_root
957                {
958                    let sector_id = SectorId::new(
959                        &public_key_hash,
960                        &shard_commitments_root,
961                        sector_index,
962                        history_size,
963                    );
964                    let expiration_history_size = sector_id
965                        .derive_expiration_history_size(
966                            history_size,
967                            &sector_expiration_check_super_segment_root,
968                            min_sector_lifetime,
969                        )
970                        .expect(
971                            "Farmers internally stores correct history size in sector \
972                            metadata; qed",
973                        );
974
975                    let expires_at = expiration_history_size.segment_index();
976
977                    trace!(
978                        %sector_index,
979                        %history_size,
980                        sector_expire_at = %expires_at,
981                        "Determined sector expiration segment index"
982                    );
983                    // +1 means we will start replotting a bit before it actually expires to avoid
984                    // storing expired sectors
985                    if expires_at <= (segment_index + SegmentIndex::ONE) {
986                        debug!(
987                            %sector_index,
988                            %history_size,
989                            %expires_at,
990                            "Sector expires soon #2, scheduling replotting"
991                        );
992
993                        let expiration_details = if expires_at <= segment_index {
994                            if let Some(metrics) = metrics {
995                                metrics.update_sector_state(SectorState::Expired);
996                            }
997                            SectorExpirationDetails::Expired
998                        } else {
999                            if let Some(metrics) = metrics {
1000                                metrics.update_sector_state(SectorState::AboutToExpire);
1001                            }
1002                            SectorExpirationDetails::AboutToExpire
1003                        };
1004                        handlers.sector_update.call_simple(&(
1005                            sector_index,
1006                            SectorUpdate::Expiration(expiration_details),
1007                        ));
1008
1009                        // Time to replot
1010                        sectors_to_replot.push(SectorToReplot {
1011                            sector_index,
1012                            expires_at,
1013                        });
1014                    } else {
1015                        trace!(
1016                            %sector_index,
1017                            %history_size,
1018                            sector_expire_at = %expires_at,
1019                            "Sector expires later, remembering sector expiration"
1020                        );
1021
1022                        handlers.sector_update.call_simple(&(
1023                            sector_index,
1024                            SectorUpdate::Expiration(SectorExpirationDetails::Determined {
1025                                expires_at,
1026                            }),
1027                        ));
1028
1029                        // Store expiration so we don't have to recalculate it later
1030                        if let Some(expires_at_entry) =
1031                            sectors_expire_at.get_mut(usize::from(sector_index))
1032                        {
1033                            expires_at_entry.replace(expires_at);
1034                        }
1035                    }
1036                }
1037            }
1038        }
1039        drop(sectors_metadata);
1040
1041        let sectors_queued = sectors_to_replot.len();
1042        sectors_to_replot.sort_by_key(|sector_to_replot| sector_to_replot.expires_at);
1043        for (
1044            index,
1045            SectorToReplot {
1046                sector_index,
1047                expires_at: _,
1048            },
1049        ) in sectors_to_replot.drain(..).enumerate()
1050        {
1051            let (acknowledgement_sender, acknowledgement_receiver) = oneshot::channel();
1052            if let Err(error) = sectors_to_plot_sender
1053                .send(SectorToPlot {
1054                    sector_index,
1055                    progress: index as f32 / sectors_queued as f32 * 100.0,
1056                    last_queued: index + 1 == sectors_queued,
1057                    acknowledgement_sender,
1058                })
1059                .await
1060            {
1061                warn!(%error, "Failed to send sector index for replotting");
1062                return Ok(());
1063            }
1064
1065            // We do not care if message was sent back or sender was just dropped
1066            let _: Result<(), _> = acknowledgement_receiver.await;
1067
1068            if let Some(expires_at_entry) = sectors_expire_at.get_mut(usize::from(sector_index)) {
1069                expires_at_entry.take();
1070            }
1071        }
1072
1073        if new_segment_index_receiver.changed().await.is_err() {
1074            break;
1075        }
1076    }
1077
1078    Ok(())
1079}