1use crate::config::{Source, Storage};
2use anyhow::{Result, bail, ensure};
3use futures::{StreamExt, TryStreamExt};
4use object_store::{
5 ClientOptions, GetOptions, ObjectStore, ObjectStoreExt, RetryConfig, aws::AmazonS3Builder,
6 path::Path,
7};
8use std::{
9 collections::HashMap,
10 io::{Read, Seek, SeekFrom},
11 sync::{Arc, Mutex},
12 time::{Duration, Instant},
13};
14
15#[derive(Debug)]
16struct Incomplete;
17impl std::fmt::Display for Incomplete {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 f.write_str("incomplete response")
20 }
21}
22impl std::error::Error for Incomplete {}
23async fn retry(error: anyhow::Error, failures: u32, progress: Instant) -> Result<()> {
24 let transient = error.is::<Incomplete>()
25 || error
26 .downcast_ref::<object_store::Error>()
27 .is_some_and(|e| {
28 matches!(
29 e,
30 object_store::Error::Generic { .. } | object_store::Error::JoinError { .. }
31 )
32 });
33 if !transient {
34 return Err(error);
35 }
36 if failures >= 64 || progress.elapsed().as_secs() >= 900 {
37 bail!("storage no-progress retry budget exhausted: {error}");
38 }
39 let mut random = [0u8; 8];
40 getrandom::fill(&mut random).map_err(|e| anyhow::anyhow!("retry jitter: {e}"))?;
41 let fraction = (u64::from_le_bytes(random) >> 11) as f64 / ((1u64 << 53) as f64);
42 let cap = (2f64.powi((failures - 1).min(5) as i32)).min(30.0);
43 let delay = (fraction * cap).min((900.0 - progress.elapsed().as_secs_f64()).max(0.0));
44 tokio::time::sleep(Duration::from_secs_f64(delay)).await;
45 Ok(())
46}
47
48pub struct Client {
49 config: Storage,
50 pub runtime: Arc<tokio::runtime::Runtime>,
51 stores: Mutex<HashMap<String, Arc<dyn ObjectStore>>>,
52}
53impl Client {
54 pub fn new(config: Storage, runtime: Arc<tokio::runtime::Runtime>) -> Self {
55 Self {
56 config,
57 runtime,
58 stores: Mutex::new(HashMap::new()),
59 }
60 }
61 pub fn store(&self, bucket: &str) -> Result<Arc<dyn ObjectStore>> {
64 let mut stores = self.stores.lock().unwrap();
65 if let Some(store) = stores.get(bucket) {
66 return Ok(store.clone());
67 }
68 let (Some(base_url), Some(region), Some(key), Some(secret)) = (
69 self.config.base_url.as_ref(),
70 self.config.region.as_ref(),
71 self.config.access_key_id.as_ref(),
72 self.config.secret_access_key.as_ref(),
73 ) else {
74 bail!("s3 URIs need storage baseUrl, region, accessKeyId and secretAccessKey");
75 };
76 let builder = AmazonS3Builder::new()
77 .with_bucket_name(bucket)
78 .with_region(region)
79 .with_endpoint(base_url)
80 .with_access_key_id(key)
81 .with_secret_access_key(secret)
82 .with_virtual_hosted_style_request(self.config.virtual_hosted_style_request)
83 .with_client_options(
84 ClientOptions::new()
85 .with_allow_http(true)
86 .with_pool_idle_timeout(Duration::from_secs(90))
87 .with_pool_max_idle_per_host(16)
88 .with_connect_timeout(Duration::from_secs(10))
89 .with_timeout(Duration::from_secs(60)),
90 )
91 .with_retry(RetryConfig {
92 max_retries: 3,
93 retry_timeout: Duration::from_secs(60),
94 ..Default::default()
95 });
96 let store: Arc<dyn ObjectStore> = Arc::new(builder.build()?);
97
98 stores.insert(bucket.into(), store.clone());
99 Ok(store)
100 }
101}
102
103pub struct Object {
104 local: Option<std::fs::File>,
105 remote: Option<(Arc<dyn ObjectStore>, Path)>,
106 runtime: Arc<tokio::runtime::Runtime>,
107 pub size: u64,
108 etag: Option<String>,
109}
110impl Object {
111 pub fn open(source: &Source, client: &Client) -> Result<Self> {
112 let mut object = Self {
113 local: None,
114 remote: None,
115 runtime: client.runtime.clone(),
116 size: 0,
117 etag: source.etag.clone().filter(|e| !e.is_empty()),
118 };
119 if let Some(uri) = source.uri.strip_prefix("s3://") {
120 let (bucket, key) = uri
121 .split_once('/')
122 .ok_or_else(|| anyhow::anyhow!("invalid S3 URI"))?;
123 let store = client.store(bucket)?;
124 let path = Path::from(key);
125 object.size = if let Some(n) = source.size_bytes {
126 n
127 } else {
128 object.runtime.block_on(async {
129 let progress = Instant::now();
130 let mut failures = 0;
131 loop {
132 match store.head(&path).await {
133 Ok(meta) => return Ok::<_, anyhow::Error>(meta.size),
134 Err(error) => {
135 failures += 1;
136 retry(error.into(), failures, progress).await?;
137 }
138 }
139 }
140 })?
141 };
142 object.remote = Some((store, path));
143 } else {
144 let file =
145 std::fs::File::open(source.uri.strip_prefix("file://").unwrap_or(&source.uri))?;
146 object.size = file.metadata()?.len();
147 if let Some(n) = source.size_bytes {
148 ensure!(n == object.size, "local size mismatch");
149 }
150 object.local = Some(file);
151 }
152 Ok(object)
153 }
154 pub fn range(&mut self, start: u64, end: u64) -> Result<Vec<u8>> {
155 ensure!(start <= end && end <= self.size, "invalid byte range");
156 let mut body = Vec::with_capacity((end - start).try_into()?);
157 if start == end {
158 return Ok(body);
159 }
160 if let Some(file) = &mut self.local {
161 file.seek(SeekFrom::Start(start))?;
162 file.take(end - start).read_to_end(&mut body)?;
163 ensure!(body.len() as u64 == end - start, "truncated local object");
164 return Ok(body);
165 }
166 let (store, path) = self.remote.as_ref().unwrap();
167 let etag = self.etag.clone();
168 self.runtime.block_on(async {
169 let ranges = (start..end).step_by(2 * 1024 * 1024).map(|offset| {
170 fetch(
171 store.as_ref(),
172 path,
173 etag.clone(),
174 offset,
175 (offset + 2 * 1024 * 1024).min(end),
176 )
177 });
178 let mut pending = futures::stream::iter(ranges).buffered(2);
179 while let Some((part, observed)) = pending.try_next().await? {
180 if let (Some(expected), Some(actual)) = (&self.etag, &observed) {
181 ensure!(expected == actual, "object changed between ranges");
182 }
183 if self.etag.is_none() {
184 self.etag = observed;
185 }
186 body.extend_from_slice(&part);
187 }
188 Ok(body)
189 })
190 }
191}
192
193async fn fetch(
194 store: &dyn ObjectStore,
195 path: &Path,
196 mut etag: Option<String>,
197 start: u64,
198 end: u64,
199) -> Result<(Vec<u8>, Option<String>)> {
200 let _timer = crate::profile::Timer::new(0);
201 let mut body = Vec::with_capacity((end - start) as usize);
202 let mut failures = 0u32;
203 let mut progress = Instant::now();
204 while start + (body.len() as u64) < end {
205 let result: Result<()> = async {
206 let options = GetOptions {
207 range: Some((start + body.len() as u64..end).into()),
208 if_match: etag.clone(),
209 ..Default::default()
210 };
211 let request = crate::profile::Request::new(end - start - body.len() as u64);
212 let response = store.get_opts(path, options).await?;
213 request.headers();
214 if etag.is_none() {
215 etag = response.meta.e_tag.clone();
216 }
217 let mut stream = response.into_stream();
218 while let Some(chunk) = stream.try_next().await? {
219 if chunk.is_empty() {
220 continue;
221 }
222 ensure!(
223 body.len() as u64 + chunk.len() as u64 <= end - start,
224 "response exceeded range"
225 );
226 request.bytes(chunk.len());
227 body.extend_from_slice(&chunk);
228 failures = 0;
229 progress = Instant::now();
230 }
231 if body.len() as u64 != end - start {
232 return Err(Incomplete.into());
233 }
234 Ok(())
235 }
236 .await;
237 if let Err(error) = result {
238 failures += 1;
239 retry(error, failures, progress).await?;
240 }
241 }
242 Ok((body, etag))
243}
244
245pub struct Input {
247 object: Arc<Mutex<Object>>,
248 position: u64,
249 buffer: std::io::Cursor<Vec<u8>>,
250}
251impl Input {
252 pub fn new(object: Object) -> Self {
253 Self::at(Arc::new(Mutex::new(object)), 0)
254 }
255 pub fn at(object: Arc<Mutex<Object>>, position: u64) -> Self {
256 Self {
257 object,
258 position,
259 buffer: std::io::Cursor::new(Vec::new()),
260 }
261 }
262}
263impl Read for Input {
264 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
265 if out.is_empty() {
266 return Ok(0);
267 }
268 if self.buffer.position() == self.buffer.get_ref().len() as u64 {
269 let mut object = self.object.lock().unwrap();
270 let end = (self.position + 4 * 1024 * 1024).min(object.size);
271 let data = object
272 .range(self.position, end)
273 .map_err(std::io::Error::other)?;
274 self.position = end;
275 self.buffer = std::io::Cursor::new(data);
276 }
277 self.buffer.read(out)
278 }
279}