Skip to main content

coven_replication/sync/store/blob/
eager_cache.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::Arc;
3
4use futures_util::StreamExt;
5use tokio::sync::watch;
6use tracing::{info, warn};
7
8use super::{BlobCacheError, RemoteStoreBlobAccess};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct EagerCacheFillProgress {
12    pub files_done: u64,
13    pub files_total: u64,
14    pub bytes_done: u64,
15    pub bytes_total: u64,
16}
17
18impl EagerCacheFillProgress {
19    pub(crate) fn empty() -> Self {
20        Self {
21            files_done: 0,
22            files_total: 0,
23            bytes_done: 0,
24            bytes_total: 0,
25        }
26    }
27}
28
29#[derive(Debug, Clone)]
30pub enum EagerCacheFillStatus {
31    NotRunning,
32    Scanning,
33    Downloading(EagerCacheFillProgress),
34    Complete {
35        files_total: u64,
36        bytes_total: u64,
37    },
38    Cancelled(EagerCacheFillProgress),
39    Failed {
40        progress: EagerCacheFillProgress,
41        error: Arc<EagerCacheFillError>,
42    },
43}
44
45#[derive(Debug, thiserror::Error)]
46pub enum EagerCacheFillError {
47    #[error("read eager cache bindings: {0}")]
48    Database(#[from] coven_database::DbError),
49    #[error("inspect eager cache row {table}/{row_id}: {source}")]
50    Inspect {
51        table: String,
52        row_id: String,
53        #[source]
54        source: BlobCacheError,
55    },
56    #[error("eager cache byte total exceeds the supported byte count")]
57    ByteCountOverflow,
58    #[error("remote eager cache row {table}/{row_id} has no exact stored reference")]
59    MissingStoredReference { table: String, row_id: String },
60    #[error("eager cache downloads ended before every admitted byte completed")]
61    IncompleteProgress,
62    #[error("download eager cache row {table}/{row_id}: {source}")]
63    Download {
64        table: String,
65        row_id: String,
66        #[source]
67        source: BlobCacheError,
68    },
69}
70
71struct EagerDownload {
72    reference: coven_protocol::blob::RowBlobRef,
73    stored_size: u64,
74}
75
76struct FileProgress {
77    bytes: AtomicU64,
78    total: u64,
79}
80
81impl FileProgress {
82    fn new(total: u64) -> Self {
83        Self {
84            bytes: AtomicU64::new(0),
85            total,
86        }
87    }
88}
89
90fn aggregate_progress(
91    files: &[Arc<FileProgress>],
92    completed_files: &AtomicU64,
93    files_total: u64,
94    bytes_total: u64,
95) -> Result<EagerCacheFillProgress, EagerCacheFillError> {
96    let mut bytes_done = 0_u64;
97    for file in files {
98        let current = file.bytes.load(Ordering::Relaxed);
99        if current > file.total {
100            return Err(EagerCacheFillError::ByteCountOverflow);
101        }
102        bytes_done = bytes_done
103            .checked_add(current)
104            .ok_or(EagerCacheFillError::ByteCountOverflow)?;
105    }
106    Ok(EagerCacheFillProgress {
107        files_done: completed_files.load(Ordering::Relaxed),
108        files_total,
109        bytes_done,
110        bytes_total,
111    })
112}
113
114fn fail(
115    status: &watch::Sender<EagerCacheFillStatus>,
116    progress: EagerCacheFillProgress,
117    error: EagerCacheFillError,
118) -> Arc<EagerCacheFillError> {
119    let error = Arc::new(error);
120    status.send_replace(EagerCacheFillStatus::Failed {
121        progress,
122        error: Arc::clone(&error),
123    });
124    error
125}
126
127pub(crate) async fn run(
128    database: &coven_database::StoreDatabase,
129    access: &RemoteStoreBlobAccess,
130    mut cancel: watch::Receiver<bool>,
131    status: &watch::Sender<EagerCacheFillStatus>,
132) -> Result<(), Arc<EagerCacheFillError>> {
133    status.send_replace(EagerCacheFillStatus::Scanning);
134    if *cancel.borrow() {
135        status.send_replace(EagerCacheFillStatus::Cancelled(
136            EagerCacheFillProgress::empty(),
137        ));
138        return Ok(());
139    }
140
141    let references = match database.eager_row_blob_refs().await {
142        Ok(references) => references,
143        Err(error) => return Err(fail(status, EagerCacheFillProgress::empty(), error.into())),
144    };
145    let mut downloads = Vec::new();
146    for reference in references {
147        if !matches!(
148            reference.authority(),
149            coven_protocol::blob::RowBlobAuthority::Remote(_)
150        ) {
151            continue;
152        }
153        let materialized = match access.is_materialized(&reference).await {
154            Ok(materialized) => materialized,
155            Err(source) => {
156                let error = EagerCacheFillError::Inspect {
157                    table: reference.table().to_string(),
158                    row_id: reference.row_id().to_string(),
159                    source,
160                };
161                return Err(fail(status, EagerCacheFillProgress::empty(), error));
162            }
163        };
164        if materialized {
165            continue;
166        }
167        let Some(stored) = reference.stored() else {
168            let error = EagerCacheFillError::MissingStoredReference {
169                table: reference.table().to_string(),
170                row_id: reference.row_id().to_string(),
171            };
172            return Err(fail(status, EagerCacheFillProgress::empty(), error));
173        };
174        let stored_size = stored.object().stored_size();
175        downloads.push(EagerDownload {
176            reference,
177            stored_size,
178        });
179    }
180
181    let files_total = downloads.len() as u64;
182    let bytes_total = match downloads.iter().try_fold(0_u64, |total, download| {
183        total.checked_add(download.stored_size)
184    }) {
185        Some(total) => total,
186        None => {
187            return Err(fail(
188                status,
189                EagerCacheFillProgress::empty(),
190                EagerCacheFillError::ByteCountOverflow,
191            ))
192        }
193    };
194    if downloads.is_empty() {
195        status.send_replace(EagerCacheFillStatus::Complete {
196            files_total,
197            bytes_total,
198        });
199        return Ok(());
200    }
201
202    let file_progress = downloads
203        .iter()
204        .map(|download| Arc::new(FileProgress::new(download.stored_size)))
205        .collect::<Vec<_>>();
206    let completed_files = Arc::new(AtomicU64::new(0));
207    let initial = EagerCacheFillProgress {
208        files_done: 0,
209        files_total,
210        bytes_done: 0,
211        bytes_total,
212    };
213    status.send_replace(EagerCacheFillStatus::Downloading(initial));
214
215    let limit = database.transfer_limits().downloads.get();
216    let stream = futures_util::stream::iter(downloads.into_iter().enumerate())
217        .map(|(index, download)| {
218            let progress = Arc::clone(&file_progress[index]);
219            let completed_files = Arc::clone(&completed_files);
220            async move {
221                let callback_progress = Arc::clone(&progress);
222                let callback: coven_storage::cloud::DownloadProgress =
223                    Arc::new(move |bytes_done| {
224                        callback_progress.bytes.store(bytes_done, Ordering::Relaxed);
225                    });
226                let table = download.reference.table().to_string();
227                let row_id = download.reference.row_id().to_string();
228                match access
229                    .materialize_with_progress(&download.reference, callback)
230                    .await
231                {
232                    Ok(()) => {
233                        progress.bytes.store(progress.total, Ordering::Relaxed);
234                        completed_files.fetch_add(1, Ordering::Relaxed);
235                        Ok(())
236                    }
237                    Err(source) => Err(EagerCacheFillError::Download {
238                        table,
239                        row_id,
240                        source,
241                    }),
242                }
243            }
244        })
245        .buffer_unordered(limit);
246    tokio::pin!(stream);
247    let mut ticker = tokio::time::interval(crate::blob::progress::TRANSFER_PROGRESS_TICK);
248    ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
249    ticker.tick().await;
250    let mut last_reported = initial;
251
252    loop {
253        tokio::select! {
254            next = stream.next() => {
255                match next {
256                    Some(Ok(())) => {}
257                    Some(Err(error)) => {
258                        let progress = aggregate_progress(
259                            &file_progress,
260                            &completed_files,
261                            files_total,
262                            bytes_total,
263                        ).map_err(|error| fail(status, last_reported, error))?;
264                        return Err(fail(status, progress, error));
265                    }
266                    None => break,
267                }
268            }
269            _ = ticker.tick() => {
270                let current = aggregate_progress(
271                    &file_progress,
272                    &completed_files,
273                    files_total,
274                    bytes_total,
275                ).map_err(|error| fail(status, last_reported, error))?;
276                if current != last_reported {
277                    last_reported = current;
278                    status.send_replace(EagerCacheFillStatus::Downloading(current));
279                }
280            }
281            changed = cancel.changed() => {
282                if changed.is_err() || *cancel.borrow() {
283                    let current = aggregate_progress(
284                        &file_progress,
285                        &completed_files,
286                        files_total,
287                        bytes_total,
288                    ).map_err(|error| fail(status, last_reported, error))?;
289                    status.send_replace(EagerCacheFillStatus::Cancelled(current));
290                    info!(files_done = current.files_done, files_total, "eager cache fill cancelled");
291                    return Ok(());
292                }
293            }
294        }
295    }
296
297    let completed = aggregate_progress(&file_progress, &completed_files, files_total, bytes_total)
298        .map_err(|error| fail(status, last_reported, error))?;
299    if completed.files_done != files_total || completed.bytes_done != bytes_total {
300        let error = EagerCacheFillError::IncompleteProgress;
301        warn!(
302            ?completed,
303            "eager cache fill finished with incomplete counters"
304        );
305        return Err(fail(status, completed, error));
306    }
307    status.send_replace(EagerCacheFillStatus::Complete {
308        files_total,
309        bytes_total,
310    });
311    info!(files_total, bytes_total, "eager cache fill complete");
312    Ok(())
313}