Skip to main content

ab_client_archiving/
task.rs

1//! Segment archiving task.
2//!
3//! Implements archiving process in that converts blockchain history (blocks) into archived history
4//! (segments and pieces).
5//!
6//! The main entry point here is [`create_segment_archiver_task`] that will create a task, which
7//! while driven will perform the archiving itself.
8//!
9//! Archiving itself will also wait for acknowledgement by various subscribers before proceeding,
10//! which includes farmer subscription, in case of reference implementation via RPC.
11//!
12//! All segment headers of the archived segments are available to other parts of the protocol that
13//! need to know what the correct archival history of the blockchain looks like through
14//! [`ChainInfo`]. For example, it is used during node sync and farmer plotting to verify pieces of
15//! archival history received from other network participants. Future segment header might also be
16//! already known in the case of Snap sync.
17//!
18//! [`encode_block`] and [`decode_block`] are symmetric encoding/decoding functions turning
19//! Blocks into bytes and back.
20
21#[cfg(test)]
22mod tests;
23
24use ab_aligned_buffer::SharedAlignedBuffer;
25use ab_archiving::archiver::{Archiver, ArchiverInstantiationError, NewArchivedSegment};
26use ab_client_api::{ChainInfo, ChainInfoWrite, PersistSegmentHeadersError, ReadBlockError};
27use ab_client_consensus_common::{BlockImportingNotification, ConsensusConstants};
28use ab_core_primitives::block::body::owned::GenericOwnedBlockBody;
29use ab_core_primitives::block::header::GenericBlockHeader;
30use ab_core_primitives::block::header::owned::GenericOwnedBlockHeader;
31use ab_core_primitives::block::owned::GenericOwnedBlock;
32use ab_core_primitives::block::{BlockNumber, BlockRoot, GenericBlock};
33use ab_core_primitives::segments::{LocalSegmentIndex, RecordedHistorySegment, SegmentHeader};
34use ab_core_primitives::shard::RealShardKind;
35use ab_erasure_coding::ErasureCoding;
36use bytesize::ByteSize;
37use chacha20::ChaCha8Rng;
38use chacha20::rand_core::{Rng, SeedableRng};
39use futures::channel::mpsc;
40use futures::prelude::*;
41use std::time::Duration;
42use tracing::{debug, info, trace, warn};
43use triomphe::Arc;
44
45/// Do not wait for acknowledgements beyond this time limit
46const ACKNOWLEDGEMENT_TIMEOUT: Duration = Duration::from_mins(2);
47
48// TODO: Maybe use or remove if database handles this completely on its own
49// /// How deep (in segments) should block be in order to be finalized.
50// ///
51// /// This is required for full nodes to not prune recent history such that keep-up sync in
52// /// Substrate works even without archival nodes (initial sync will be done from DSN).
53// ///
54// /// Ideally, we'd decouple pruning from finalization, but it may require invasive changes in
55// /// Substrate and is not worth it right now.
56// /// https://github.com/paritytech/substrate/discussions/14359
57// const FINALIZATION_DEPTH_IN_SEGMENTS: SegmentIndex = SegmentIndex::from(5);
58
59/// Notification with a new archived segment that was just archived
60#[derive(Debug)]
61pub struct ArchivedSegmentNotification {
62    /// Archived segment.
63    pub archived_segment: Arc<NewArchivedSegment>,
64    /// Sender that signified the fact of receiving an archived segment by farmer.
65    ///
66    /// This must be used to send a message, or else the block import pipeline will get stuck.
67    pub acknowledgement_sender: mpsc::Sender<()>,
68}
69
70async fn find_last_archived_block<Block, CI>(
71    chain_info: &CI,
72    best_block_number_to_archive: BlockNumber,
73    best_block_root: &BlockRoot,
74) -> Option<(SegmentHeader, Block)>
75where
76    Block: GenericOwnedBlock,
77    CI: ChainInfo<Block>,
78{
79    let max_local_segment_index = chain_info.last_segment_header()?.index.as_inner();
80
81    for segment_header in (LocalSegmentIndex::ZERO..=max_local_segment_index)
82        .rev()
83        .filter_map(|segment_index| chain_info.get_segment_header(segment_index))
84    {
85        let last_archived_block_number = segment_header.last_archived_block.number();
86
87        if last_archived_block_number > best_block_number_to_archive {
88            // Last archived block in segment header is too high for the current state of the chain
89            // (segment headers store may know about more blocks in existence than is currently
90            // imported)
91            continue;
92        }
93
94        let Some(last_archived_block_header) =
95            chain_info.ancestor_header(last_archived_block_number, best_block_root)
96        else {
97            // This block number is not in our chain yet (segment headers store may know about more
98            // blocks in existence than is currently imported)
99            continue;
100        };
101
102        let last_archived_block_root = &*last_archived_block_header.header().root();
103
104        let Ok(last_archived_block) = chain_info.block(last_archived_block_root).await else {
105            // Block might have been pruned between ancestor search and disk read
106            continue;
107        };
108
109        return Some((segment_header, last_archived_block));
110    }
111
112    None
113}
114
115/// Encode block for archiving purposes
116pub fn encode_block<Block>(block: &Block) -> Vec<u8>
117where
118    Block: GenericOwnedBlock,
119{
120    let is_beacon_chain_genesis_block = Block::Block::SHARD_KIND == RealShardKind::BeaconChain
121        && block.header().header().prefix.number == BlockNumber::ZERO;
122    let header_buffer = block.header().buffer();
123    let body_buffer = block.body().buffer();
124
125    // TODO: Extra allocation here is unfortunate, would be nice to avoid it
126    let mut encoded_block = Vec::with_capacity(if is_beacon_chain_genesis_block {
127        RecordedHistorySegment::SIZE
128    } else {
129        size_of::<u32>() * 2 + header_buffer.len() as usize + body_buffer.len() as usize
130    });
131
132    encoded_block.extend_from_slice(&header_buffer.len().to_le_bytes());
133    encoded_block.extend_from_slice(&body_buffer.len().to_le_bytes());
134    encoded_block.extend_from_slice(header_buffer);
135    encoded_block.extend_from_slice(body_buffer);
136
137    let encoded_block_length = encoded_block.len();
138    if is_beacon_chain_genesis_block && encoded_block_length < RecordedHistorySegment::SIZE {
139        // Encoding of the genesis block is extended with extra data such that the very first
140        // archived segment can be produced right away, bootstrapping the farming process.
141        //
142        // Note: we add it to the end of the encoded block, so during decoding it'll actually be
143        // ignored even though it is technically present in encoded form.
144        encoded_block.resize(RecordedHistorySegment::SIZE, 0);
145        let mut rng = ChaCha8Rng::from_seed(*block.header().header().result.state_root);
146        rng.fill_bytes(&mut encoded_block[encoded_block_length..]);
147    }
148
149    encoded_block
150}
151
152/// Symmetrical to [`encode_block()`], used to decode previously encoded blocks
153pub fn decode_block<Block>(mut encoded_block: &[u8]) -> Option<Block>
154where
155    Block: GenericOwnedBlock,
156{
157    let header_length = {
158        let header_length = encoded_block.split_off(..size_of::<u32>())?;
159        u32::from_le_bytes([
160            header_length[0],
161            header_length[1],
162            header_length[2],
163            header_length[3],
164        ])
165    };
166    let body_length = {
167        let body_length = encoded_block.split_off(..size_of::<u32>())?;
168        u32::from_le_bytes([
169            body_length[0],
170            body_length[1],
171            body_length[2],
172            body_length[3],
173        ])
174    };
175
176    let header_buffer = encoded_block.split_off(..header_length as usize)?;
177    let body_buffer = encoded_block.split_off(..body_length as usize)?;
178
179    let header_buffer = SharedAlignedBuffer::from_bytes(header_buffer);
180    let body_buffer = SharedAlignedBuffer::from_bytes(body_buffer);
181
182    Block::from_buffers(header_buffer, body_buffer)
183}
184
185/// Segment archiver task error
186#[derive(Debug, thiserror::Error)]
187pub enum SegmentArchiverTaskError {
188    /// Archiver instantiation error
189    #[error("Archiver instantiation error: {error}")]
190    Instantiation {
191        /// Low-level error
192        #[from]
193        error: ArchiverInstantiationError,
194    },
195    /// Failed to persist a new segment header
196    #[error("Failed to persist a new segment header: {error}")]
197    PersistSegmentHeaders {
198        /// Low-level error
199        #[from]
200        error: PersistSegmentHeadersError,
201    },
202    /// Block that must be archived is missing in the chain
203    #[error("Block {block_number} that must be archived is missing in the chain")]
204    BlockMissing {
205        /// Block number
206        block_number: BlockNumber,
207    },
208    /// Failed to read a block that must be archived
209    #[error("Failed to read block {block_number} that must be archived: {error}")]
210    ReadBlock {
211        /// Block number
212        block_number: BlockNumber,
213        /// Low-level error
214        error: ReadBlockError,
215    },
216    /// Encoded block is too large to archive
217    #[error("Encoded block {block_number} is too large to archive: {size} bytes")]
218    BlockTooLarge {
219        /// Block number
220        block_number: BlockNumber,
221        /// Encoded block size in bytes
222        size: usize,
223    },
224    /// Attempt to switch to a different fork beyond archiving depth
225    #[error(
226        "Attempt to switch to a different fork beyond archiving depth: parent block root \
227        {parent_block_root}, best archived block root {best_archived_block_root}"
228    )]
229    ArchivingReorg {
230        /// Parent block root
231        parent_block_root: BlockRoot,
232        /// Best archived block root
233        best_archived_block_root: BlockRoot,
234    },
235    /// Re-initialization left a gap before the block to archive
236    #[error(
237        "Re-initialization left a gap before the block to archive (best archived block number \
238        {best_archived_block_number}, block number to archive {block_number_to_archive}, block \
239        about to be imported {importing_block_number})"
240    )]
241    BlockGap {
242        /// Best archived block number
243        best_archived_block_number: BlockNumber,
244        /// Block number to archive
245        block_number_to_archive: BlockNumber,
246        /// Importing block number
247        importing_block_number: BlockNumber,
248    },
249}
250
251struct InitializedArchiver {
252    archiver: Archiver,
253    best_archived_block: (BlockRoot, BlockNumber),
254}
255
256async fn initialize_archiver<Block, CI>(
257    chain_info: &CI,
258    block_confirmation_depth: BlockNumber,
259    erasure_coding: ErasureCoding,
260) -> Result<InitializedArchiver, SegmentArchiverTaskError>
261where
262    Block: GenericOwnedBlock,
263    CI: ChainInfoWrite<Block>,
264{
265    let best_block_header = chain_info.best_header();
266    let best_block_root = *best_block_header.header().root();
267    let best_block_number: BlockNumber = best_block_header.header().prefix.number;
268
269    let mut best_block_to_archive = best_block_number.saturating_sub(block_confirmation_depth);
270
271    if (best_block_to_archive..best_block_number).any(|block_number| {
272        chain_info
273            .ancestor_header(block_number, &best_block_root)
274            .is_none()
275    }) {
276        // If there are blocks missing headers between best block to archive and best block of the
277        // blockchain it means newer block was inserted by Snap sync and as such is by definition
278        // valid, so we can simply assume that is our best block to archive instead
279        best_block_to_archive = best_block_number;
280    }
281
282    let maybe_last_archived_block =
283        find_last_archived_block(chain_info, best_block_to_archive, &best_block_root).await;
284
285    let have_last_segment_header = maybe_last_archived_block.is_some();
286    let mut best_archived_block = None::<(BlockRoot, BlockNumber)>;
287
288    let mut archiver =
289        if let Some((last_segment_header, last_archived_block)) = maybe_last_archived_block {
290            // Continuing from existing initial state
291            let last_archived_block_number = last_segment_header.last_archived_block.number;
292            info!(
293                %last_archived_block_number,
294                "Resuming archiver from last archived block",
295            );
296
297            let last_archived_block_header = last_archived_block.header().header();
298            // Set initial value, this is needed in case only genesis block was archived and there
299            // is nothing else available
300            best_archived_block.replace((
301                *last_archived_block_header.root(),
302                last_archived_block_header.prefix.number,
303            ));
304
305            let last_archived_block_encoded = encode_block(&last_archived_block);
306
307            Archiver::with_initial_state(
308                best_block_header.header().prefix.shard_index,
309                erasure_coding,
310                last_segment_header,
311                &last_archived_block_encoded,
312                Vec::new(),
313            )?
314        } else {
315            info!("Starting archiving from genesis");
316
317            Archiver::new(
318                best_block_header.header().prefix.shard_index,
319                erasure_coding,
320            )
321        };
322
323    // Process blocks since last fully archived block up to
324    // `best_block_number - block_confirmation_depth`
325    {
326        let blocks_to_archive_from = archiver
327            .last_archived_block_number()
328            .map_or_default(|n| n + BlockNumber::ONE);
329        let blocks_to_archive_to = best_block_number
330            .checked_sub(block_confirmation_depth)
331            .filter(|&blocks_to_archive_to| blocks_to_archive_to >= blocks_to_archive_from)
332            .or({
333                if have_last_segment_header {
334                    None
335                } else {
336                    // If not continuation, archive genesis block
337                    Some(BlockNumber::ZERO)
338                }
339            });
340
341        if let Some(blocks_to_archive_to) = blocks_to_archive_to {
342            info!(
343                "Archiving already produced blocks {}..={}",
344                blocks_to_archive_from, blocks_to_archive_to,
345            );
346
347            for block_number_to_archive in blocks_to_archive_from..=blocks_to_archive_to {
348                // Blocks are missing if segment headers don't match the chain, for example when
349                // Snap sync didn't store them first (see `create_segment_archiver_task()`)
350                let header = chain_info
351                    .ancestor_header(block_number_to_archive, &best_block_root)
352                    .ok_or(SegmentArchiverTaskError::BlockMissing {
353                        block_number: block_number_to_archive,
354                    })?;
355
356                let block = chain_info
357                    .block(&header.header().root())
358                    .await
359                    .map_err(|error| SegmentArchiverTaskError::ReadBlock {
360                        block_number: block_number_to_archive,
361                        error,
362                    })?;
363
364                let encoded_block = encode_block(&block);
365
366                debug!(
367                    "Encoded block {} has size of {}",
368                    block_number_to_archive,
369                    ByteSize::b(encoded_block.len() as u64).display().iec(),
370                );
371
372                let encoded_block_size = encoded_block.len();
373                let block_outcome = archiver.add_block(encoded_block, Vec::new()).ok_or(
374                    SegmentArchiverTaskError::BlockTooLarge {
375                        block_number: block_number_to_archive,
376                        size: encoded_block_size,
377                    },
378                )?;
379                let new_segment_headers: Vec<SegmentHeader> = block_outcome
380                    .archived_segments
381                    .iter()
382                    .map(|archived_segment| archived_segment.segment_header)
383                    .collect();
384
385                if !new_segment_headers.is_empty() {
386                    chain_info
387                        .persist_segment_headers(new_segment_headers)
388                        .await?;
389                }
390
391                if block_number_to_archive == blocks_to_archive_to {
392                    best_archived_block.replace((*header.header().root(), block_number_to_archive));
393                }
394            }
395        }
396    }
397
398    Ok(InitializedArchiver {
399        archiver,
400        best_archived_block: best_archived_block
401            .expect("Must always set if there is no logical error; qed"),
402    })
403}
404
405// TODO: Public API for re-archiving of blocks with ability to produce object mappings
406/// Create a segment archiver task.
407///
408/// Archiver task will listen for importing blocks and archive confirmed blocks, producing pieces
409/// and segment headers. It produces local segments initially, which after confirmation by the
410/// beacon chain will be updated with the necessary proof and given a global segment index.
411///
412/// NOTE: Archiver is doing blocking operations and must run in a dedicated task.
413///
414/// Archiver is only able to move forward and doesn't support reorgs. Upon restart, it will check
415/// segments in [`ChainInfo`] and chain history to reconstruct the "current" state it was in before
416/// the last shutdown and continue incrementally archiving blockchain history from there.
417///
418/// Archiving is triggered by block importing notification (`block_importing_notification_receiver`)
419/// and tries to archive the block that the block being imported confirms (see
420/// [`ConsensusConstants::block_confirmation_depth`]). Block importing will then wait for archiver
421/// to acknowledge processing, which is necessary for ensuring that when a child of the block being
422/// imported is imported, the newly archived segment is already available deterministically.
423///
424/// Once a new segment is archived, a notification (`archived_segment_notification_sender`) will be
425/// sent and archiver will be paused until all receivers have provided an acknowledgement for it (or
426/// a very generous timeout has passed). After a restart, a segment that was already announced can
427/// be announced again: blocks that the database didn't write to disk yet (above its
428/// `soft_confirmation_depth`) are lost and imported again, and the blocks they confirm are archived
429/// again.
430///
431/// Snap sync must store all known segment headers before any block is imported, and a block that it
432/// inserts directly, without its ancestors, must be the last archived block of one of those
433/// segments. Re-initialization then resumes archiving from the latest of those segments whose last
434/// archived block is in the chain.
435pub async fn create_segment_archiver_task<Block, CI>(
436    chain_info: CI,
437    mut block_importing_notification_receiver: mpsc::Receiver<BlockImportingNotification>,
438    mut archived_segment_notification_sender: mpsc::Sender<ArchivedSegmentNotification>,
439    consensus_constants: ConsensusConstants,
440    erasure_coding: ErasureCoding,
441) -> Result<
442    impl Future<Output = Result<(), SegmentArchiverTaskError>> + Send + 'static,
443    SegmentArchiverTaskError,
444>
445where
446    Block: GenericOwnedBlock,
447    CI: ChainInfoWrite<Block> + 'static,
448{
449    let maybe_archiver = if chain_info.last_segment_header().is_none() {
450        let initialize_archiver_fut = initialize_archiver(
451            &chain_info,
452            consensus_constants.block_confirmation_depth,
453            erasure_coding.clone(),
454        );
455        Some(initialize_archiver_fut.await?)
456    } else {
457        None
458    };
459
460    Ok(async move {
461        let archiver = if let Some(archiver) = maybe_archiver {
462            archiver
463        } else {
464            let initialize_archiver_fut = initialize_archiver(
465                &chain_info,
466                consensus_constants.block_confirmation_depth,
467                erasure_coding.clone(),
468            );
469            initialize_archiver_fut.await?
470        };
471
472        let InitializedArchiver {
473            mut archiver,
474            best_archived_block,
475        } = archiver;
476        let (mut best_archived_block_root, mut best_archived_block_number) = best_archived_block;
477
478        while let Some(block_importing_notification) =
479            block_importing_notification_receiver.next().await
480        {
481            let importing_block_number = block_importing_notification.block_number;
482            let Some(block_number_to_archive) =
483                importing_block_number.checked_sub(consensus_constants.block_confirmation_depth)
484            else {
485                // Too early to archive blocks
486                continue;
487            };
488
489            // There is no segment header yet when the genesis block doesn't fill a segment, which
490            // is the case on intermediate and leaf shards
491            let maybe_last_archived_block_number = chain_info
492                .last_segment_header()
493                .map(|last_segment_header| last_segment_header.last_archived_block.number());
494            trace!(
495                %importing_block_number,
496                %block_number_to_archive,
497                %best_archived_block_number,
498                last_archived_block_number = ?maybe_last_archived_block_number,
499                "Checking if block needs to be skipped"
500            );
501
502            // Skip archived blocks
503            let skip_last_archived_blocks =
504                maybe_last_archived_block_number.is_some_and(|last_archived_block_number| {
505                    last_archived_block_number > block_number_to_archive
506                });
507            if best_archived_block_number >= block_number_to_archive || skip_last_archived_blocks {
508                // This block was already archived, skip
509                debug!(
510                    %importing_block_number,
511                    %block_number_to_archive,
512                    %best_archived_block_number,
513                    last_archived_block_number = ?maybe_last_archived_block_number,
514                    "Skipping already archived block",
515                );
516                continue;
517            }
518
519            let best_block_root = chain_info.best_root();
520
521            // The best archived block is behind the block to archive when Snap sync inserted a
522            // block directly, or when blocks below the last archived block of the last stored
523            // segment were skipped above (after a restart that lost blocks, for example).
524            // Re-initialize the archiver from stored segments and the chain (see
525            // `create_segment_archiver_task()`).
526            if best_archived_block_number + BlockNumber::ONE != block_number_to_archive {
527                let initialize_archiver_fut = initialize_archiver(
528                    &chain_info,
529                    consensus_constants.block_confirmation_depth,
530                    erasure_coding.clone(),
531                );
532                InitializedArchiver {
533                    archiver,
534                    best_archived_block: (best_archived_block_root, best_archived_block_number),
535                } = initialize_archiver_fut.await?;
536
537                if best_archived_block_number + BlockNumber::ONE == block_number_to_archive {
538                    // As expected, can archive this block
539                } else if best_archived_block_number >= block_number_to_archive {
540                    // This block was already archived, skip
541                    continue;
542                } else {
543                    // Re-initialization leaves the best archived block at
544                    // `best_block_number - block_confirmation_depth` or higher, and the best block
545                    // it reads is the parent of the block being imported, so there can't be a gap
546                    // here unless there is a logic error elsewhere. A block that Snap sync inserts
547                    // directly is found archived, since its segment header is stored first.
548                    return Err(SegmentArchiverTaskError::BlockGap {
549                        best_archived_block_number,
550                        block_number_to_archive,
551                        importing_block_number,
552                    });
553                }
554            }
555
556            (best_archived_block_root, best_archived_block_number) = archive_block(
557                &mut archiver,
558                &chain_info,
559                &mut archived_segment_notification_sender,
560                best_archived_block_root,
561                block_number_to_archive,
562                &best_block_root,
563            )
564            .await?;
565        }
566
567        Ok(())
568    })
569}
570
571/// Tries to archive `block_number` and returns new (or old if not changed) best archived block
572async fn archive_block<Block, CI>(
573    archiver: &mut Archiver,
574    chain_info: &CI,
575    archived_segment_notification_sender: &mut mpsc::Sender<ArchivedSegmentNotification>,
576    best_archived_block_root: BlockRoot,
577    block_number_to_archive: BlockNumber,
578    best_block_root: &BlockRoot,
579) -> Result<(BlockRoot, BlockNumber), SegmentArchiverTaskError>
580where
581    Block: GenericOwnedBlock,
582    CI: ChainInfoWrite<Block>,
583{
584    let header = chain_info
585        .ancestor_header(block_number_to_archive, best_block_root)
586        .ok_or(SegmentArchiverTaskError::BlockMissing {
587            block_number: block_number_to_archive,
588        })?;
589
590    let parent_block_root = header.header().prefix.parent_root;
591    if parent_block_root != best_archived_block_root {
592        return Err(SegmentArchiverTaskError::ArchivingReorg {
593            parent_block_root,
594            best_archived_block_root,
595        });
596    }
597
598    let block_root_to_archive = *header.header().root();
599
600    let block = chain_info
601        .block(&block_root_to_archive)
602        .await
603        .map_err(|error| SegmentArchiverTaskError::ReadBlock {
604            block_number: block_number_to_archive,
605            error,
606        })?;
607
608    debug!("Archiving block {block_number_to_archive} ({block_root_to_archive})");
609
610    let encoded_block = encode_block(&block);
611    debug!(
612        "Encoded block {block_number_to_archive} has size of {}",
613        ByteSize::b(encoded_block.len() as u64).display().iec(),
614    );
615
616    let encoded_block_size = encoded_block.len();
617    let block_outcome = archiver.add_block(encoded_block, Vec::new()).ok_or(
618        SegmentArchiverTaskError::BlockTooLarge {
619            block_number: block_number_to_archive,
620            size: encoded_block_size,
621        },
622    )?;
623    for archived_segment in block_outcome.archived_segments {
624        let segment_header = archived_segment.segment_header;
625
626        chain_info
627            .persist_segment_headers(vec![segment_header])
628            .await?;
629
630        send_archived_segment_notification(archived_segment_notification_sender, archived_segment)
631            .await;
632    }
633
634    Ok((block_root_to_archive, block_number_to_archive))
635}
636
637async fn send_archived_segment_notification(
638    archived_segment_notification_sender: &mut mpsc::Sender<ArchivedSegmentNotification>,
639    archived_segment: NewArchivedSegment,
640) {
641    let segment_index = archived_segment.segment_header.index;
642    let (acknowledgement_sender, mut acknowledgement_receiver) = mpsc::channel(1);
643    // Keep `archived_segment` around until all acknowledgements are received since some receivers
644    // might use weak references
645    let archived_segment = Arc::new(archived_segment);
646    let archived_segment_notification = ArchivedSegmentNotification {
647        archived_segment: Arc::clone(&archived_segment),
648        acknowledgement_sender,
649    };
650
651    if let Err(error) = archived_segment_notification_sender
652        .send(archived_segment_notification)
653        .await
654    {
655        warn!(
656            %error,
657            "Failed to send archived segment notification"
658        );
659    }
660
661    let wait_fut = async {
662        while acknowledgement_receiver.next().await.is_some() {
663            debug!(
664                "Archived segment notification acknowledged: {}",
665                segment_index
666            );
667        }
668    };
669
670    if tokio::time::timeout(ACKNOWLEDGEMENT_TIMEOUT, wait_fut)
671        .await
672        .is_err()
673    {
674        warn!(
675            "Archived segment notification was not acknowledged and reached timeout, continue \
676            regardless"
677        );
678    }
679}