iota_data_ingestion_core/history/
reader.rs1use std::{num::NonZeroUsize, ops::Range, sync::Arc, time::Duration};
6
7use bytes::{Buf, Bytes, buf::Reader};
8use futures::{Stream, StreamExt, TryStreamExt};
9use iota_config::object_storage_config::ObjectStoreConfig;
10use iota_storage::{
11 compute_sha3_checksum_for_bytes, make_iterator,
12 object_store::{ObjectStoreGetExt, http::HttpDownloaderBuilder, util::get},
13};
14use iota_types::{
15 full_checkpoint_content::CheckpointData, messages_checkpoint::CheckpointSequenceNumber,
16};
17use object_store::path::Path;
18use tokio::sync::{
19 Mutex,
20 oneshot::{self, Sender},
21};
22use tracing::info;
23
24use crate::{
25 IngestionError,
26 errors::IngestionResult as Result,
27 history::{
28 CHECKPOINT_FILE_MAGIC,
29 epoch_boundaries::{EpochBoundaries, read_epoch_boundaries},
30 manifest::{FileMetadata, Manifest, read_manifest},
31 },
32};
33
34#[derive(Clone)]
35pub struct HistoricalReader {
36 concurrency: usize,
37 #[expect(dead_code)]
38 sender: Arc<Sender<()>>,
42 manifest: Arc<Mutex<Manifest>>,
43 remote_object_store: Arc<dyn ObjectStoreGetExt>,
44}
45
46#[derive(Debug, Clone)]
47pub struct HistoricalReaderConfig {
48 pub remote_store_config: ObjectStoreConfig,
49 pub download_concurrency: NonZeroUsize,
50}
51
52impl HistoricalReader {
53 pub fn new(config: HistoricalReaderConfig) -> Result<Self> {
54 let remote_object_store = if config.remote_store_config.no_sign_request {
55 config.remote_store_config.make_http()?
56 } else {
57 config.remote_store_config.make().map(Arc::new)?
58 };
59 let (sender, recv) = oneshot::channel();
60 let manifest = Arc::new(Mutex::new(Manifest::new(0)));
61 Self::spawn_manifest_sync_task(remote_object_store.clone(), manifest.clone(), recv);
63 Ok(Self {
64 manifest,
65 sender: Arc::new(sender),
66 remote_object_store,
67 concurrency: config.download_concurrency.get(),
68 })
69 }
70
71 pub fn verify_and_get_manifest_files(&self, manifest: Manifest) -> Result<Vec<FileMetadata>> {
83 let mut files = manifest.to_files();
84 if files.is_empty() {
85 return Err(IngestionError::HistoryRead(
86 "unexpected empty remote store of historical data".to_string(),
87 ));
88 }
89
90 files.sort_by_key(|f| f.checkpoint_seq_range.start);
91
92 let covers_every_checkpoint = files[0].checkpoint_seq_range.start == 0
93 && files
94 .windows(2)
95 .all(|w| w[1].checkpoint_seq_range.start == w[0].checkpoint_seq_range.end)
96 && files.last().map(|f| f.checkpoint_seq_range.end)
97 == Some(manifest.next_checkpoint_seq_num());
98 if !covers_every_checkpoint {
99 return Err(IngestionError::HistoryRead(
100 "manifest files do not cover every checkpoint from 0".to_string(),
101 ));
102 }
103
104 Ok(files)
105 }
106
107 pub async fn verify_file_consistency(&self, files: Vec<FileMetadata>) -> Result<()> {
110 let remote_object_store = self.remote_object_store.clone();
111 futures::stream::iter(files.iter())
112 .map(|metadata| {
113 let remote_object_store = remote_object_store.clone();
114 async move {
115 let checkpoint_data = get(&remote_object_store, &metadata.file_path()).await?;
116 Ok::<(Bytes, &FileMetadata), IngestionError>((checkpoint_data, metadata))
117 }
118 })
119 .boxed()
120 .buffer_unordered(self.concurrency)
121 .try_for_each(|(checkpoint_data, metadata)| {
122 let checksum = compute_sha3_checksum_for_bytes(checkpoint_data).map_err(Into::into);
123 let result = checksum.and_then(|checksum| {
124 if checksum == metadata.sha3_digest {
125 return Ok(());
126 };
127 Err(IngestionError::HistoryRead(format!(
128 "checksum doesn't match for file: {:?}",
129 metadata.file_path()
130 )))
131 });
132 futures::future::ready(result)
133 })
134 .await
135 }
136
137 pub async fn stream_blobs_for_range(
167 &self,
168 checkpoint_range: Range<CheckpointSequenceNumber>,
169 ) -> Result<impl Stream<Item = Result<Bytes>> + Send + use<'_>> {
170 let files = self.get_files_for_range(checkpoint_range).await?;
171 Ok(futures::stream::iter(files)
172 .map(move |metadata| async move {
173 let remote_object_store = Arc::clone(&self.remote_object_store);
174 let file_path = metadata.file_path();
175 Ok(get(&remote_object_store, &file_path).await?)
176 })
177 .buffered(self.concurrency))
178 }
179
180 pub async fn iter_for_range(
191 &self,
192 checkpoint_range: Range<CheckpointSequenceNumber>,
193 ) -> Result<impl Iterator<Item = CheckpointData>> {
194 let blobs = self
195 .stream_blobs_for_range(checkpoint_range.clone())
196 .await?
197 .try_collect::<Vec<_>>()
198 .await?;
199 let data_iterators = blobs
200 .into_iter()
201 .map(|blob| {
202 let range = checkpoint_range.clone();
203 make_blob_iterator_for_range(blob, range)
204 })
205 .collect::<Result<Vec<_>>>()?;
206 Ok(data_iterators.into_iter().flatten())
207 }
208
209 pub async fn iter_for_file(
222 &self,
223 file_path: Path,
224 ) -> Result<impl Iterator<Item = CheckpointData>> {
225 let raw_data_batch = get(&self.remote_object_store, &file_path).await?;
226 make_blob_iterator(raw_data_batch)
227 }
228
229 pub async fn latest_available_checkpoint(&self) -> Result<CheckpointSequenceNumber> {
231 self.manifest
232 .lock()
233 .await
234 .next_checkpoint_seq_num()
235 .checked_sub(1)
236 .ok_or_else(|| {
237 IngestionError::HistoryRead("no checkpoint data in the remote store".into())
238 })
239 }
240
241 pub fn remote_store_identifier(&self) -> String {
242 self.remote_object_store.to_string()
243 }
244
245 pub async fn epoch_boundaries(&self) -> Result<EpochBoundaries> {
255 read_epoch_boundaries(self.remote_object_store.clone()).await
256 }
257
258 pub async fn sync_manifest_once(&self) -> Result<()> {
260 Self::sync_manifest(self.remote_object_store.clone(), self.manifest.clone()).await?;
261 Ok(())
262 }
263
264 pub async fn get_manifest(&self) -> Manifest {
265 self.manifest.lock().await.clone()
266 }
267
268 async fn sync_manifest(
270 remote_store: Arc<dyn ObjectStoreGetExt>,
271 manifest: Arc<Mutex<Manifest>>,
272 ) -> Result<()> {
273 let new_manifest = read_manifest(remote_store.clone()).await?;
274 let mut locked = manifest.lock().await;
275 *locked = new_manifest;
276 Ok(())
277 }
278
279 async fn get_files_for_range(
290 &self,
291 checkpoint_range: Range<CheckpointSequenceNumber>,
292 ) -> Result<impl Iterator<Item = FileMetadata>> {
293 let manifest = self.get_manifest().await;
294
295 let latest_available_checkpoint = manifest
296 .next_checkpoint_seq_num()
297 .checked_sub(1)
298 .ok_or_else(|| {
299 IngestionError::HistoryRead("no checkpoint data in the remote store".into())
300 })?;
301
302 if checkpoint_range.start > latest_available_checkpoint {
303 return Err(IngestionError::HistoryRead(format!(
304 "latest available checkpoint is: {latest_available_checkpoint}",
305 )));
306 }
307
308 let files = self.verify_and_get_manifest_files(manifest)?;
309
310 let start_index = match files
311 .binary_search_by_key(&checkpoint_range.start, |s| s.checkpoint_seq_range.start)
312 {
313 Ok(index) => index,
314 Err(index) => index - 1,
315 };
316
317 let end_index = match files
318 .binary_search_by_key(&checkpoint_range.end, |s| s.checkpoint_seq_range.start)
319 {
320 Ok(index) => index,
321 Err(index) => index,
322 };
323
324 Ok(files
325 .into_iter()
326 .enumerate()
327 .filter_map(move |(index, metadata)| {
328 (index >= start_index && index < end_index).then_some(metadata)
329 }))
330 }
331
332 fn spawn_manifest_sync_task(
333 remote_store: Arc<dyn ObjectStoreGetExt>,
334 manifest: Arc<Mutex<Manifest>>,
335 mut recv: oneshot::Receiver<()>,
336 ) {
337 tokio::task::spawn(async move {
338 let mut interval = tokio::time::interval(Duration::from_secs(60));
339 loop {
340 tokio::select! {
341 _ = interval.tick() => {
342 Self::sync_manifest(remote_store.clone(), manifest.clone()).await?;
343 }
344 _ = &mut recv => break,
345 }
346 }
347 info!("terminating the manifest sync loop");
348 Ok::<(), IngestionError>(())
349 });
350 }
351}
352
353fn make_blob_iterator(blob: Bytes) -> Result<impl Iterator<Item = CheckpointData>> {
354 Ok(make_iterator::<CheckpointData, Reader<Bytes>>(
355 CHECKPOINT_FILE_MAGIC,
356 blob.reader(),
357 )?)
358}
359
360pub fn make_blob_iterator_for_range(
368 blob: Bytes,
369 range: Range<CheckpointSequenceNumber>,
370) -> Result<impl Iterator<Item = CheckpointData>> {
371 Ok(make_blob_iterator(blob)?
372 .filter(move |data| range.contains(&data.checkpoint_summary.sequence_number)))
373}