Skip to main content

o41_training_data/
storage.rs

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    /// The object store of one bucket, built once per client with the
62    /// configured endpoint, credentials, timeouts and retries.
63    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
245// Bounded range requests preserve decoder state across transient network errors.
246pub 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}