1use crate::config::{Source, Storage};
2use anyhow::{Result, bail, ensure};
3use bytes::Bytes;
4use futures::{StreamExt, TryStreamExt};
5use object_store::{
6 ClientOptions, GetOptions, ObjectStore, ObjectStoreExt, RetryConfig, aws::AmazonS3Builder,
7 path::Path,
8};
9use std::{
10 collections::HashMap,
11 io::Read,
12 ops::Range,
13 os::unix::fs::FileExt,
14 sync::{Arc, Mutex},
15 time::{Duration, Instant},
16};
17
18const PART: u64 = 4 * 1024 * 1024;
22const PARALLEL: usize = 8;
24const AHEAD: usize = 4;
26pub const HELD: usize = AHEAD + 2;
28pub const PARTS: u64 = (AHEAD * PARALLEL) as u64 * PART;
30pub const WINDOW: u64 = 8 * 1024 * 1024;
32
33#[derive(Debug)]
34struct Incomplete;
35impl std::fmt::Display for Incomplete {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str("incomplete response")
38 }
39}
40impl std::error::Error for Incomplete {}
41async fn retry(error: anyhow::Error, failures: u32, progress: Instant) -> Result<()> {
42 let transient = error.is::<Incomplete>()
43 || error
44 .downcast_ref::<object_store::Error>()
45 .is_some_and(|e| {
46 matches!(
47 e,
48 object_store::Error::Generic { .. } | object_store::Error::JoinError { .. }
49 )
50 });
51 if !transient {
52 return Err(error);
53 }
54 if failures >= 64 || progress.elapsed().as_secs() >= 900 {
55 bail!("storage no-progress retry budget exhausted: {error}");
56 }
57 let mut random = [0u8; 8];
58 getrandom::fill(&mut random).map_err(|e| anyhow::anyhow!("retry jitter: {e}"))?;
59 let fraction = (u64::from_le_bytes(random) >> 11) as f64 / ((1u64 << 53) as f64);
60 let cap = (2f64.powi((failures - 1).min(5) as i32)).min(30.0);
61 let delay = (fraction * cap).min((900.0 - progress.elapsed().as_secs_f64()).max(0.0));
62 tokio::time::sleep(Duration::from_secs_f64(delay)).await;
63 Ok(())
64}
65
66pub struct Client {
67 config: Storage,
68 pub runtime: Arc<tokio::runtime::Runtime>,
69 stores: Mutex<HashMap<String, Arc<dyn ObjectStore>>>,
70}
71impl Client {
72 pub fn new(config: Storage, runtime: Arc<tokio::runtime::Runtime>) -> Self {
73 Self {
74 config,
75 runtime,
76 stores: Mutex::new(HashMap::new()),
77 }
78 }
79 pub fn store(&self, bucket: &str) -> Result<Arc<dyn ObjectStore>> {
82 let mut stores = self.stores.lock().unwrap();
83 if let Some(store) = stores.get(bucket) {
84 return Ok(store.clone());
85 }
86 let (Some(base_url), Some(region), Some(key), Some(secret)) = (
87 self.config.base_url.as_ref(),
88 self.config.region.as_ref(),
89 self.config.access_key_id.as_ref(),
90 self.config.secret_access_key.as_ref(),
91 ) else {
92 bail!("s3 URIs need storage baseUrl, region, accessKeyId and secretAccessKey");
93 };
94 let builder = AmazonS3Builder::new()
95 .with_bucket_name(bucket)
96 .with_region(region)
97 .with_endpoint(base_url)
98 .with_access_key_id(key)
99 .with_secret_access_key(secret)
100 .with_virtual_hosted_style_request(self.config.virtual_hosted_style_request)
101 .with_client_options(
102 ClientOptions::new()
103 .with_allow_http(true)
104 .with_pool_idle_timeout(Duration::from_secs(90))
105 .with_pool_max_idle_per_host(64)
106 .with_connect_timeout(Duration::from_secs(10))
107 .with_timeout(Duration::from_secs(60)),
108 )
109 .with_retry(RetryConfig {
110 max_retries: 3,
111 retry_timeout: Duration::from_secs(60),
112 ..Default::default()
113 });
114 let store: Arc<dyn ObjectStore> = Arc::new(builder.build()?);
115
116 stores.insert(bucket.into(), store.clone());
117 Ok(store)
118 }
119}
120
121#[derive(Clone)]
122enum Location {
123 Local(Arc<std::fs::File>),
124 Remote(Arc<dyn ObjectStore>, Path),
125}
126pub struct Object {
127 location: Location,
128 runtime: Arc<tokio::runtime::Runtime>,
129 pub size: u64,
130 etag: Option<String>,
131}
132impl Object {
133 pub fn open(source: &Source, client: &Client) -> Result<Self> {
134 let runtime = client.runtime.clone();
135 let etag = source.etag.clone().filter(|e| !e.is_empty());
136 if let Some(uri) = source.uri.strip_prefix("s3://") {
137 let (bucket, key) = uri
138 .split_once('/')
139 .ok_or_else(|| anyhow::anyhow!("invalid S3 URI"))?;
140 let store = client.store(bucket)?;
141 let path = Path::from(key);
142 let size = if let Some(n) = source.size_bytes {
143 n
144 } else {
145 runtime.block_on(async {
146 let progress = Instant::now();
147 let mut failures = 0;
148 loop {
149 match store.head(&path).await {
150 Ok(meta) => return Ok::<_, anyhow::Error>(meta.size),
151 Err(error) => {
152 failures += 1;
153 retry(error.into(), failures, progress).await?;
154 }
155 }
156 }
157 })?
158 };
159 return Ok(Self {
160 location: Location::Remote(store, path),
161 runtime,
162 size,
163 etag,
164 });
165 }
166 let file = std::fs::File::open(source.uri.strip_prefix("file://").unwrap_or(&source.uri))?;
167 let size = file.metadata()?.len();
168 if let Some(n) = source.size_bytes {
169 ensure!(n == size, "local size mismatch");
170 }
171 Ok(Self {
172 location: Location::Local(Arc::new(file)),
173 runtime,
174 size,
175 etag,
176 })
177 }
178 pub fn range(&mut self, start: u64, end: u64) -> Result<Bytes> {
179 ensure!(start <= end && end <= self.size, "invalid byte range");
180 let (mut bodies, observed) = self.runtime.block_on(fetch_ranges(
181 self.location.clone(),
182 self.etag.clone(),
183 std::iter::once(start..end).collect(),
184 ))?;
185 if self.etag.is_none() {
186 self.etag = observed;
187 }
188 Ok(bodies.pop().unwrap())
189 }
190 pub fn prefetch(&self, lists: Vec<Vec<Range<u64>>>) -> Result<Prefetch> {
193 for range in lists.iter().flatten() {
194 ensure!(
195 range.start <= range.end && range.end <= self.size,
196 "invalid byte range"
197 );
198 }
199 let (send, receiver) = tokio::sync::mpsc::channel(1);
200 let (location, etag) = (self.location.clone(), self.etag.clone());
201 let task = self.runtime.spawn(async move {
202 let mut fetched = futures::stream::iter(lists)
203 .map(|ranges| fetch_ranges(location.clone(), etag.clone(), ranges))
204 .buffered(AHEAD);
205 while let Some(result) = fetched.next().await {
206 let failed = result.is_err();
207 if send.send(result).await.is_err() || failed {
208 return;
209 }
210 }
211 });
212 Ok(Prefetch {
213 receiver,
214 task,
215 etag: self.etag.clone(),
216 })
217 }
218}
219
220type Fetched = Result<(Vec<Bytes>, Option<String>)>;
221pub struct Prefetch {
222 receiver: tokio::sync::mpsc::Receiver<Fetched>,
223 task: tokio::task::JoinHandle<()>,
224 etag: Option<String>,
225}
226impl Iterator for Prefetch {
227 type Item = Result<Vec<Bytes>>;
228 fn next(&mut self) -> Option<Self::Item> {
229 let wait = crate::profile::Timer::new(11);
230 let fetched = self.receiver.blocking_recv()?;
231 drop(wait);
232 Some(fetched.and_then(|(bodies, observed)| {
233 if let (Some(expected), Some(actual)) = (&self.etag, &observed) {
234 ensure!(expected == actual, "object changed between ranges");
235 }
236 if self.etag.is_none() {
237 self.etag = observed;
238 }
239 Ok(bodies)
240 }))
241 }
242}
243impl Drop for Prefetch {
244 fn drop(&mut self) {
245 self.task.abort();
246 }
247}
248
249async fn fetch_ranges(
252 location: Location,
253 mut etag: Option<String>,
254 ranges: Vec<Range<u64>>,
255) -> Fetched {
256 let (store, path) = match location {
257 Location::Remote(store, path) => (store, path),
258 Location::Local(file) => {
259 let bodies = tokio::task::spawn_blocking(move || {
260 ranges
261 .iter()
262 .map(|range| {
263 let mut body = vec![0; (range.end - range.start).try_into()?];
264 file.read_exact_at(&mut body, range.start)?;
265 Ok(Bytes::from(body))
266 })
267 .collect::<Result<Vec<_>>>()
268 })
269 .await??;
270 return Ok((bodies, None));
271 }
272 };
273 let parts: Vec<_> = ranges
274 .iter()
275 .flat_map(|range| {
276 (range.start..range.end)
277 .step_by(PART as usize)
278 .map(move |offset| (offset, (offset + PART).min(range.end)))
279 })
280 .collect();
281 let requested = etag.clone();
282 let mut pending = futures::stream::iter(parts)
283 .map(move |(start, end)| {
284 let (store, path, etag) = (store.clone(), path.clone(), requested.clone());
285 async move { fetch(store.as_ref(), &path, etag, start, end).await }
286 })
287 .buffered(PARALLEL);
288 let mut bodies = Vec::with_capacity(ranges.len());
289 for range in &ranges {
290 let mut body = Vec::with_capacity((range.end - range.start).try_into()?);
291 while (body.len() as u64) < range.end - range.start {
292 let (part, observed) = pending.try_next().await?.unwrap();
293 if let (Some(expected), Some(actual)) = (&etag, &observed) {
294 ensure!(expected == actual, "object changed between ranges");
295 }
296 if etag.is_none() {
297 etag = observed;
298 }
299 body.extend_from_slice(&part);
300 }
301 bodies.push(Bytes::from(body));
302 }
303 Ok((bodies, etag))
304}
305
306async fn fetch(
307 store: &dyn ObjectStore,
308 path: &Path,
309 mut etag: Option<String>,
310 start: u64,
311 end: u64,
312) -> Result<(Vec<u8>, Option<String>)> {
313 let _timer = crate::profile::Timer::new(0);
314 let mut body = Vec::with_capacity((end - start) as usize);
315 let mut failures = 0u32;
316 let mut progress = Instant::now();
317 while start + (body.len() as u64) < end {
318 let result: Result<()> = async {
319 let options = GetOptions {
320 range: Some((start + body.len() as u64..end).into()),
321 if_match: etag.clone(),
322 ..Default::default()
323 };
324 let request = crate::profile::Request::new(end - start - body.len() as u64);
325 let response = store.get_opts(path, options).await?;
326 request.headers();
327 if etag.is_none() {
328 etag = response.meta.e_tag.clone();
329 }
330 let mut stream = response.into_stream();
331 while let Some(chunk) = stream.try_next().await? {
332 if chunk.is_empty() {
333 continue;
334 }
335 ensure!(
336 body.len() as u64 + chunk.len() as u64 <= end - start,
337 "response exceeded range"
338 );
339 request.bytes(chunk.len());
340 body.extend_from_slice(&chunk);
341 failures = 0;
342 progress = Instant::now();
343 }
344 if body.len() as u64 != end - start {
345 return Err(Incomplete.into());
346 }
347 Ok(())
348 }
349 .await;
350 if let Err(error) = result {
351 failures += 1;
352 retry(error, failures, progress).await?;
353 }
354 }
355 Ok((body, etag))
356}
357
358pub struct Input {
363 object: Arc<Mutex<Object>>,
364 position: u64,
365 size: u64,
366 window: u64,
367 buffer: std::io::Cursor<Bytes>,
368 ahead: Option<Prefetch>,
369}
370impl Input {
371 pub fn stream(object: Object) -> Result<Self> {
372 let windows = (0..object.size)
373 .step_by(WINDOW as usize)
374 .map(|start| std::iter::once(start..(start + WINDOW).min(object.size)).collect())
375 .collect();
376 let ahead = Some(object.prefetch(windows)?);
377 let mut input = Self::at(Arc::new(Mutex::new(object)), 0, WINDOW);
378 input.ahead = ahead;
379 Ok(input)
380 }
381 pub fn at(object: Arc<Mutex<Object>>, position: u64, window: u64) -> Self {
382 let size = object.lock().unwrap().size;
383 Self {
384 object,
385 position,
386 size,
387 window,
388 buffer: std::io::Cursor::new(Bytes::new()),
389 ahead: None,
390 }
391 }
392}
393impl Read for Input {
394 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
395 if out.is_empty() {
396 return Ok(0);
397 }
398 if self.buffer.position() == self.buffer.get_ref().len() as u64 && self.position < self.size
399 {
400 let data = match &mut self.ahead {
401 Some(ahead) => ahead
402 .next()
403 .ok_or_else(|| std::io::Error::other("object prefetch stopped"))?
404 .map_err(std::io::Error::other)?
405 .pop()
406 .unwrap(),
407 None => {
408 let end = (self.position + self.window).min(self.size);
409 self.object
410 .lock()
411 .unwrap()
412 .range(self.position, end)
413 .map_err(std::io::Error::other)?
414 }
415 };
416 self.position += data.len() as u64;
417 self.buffer = std::io::Cursor::new(data);
418 }
419 self.buffer.read(out)
420 }
421}