snix_castore/blob_engine/
concurrent_uploads.rs1use std::{
4 io::{Cursor, Write},
5 sync::Arc,
6};
7
8use tokio::{
9 io::AsyncRead,
10 sync::Semaphore,
11 task::{JoinError, JoinSet},
12};
13use tokio_util::io::InspectReader;
14use tracing::{Instrument, Level, instrument};
15
16use crate::{
17 B3Digest, Path, PathBuf,
18 blob_engine::{self, BlobEngine},
19};
20
21const CONCURRENT_BLOB_UPLOAD_THRESHOLD: u32 = 1024 * 1024;
28
29const MAX_BUFFER_SIZE: usize = 128 * 1024 * 1024;
31
32#[derive(Debug, thiserror::Error)]
33pub enum Error {
34 #[error("unable to read blob contents for {0}: {1}")]
35 BlobRead(PathBuf, std::io::Error),
36
37 #[error("unable to check whether blob at {0} already exists: {1}")]
38 BlobCheck(PathBuf, blob_engine::Error),
39
40 #[error("unable to finalize blob {0}: {1}")]
42 BlobFinalize(PathBuf, std::io::Error),
43
44 #[error("unexpected size for {path} wanted: {wanted} got: {got}")]
45 UnexpectedSize {
46 path: PathBuf,
47 wanted: u64,
48 got: u64,
49 },
50
51 #[error("blob upload join error: {0}")]
52 JoinError(#[from] JoinError),
53}
54
55pub struct ConcurrentBlobUploader<BE> {
65 blob_engine: BE,
66 upload_tasks: JoinSet<Result<(), Error>>,
67 upload_semaphore: Arc<Semaphore>,
68}
69
70impl<BE> ConcurrentBlobUploader<BE>
71where
72 BE: BlobEngine + Clone + 'static,
73{
74 pub fn new(blob_engine: BE) -> Self {
77 Self {
78 blob_engine,
79 upload_tasks: JoinSet::new(),
80 upload_semaphore: Arc::new(Semaphore::new(MAX_BUFFER_SIZE)),
81 }
82 }
83
84 #[instrument(skip_all, fields(nar.path=%path, blob.size=expected_size), err, ret(level = Level::TRACE, Display))]
89 pub async fn upload<R>(
90 &mut self,
91 path: &Path,
92 expected_size: u64,
93 mut r: R,
94 ) -> Result<B3Digest, Error>
95 where
96 R: AsyncRead + Unpin,
97 {
98 if expected_size < CONCURRENT_BLOB_UPLOAD_THRESHOLD as u64 {
99 let mut buffer = Vec::with_capacity(expected_size as usize);
100 let mut hasher = blake3::Hasher::new();
101 let mut reader = InspectReader::new(&mut r, |bytes| {
102 hasher.write_all(bytes).unwrap();
103 });
104
105 let permit = self
106 .upload_semaphore
107 .clone()
108 .acquire_many_owned(expected_size as u32)
111 .await
112 .unwrap();
113 let size = tokio::io::copy(&mut reader, &mut buffer)
114 .await
115 .map_err(|e| Error::BlobRead(path.into(), e))?;
116 let digest: B3Digest = hasher.finalize().as_bytes().into();
117
118 if size != expected_size {
119 return Err(Error::UnexpectedSize {
120 path: path.into(),
121 wanted: expected_size,
122 got: size,
123 });
124 }
125
126 self.upload_tasks.spawn({
127 let blob_engine = self.blob_engine.clone();
128 let path = path.to_owned();
129 let r = Cursor::new(buffer);
130 async move {
131 if blob_engine
133 .has(&digest)
134 .await
135 .map_err(|e| Error::BlobCheck(path.clone(), e))?
136 {
137 drop(permit);
138 return Ok(());
139 }
140
141 let uploaded_digest =
142 upload_blob(&blob_engine, &path, expected_size, r).await?;
143
144 assert_eq!(uploaded_digest, digest, "Snix bug: blob digest mismatch");
145
146 drop(permit);
149 Ok(())
150 }
151 .in_current_span()
152 });
153
154 return Ok(digest);
155 }
156
157 upload_blob(&self.blob_engine, path, expected_size, r).await
158 }
159
160 pub async fn join(mut self) -> Result<(), Error> {
162 while let Some(result) = self.upload_tasks.join_next().await {
163 result??;
164 }
165 Ok(())
166 }
167}
168
169async fn upload_blob<BE, R>(
170 blob_engine: &BE,
171 path: &Path,
172 expected_size: u64,
173 mut r: R,
174) -> Result<B3Digest, Error>
175where
176 BE: BlobEngine,
177 R: AsyncRead + Unpin,
178{
179 let mut writer = blob_engine.open_write().await;
180
181 let size = tokio::io::copy(&mut r, &mut writer)
182 .await
183 .map_err(|e| Error::BlobRead(path.into(), e))?;
184
185 let digest = writer
186 .close()
187 .await
188 .map_err(|e| Error::BlobFinalize(path.into(), e))?;
189
190 if size != expected_size {
191 return Err(Error::UnexpectedSize {
192 path: path.into(),
193 wanted: expected_size,
194 got: size,
195 });
196 }
197
198 Ok(digest)
199}