Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/s3_store.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::cmp;
16
use core::pin::Pin;
17
use core::time::Duration;
18
use std::borrow::Cow;
19
use std::sync::Arc;
20
21
use async_trait::async_trait;
22
use aws_config::default_provider::credentials;
23
use aws_config::provider_config::ProviderConfig;
24
use aws_config::{AppName, BehaviorVersion};
25
use aws_sdk_s3::Client;
26
use aws_sdk_s3::config::Region;
27
use aws_sdk_s3::operation::create_multipart_upload::CreateMultipartUploadOutput;
28
use aws_sdk_s3::operation::get_object::GetObjectError;
29
use aws_sdk_s3::operation::head_object::HeadObjectError;
30
use aws_sdk_s3::primitives::ByteStream; // SdkBody
31
use aws_sdk_s3::types::builders::{CompletedMultipartUploadBuilder, CompletedPartBuilder};
32
use aws_smithy_types::body::SdkBody;
33
use futures::future::FusedFuture;
34
use futures::stream::{FuturesUnordered, unfold};
35
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
36
use nativelink_config::stores::ExperimentalAwsSpec;
37
// Note: S3 store should be very careful about the error codes it returns
38
// when in a retryable wrapper. Always prefer Code::Aborted or another
39
// retryable code over Code::InvalidArgument or make_input_err!().
40
// ie: Don't import make_input_err!() to help prevent this.
41
use nativelink_error::{Code, Error, ResultExt, make_err};
42
use nativelink_metric::MetricsComponent;
43
use nativelink_util::buf_channel::{
44
    DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair,
45
};
46
use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator};
47
use nativelink_util::instant_wrapper::InstantWrapper;
48
use nativelink_util::retry::{Retrier, RetryResult};
49
use nativelink_util::store_trait::{
50
    RemoveCallback, StoreDriver, StoreKey, StoreOptimizations, UploadSizeInfo,
51
};
52
use parking_lot::Mutex;
53
use tokio::sync::mpsc;
54
use tokio::time::sleep;
55
use tracing::{error, info};
56
57
use crate::cas_utils::is_zero_digest;
58
use crate::common_s3_utils::{BodyWrapper, TlsClient};
59
60
// S3 object cannot be larger than this number. See:
61
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
62
const MAX_UPLOAD_SIZE: u64 = 48 * 1024 * 1024 * 1024 * 1024; // 48TiB (technically should be 48.8 TiB, but close enough)
63
64
// S3 parts cannot be smaller than this number. See:
65
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
66
const MIN_MULTIPART_SIZE: u64 = 5 * 1024 * 1024; // 5MB.
67
68
// S3 parts cannot be larger than this number. See:
69
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
70
const MAX_MULTIPART_SIZE: u64 = 5 * 1024 * 1024 * 1024; // 5GB.
71
72
// S3 parts cannot be more than this number. See:
73
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
74
// Note: Type 'u64' chosen to simplify calculations
75
const MAX_UPLOAD_PARTS: u64 = 10_000;
76
77
// Default max buffer size for retrying upload requests.
78
// Note: If you change this, adjust the docs in the config.
79
const DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST: usize = 5 * 1024 * 1024; // 5MB.
80
81
// Default limit for concurrent part uploads per multipart upload.
82
// Note: If you change this, adjust the docs in the config.
83
const DEFAULT_MULTIPART_MAX_CONCURRENT_UPLOADS: usize = 10;
84
85
#[derive(Debug, MetricsComponent)]
86
pub struct S3Store<NowFn> {
87
    s3_client: Arc<Client>,
88
    now_fn: NowFn,
89
    #[metric(help = "The bucket name for the S3 store")]
90
    bucket: String,
91
    #[metric(help = "The key prefix for the S3 store")]
92
    key_prefix: String,
93
    retrier: Retrier,
94
    #[metric(help = "The number of seconds to consider an object expired")]
95
    consider_expired_after_s: i64,
96
    #[metric(help = "The number of bytes to buffer for retrying requests")]
97
    max_retry_buffer_per_request: usize,
98
    #[metric(help = "The number of concurrent uploads allowed for multipart uploads")]
99
    multipart_max_concurrent_uploads: usize,
100
101
    remove_callbacks: Mutex<Vec<RemoveCallback>>,
102
}
103
104
impl<I, NowFn> S3Store<NowFn>
105
where
106
    I: InstantWrapper,
107
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
108
{
109
0
    pub async fn new(spec: &ExperimentalAwsSpec, now_fn: NowFn) -> Result<Arc<Self>, Error> {
110
0
        let jitter_fn = spec.common.retry.make_jitter_fn();
111
0
        let s3_client = {
112
0
            let http_client = TlsClient::new(&spec.common.clone());
113
114
0
            let credential_provider = credentials::DefaultCredentialsChain::builder()
115
0
                .configure(
116
0
                    ProviderConfig::without_region()
117
0
                        .with_region(Some(Region::new(Cow::Owned(spec.region.clone()))))
118
0
                        .with_http_client(http_client.clone()),
119
0
                )
120
0
                .build()
121
0
                .await;
122
123
0
            let config = aws_config::defaults(BehaviorVersion::latest())
124
0
                .credentials_provider(credential_provider)
125
0
                .app_name(AppName::new("nativelink").expect("valid app name"))
126
0
                .timeout_config(
127
0
                    aws_config::timeout::TimeoutConfig::builder()
128
0
                        .connect_timeout(Duration::from_secs(15))
129
0
                        .build(),
130
0
                )
131
0
                .region(Region::new(Cow::Owned(spec.region.clone())))
132
0
                .http_client(http_client)
133
0
                .load()
134
0
                .await;
135
136
0
            Client::new(&config)
137
        };
138
0
        Self::new_with_client_and_jitter(spec, s3_client, jitter_fn, now_fn)
139
0
    }
140
141
19
    pub fn new_with_client_and_jitter(
142
19
        spec: &ExperimentalAwsSpec,
143
19
        s3_client: Client,
144
19
        jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>,
145
19
        now_fn: NowFn,
146
19
    ) -> Result<Arc<Self>, Error> {
147
19
        Ok(Arc::new(Self {
148
19
            s3_client: Arc::new(s3_client),
149
19
            now_fn,
150
19
            bucket: spec.bucket.clone(),
151
19
            key_prefix: spec
152
19
                .common
153
19
                .key_prefix
154
19
                .as_ref()
155
19
                .unwrap_or(&String::new())
156
19
                .clone(),
157
19
            retrier: Retrier::new(
158
19
                Arc::new(|duration| 
Box::pin2
(
sleep2
(
duration2
))),
159
19
                jitter_fn,
160
19
                spec.common.retry.clone(),
161
            ),
162
19
            consider_expired_after_s: i64::from(spec.common.consider_expired_after_s),
163
19
            max_retry_buffer_per_request: spec
164
19
                .common
165
19
                .max_retry_buffer_per_request
166
19
                .unwrap_or(DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST),
167
19
            multipart_max_concurrent_uploads: spec
168
19
                .common
169
19
                .multipart_max_concurrent_uploads
170
19
                .map_or(DEFAULT_MULTIPART_MAX_CONCURRENT_UPLOADS, |v| v),
171
19
            remove_callbacks: Mutex::new(Vec::new()),
172
        }))
173
19
    }
174
175
19
    fn make_s3_path(&self, key: &StoreKey<'_>) -> String {
176
19
        format!("{}{}", self.key_prefix, key.as_str())
177
19
    }
178
179
7
    async fn has(self: Pin<&Self>, digest: StoreKey<'_>) -> Result<Option<u64>, Error> {
180
7
        let digest_clone = digest.into_owned();
181
7
        self.retrier
182
8
            .
retry7
(
unfold7
(
()7
, move |state| {
183
8
                let local_digest = digest_clone.clone();
184
8
                async move {
185
8
                    let result = self
186
8
                        .s3_client
187
8
                        .head_object()
188
8
                        .bucket(&self.bucket)
189
8
                        .key(self.make_s3_path(&local_digest))
190
8
                        .send()
191
8
                        .await;
192
193
8
                    match result {
194
5
                        Ok(head_object_output) => {
195
5
                            if self.consider_expired_after_s != 0
196
2
                                && let Some(last_modified) = head_object_output.last_modified
197
                            {
198
2
                                let now_s = (self.now_fn)().unix_timestamp() as i64;
199
2
                                if last_modified.secs() + self.consider_expired_after_s <= now_s {
200
1
                                    let remove_callbacks = self.remove_callbacks.lock().clone();
201
1
                                    let mut callbacks: FuturesUnordered<_> = remove_callbacks
202
1
                                        .iter()
203
1
                                        .map(|callback| 
callback0
.
callback0
(
local_digest0
.
borrow0
()))
204
1
                                        .collect();
205
1
                                    while callbacks.next().await.is_some() 
{}0
206
1
                                    return Some((RetryResult::Ok(None), state));
207
1
                                }
208
3
                            }
209
4
                            let Some(length) = head_object_output.content_length else {
210
0
                                return Some((RetryResult::Ok(None), state));
211
                            };
212
4
                            if length >= 0 {
213
4
                                return Some((RetryResult::Ok(Some(length as u64)), state));
214
0
                            }
215
0
                            Some((
216
0
                                RetryResult::Err(make_err!(
217
0
                                    Code::InvalidArgument,
218
0
                                    "Negative content length in S3: {length:?}",
219
0
                                )),
220
0
                                state,
221
0
                            ))
222
                        }
223
3
                        Err(sdk_error) => match sdk_error.into_service_error() {
224
2
                            HeadObjectError::NotFound(_) => Some((RetryResult::Ok(None), state)),
225
1
                            other => Some((
226
1
                                RetryResult::Retry(
227
1
                                    Error::from_std_err(Code::Unavailable, &other)
228
1
                                        .append("Unhandled HeadObjectError in S3"),
229
1
                                ),
230
1
                                state,
231
1
                            )),
232
                        },
233
                    }
234
8
                }
235
8
            }))
236
7
            .await
237
7
    }
238
}
239
240
#[async_trait]
241
impl<I, NowFn> StoreDriver for S3Store<NowFn>
242
where
243
    I: InstantWrapper,
244
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
245
{
246
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
247
        Ok(())
248
0
    }
249
250
    async fn has_with_results(
251
        self: Pin<&Self>,
252
        keys: &[StoreKey<'_>],
253
        results: &mut [Option<u64>],
254
8
    ) -> Result<(), Error> {
255
        keys.iter()
256
            .zip(results.iter_mut())
257
8
            .map(|(key, result)| async move {
258
                // We need to do a special pass to ensure our zero key exist.
259
8
                if is_zero_digest(key.borrow()) {
260
1
                    *result = Some(0);
261
1
                    return Ok::<_, Error>(());
262
7
                }
263
7
                *result = self.has(key.borrow()).await
?0
;
264
7
                Ok::<_, Error>(())
265
16
            })
266
            .collect::<FuturesUnordered<_>>()
267
            .try_collect()
268
            .await
269
8
    }
270
271
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
272
0
        matches!(optimization, StoreOptimizations::LazyExistenceOnSync)
273
0
    }
274
275
    async fn update(
276
        self: Pin<&Self>,
277
        digest: StoreKey<'_>,
278
        mut reader: DropCloserReadHalf,
279
        upload_size: UploadSizeInfo,
280
4
    ) -> Result<u64, Error> {
281
        let s3_path = &self.make_s3_path(&digest);
282
283
        let max_size = match upload_size {
284
            UploadSizeInfo::ExactSize(sz) | UploadSizeInfo::MaxSize(sz) => sz,
285
        };
286
287
        // Sanity check S3 maximum upload size.
288
        if max_size > MAX_UPLOAD_SIZE {
289
            return Err(make_err!(
290
                Code::FailedPrecondition,
291
                "File size exceeds max of {MAX_UPLOAD_SIZE}"
292
            ));
293
        }
294
295
        // Note(aaronmondal) It might be more optimal to use a different
296
        // heuristic here, but for simplicity we use a hard coded value.
297
        // Anything going down this if-statement will have the advantage of only
298
        // 1 network request for the upload instead of minimum of 3 required for
299
        // multipart upload requests.
300
        //
301
        // Note(aaronmondal) If the upload size is not known, we go down the multipart upload path.
302
        // This is not very efficient, but it greatly reduces the complexity of the code.
303
        if max_size < MIN_MULTIPART_SIZE && matches!(upload_size, UploadSizeInfo::ExactSize(_)) {
304
            let UploadSizeInfo::ExactSize(sz) = upload_size else {
305
                unreachable!("upload_size must be UploadSizeInfo::ExactSize here");
306
            };
307
            reader.set_max_recent_data_size(
308
                u64::try_from(self.max_retry_buffer_per_request)
309
                    .err_tip(|| "Could not convert max_retry_buffer_per_request to u64")?,
310
            );
311
            return self
312
                .retrier
313
2
                .retry(unfold(reader, move |mut reader| async move {
314
                    // We need to make a new pair here because the aws sdk does not give us
315
                    // back the body after we send it in order to retry.
316
2
                    let (mut tx, rx) = make_buf_channel_pair();
317
318
                    // Upload the data to the S3 backend.
319
2
                    let result = {
320
2
                        let reader_ref = &mut reader;
321
2
                        let (upload_res, bind_res) = tokio::join!(
322
2
                            self.s3_client
323
2
                                .put_object()
324
2
                                .bucket(&self.bucket)
325
2
                                .key(s3_path.clone())
326
2
                                .content_length(sz as i64)
327
2
                                .body(ByteStream::from_body_1_x(BodyWrapper {
328
2
                                    reader: rx,
329
2
                                    size: sz,
330
2
                                }))
331
2
                                .send()
332
2
                                .map_ok_or_else(|e| Err(
Error::from_std_err0
(
Code::Aborted0
,
&e0
)), |_| Ok(sz)),
333
                            // Stream all data from the reader channel to the writer channel.
334
2
                            tx.bind_buffered(reader_ref)
335
                        );
336
2
                        match (upload_res, bind_res) {
337
2
                            (Ok(size), Ok(())) => Ok(size),
338
0
                            (Err(e), _) | (_, Err(e)) => Err(e),
339
                        }
340
2
                        .err_tip(|| "Failed to upload file to s3 in single chunk")
341
                    };
342
343
                    // If we failed to upload the file, check to see if we can retry.
344
2
                    let retry_result = result.map_or_else(|mut err| 
{0
345
                        // Ensure our code is Code::Aborted, so the client can retry if possible.
346
0
                        err.code = Code::Aborted;
347
0
                        let bytes_received = reader.get_bytes_received();
348
0
                        if let Err(try_reset_err) = reader.try_reset_stream() {
349
0
                            error!(
350
                                ?bytes_received,
351
                                err = ?try_reset_err,
352
                                "Unable to reset stream after failed upload in S3Store::update"
353
                            );
354
0
                            return RetryResult::Err(err
355
0
                                .merge(try_reset_err)
356
0
                                .append(format!("Failed to retry upload with {bytes_received} bytes received in S3Store::update")));
357
0
                        }
358
0
                        let err = err.append(format!("Retry on upload happened with {bytes_received} bytes received in S3Store::update"));
359
0
                        info!(
360
                            ?err,
361
                            ?bytes_received,
362
                            "Retryable S3 error"
363
                        );
364
0
                        RetryResult::Retry(err)
365
0
                    }, RetryResult::Ok);
366
2
                    Some((retry_result, reader))
367
4
                }))
368
                .await;
369
        }
370
371
        let upload_id = &self
372
            .retrier
373
2
            .retry(unfold((), move |()| async move {
374
2
                let retry_result = self
375
2
                    .s3_client
376
2
                    .create_multipart_upload()
377
2
                    .bucket(&self.bucket)
378
2
                    .key(s3_path)
379
2
                    .send()
380
2
                    .await
381
2
                    .map_or_else(
382
0
                        |e| {
383
0
                            RetryResult::Retry(
384
0
                                Error::from_std_err(Code::Aborted, &e)
385
0
                                    .append("Failed to create multipart upload to s3"),
386
0
                            )
387
0
                        },
388
2
                        |CreateMultipartUploadOutput { upload_id, .. }| {
389
2
                            upload_id.map_or_else(
390
0
                                || {
391
0
                                    RetryResult::Err(make_err!(
392
0
                                        Code::Internal,
393
0
                                        "Expected upload_id to be set by s3 response"
394
0
                                    ))
395
0
                                },
396
                                RetryResult::Ok,
397
                            )
398
2
                        },
399
                    );
400
2
                Some((retry_result, ()))
401
4
            }))
402
            .await?;
403
404
        // S3 requires us to upload in parts if the size is greater than 5GB. The part size must be at least
405
        // 5MB (except last part) and can have up to 10,000 parts.
406
407
        // Calculate of number of chunks if we upload in 5MB chucks (min chunk size), clamping to
408
        // 10,000 parts and correcting for lossy integer division. This provides the
409
        let chunk_count = (max_size / MIN_MULTIPART_SIZE).clamp(0, MAX_UPLOAD_PARTS - 1) + 1;
410
411
        // Using clamped first approximation of number of chunks, calculate byte count of each
412
        // chunk, excluding last chunk, clamping to min/max upload size 5MB, 5GB.
413
        let bytes_per_upload_part =
414
            (max_size / chunk_count).clamp(MIN_MULTIPART_SIZE, MAX_MULTIPART_SIZE);
415
416
        // Sanity check before continuing.
417
        if !(MIN_MULTIPART_SIZE..MAX_MULTIPART_SIZE).contains(&bytes_per_upload_part) {
418
            return Err(make_err!(
419
                Code::FailedPrecondition,
420
                "Failed to calculate file chuck size (min, max, calc): {MIN_MULTIPART_SIZE}, {MAX_MULTIPART_SIZE}, {bytes_per_upload_part}",
421
            ));
422
        }
423
424
2
        let upload_parts = move || async move {
425
            // This will ensure we only have `multipart_max_concurrent_uploads` * `bytes_per_upload_part`
426
            // bytes in memory at any given time waiting to be uploaded.
427
2
            let (tx, mut rx) = mpsc::channel(self.multipart_max_concurrent_uploads);
428
429
2
            let read_stream_fut = async move {
430
2
                let retrier = &Pin::get_ref(self).retrier;
431
2
                let mut total_uploaded = 0;
432
                // Note: Our break condition is when we reach EOF.
433
9
                for part_number in 
1..i32::MAX2
{
434
9
                    let write_buf = reader
435
9
                        .consume(Some(usize::try_from(bytes_per_upload_part).err_tip(
436
                            || "Could not convert bytes_per_upload_part to usize",
437
0
                        )?))
438
9
                        .await
439
9
                        .err_tip(|| "Failed to read chunk in s3_store")
?0
;
440
9
                    if write_buf.is_empty() {
441
2
                        break; // Reached EOF.
442
7
                    }
443
444
7
                    total_uploaded += write_buf.len() as u64;
445
446
7
                    tx.send(retrier.retry(unfold(write_buf, move |write_buf| {
447
7
                        async move {
448
7
                            let retry_result = self
449
7
                                .s3_client
450
7
                                .upload_part()
451
7
                                .bucket(&self.bucket)
452
7
                                .key(s3_path)
453
7
                                .upload_id(upload_id)
454
7
                                .body(ByteStream::new(SdkBody::from(write_buf.clone())))
455
7
                                .part_number(part_number)
456
7
                                .send()
457
7
                                .await
458
7
                                .map_or_else(
459
0
                                    |e| {
460
0
                                        RetryResult::Retry(
461
0
                                            Error::from_std_err(Code::Aborted, &e).append(format!(
462
0
                                                "Failed to upload part {part_number} in S3 store"
463
0
                                            )),
464
0
                                        )
465
0
                                    },
466
7
                                    |mut response| {
467
7
                                        RetryResult::Ok(
468
7
                                            CompletedPartBuilder::default()
469
7
                                                // Only set an entity tag if it exists. This saves
470
7
                                                // 13 bytes per part on the final request if it can
471
7
                                                // omit the `<ETAG><ETAG/>` string.
472
7
                                                .set_e_tag(response.e_tag.take())
473
7
                                                .part_number(part_number)
474
7
                                                .build(),
475
7
                                        )
476
7
                                    },
477
                                );
478
7
                            Some((retry_result, write_buf))
479
7
                        }
480
7
                    })))
481
7
                    .await
482
7
                    .map_err(|err| 
{0
483
0
                        Error::from_std_err(Code::Internal, &err)
484
0
                            .append("Failed to send part to channel in s3_store")
485
0
                    })?;
486
                }
487
2
                Result::<_, Error>::Ok(total_uploaded)
488
2
            }
489
2
            .fuse();
490
491
2
            let mut upload_futures = FuturesUnordered::new();
492
2
            let mut total_uploaded = 0;
493
494
2
            let mut completed_parts = Vec::with_capacity(
495
2
                usize::try_from(cmp::min(MAX_UPLOAD_PARTS, chunk_count))
496
2
                    .err_tip(|| "Could not convert u64 to usize")
?0
,
497
            );
498
2
            tokio::pin!(read_stream_fut);
499
            loop {
500
18
                if read_stream_fut.is_terminated() && 
rx16
.
is_empty16
() &&
upload_futures6
.
is_empty6
() {
501
2
                    break; // No more data to process.
502
16
                }
503
16
                tokio::select! {
504
16
                    
result2
= &mut read_stream_fut => {
505
2
                        total_uploaded = result
?0
;
506
                    }, // Return error or wait for other futures.
507
16
                    Some(
upload_result7
) = upload_futures.next() =>
completed_parts7
.
push7
(
upload_result7
?0
),
508
16
                    Some(
fut7
) = rx.recv() =>
upload_futures7
.
push7
(
fut7
),
509
                }
510
            }
511
512
            // Even though the spec does not require parts to be sorted by number, we do it just in case
513
            // there's an S3 implementation that requires it.
514
2
            completed_parts.sort_unstable_by_key(|part| part.part_number);
515
516
2
            self.retrier
517
2
                .retry(unfold(completed_parts, move |completed_parts| async move {
518
                    Some((
519
2
                        self.s3_client
520
2
                            .complete_multipart_upload()
521
2
                            .bucket(&self.bucket)
522
2
                            .key(s3_path)
523
2
                            .multipart_upload(
524
2
                                CompletedMultipartUploadBuilder::default()
525
2
                                    .set_parts(Some(completed_parts.clone()))
526
2
                                    .build(),
527
2
                            )
528
2
                            .upload_id(upload_id)
529
2
                            .send()
530
2
                            .await
531
2
                            .map_or_else(
532
0
                                |e| {
533
0
                                    RetryResult::Retry(
534
0
                                        Error::from_std_err(Code::Aborted, &e).append(
535
0
                                            "Failed to complete multipart upload in S3 store",
536
0
                                        ),
537
0
                                    )
538
0
                                },
539
2
                                |_| RetryResult::Ok(total_uploaded),
540
                            ),
541
2
                        completed_parts,
542
                    ))
543
4
                }))
544
2
                .await
545
4
        };
546
        // Upload our parts and complete the multipart upload.
547
        // If we fail attempt to abort the multipart upload (cleanup).
548
        upload_parts()
549
0
            .or_else(move |mut e| async move {
550
0
                let abort_res = self
551
0
                    .s3_client
552
0
                    .abort_multipart_upload()
553
0
                    .bucket(&self.bucket)
554
0
                    .key(s3_path)
555
0
                    .upload_id(upload_id)
556
0
                    .send()
557
0
                    .await;
558
0
                if let Err(abort_err) = abort_res {
559
0
                    let err = Error::from_std_err(Code::Aborted, &abort_err)
560
0
                        .append("Failed to abort multipart upload in S3 store");
561
0
                    info!(?err, "Multipart upload error");
562
0
                    e = e.merge(err);
563
0
                }
564
0
                Err(e)
565
0
            })
566
            .await
567
4
    }
568
569
    async fn get_part(
570
        self: Pin<&Self>,
571
        key: StoreKey<'_>,
572
        writer: &mut DropCloserWriteHalf,
573
        offset: u64,
574
        length: Option<u64>,
575
8
    ) -> Result<(), Error> {
576
        if is_zero_digest(key.borrow()) {
577
            writer
578
                .send_eof()
579
                .err_tip(|| "Failed to send zero EOF in filesystem store get_part")?;
580
            return Ok(());
581
        }
582
583
        let s3_path = &self.make_s3_path(&key);
584
        let end_read_byte = length
585
3
            .map_or(Some(None), |length| Some(offset.checked_add(length)))
586
            .err_tip(|| "Integer overflow protection triggered")?;
587
588
        self.retrier
589
8
            .retry(unfold(writer, move |writer| async move {
590
8
                let result = self
591
8
                    .s3_client
592
8
                    .get_object()
593
8
                    .bucket(&self.bucket)
594
8
                    .key(s3_path)
595
8
                    .range(format!(
596
                        "bytes={}-{}",
597
8
                        offset + writer.get_bytes_written(),
598
8
                        end_read_byte.map_or_else(String::new, |v| 
v3
.
to_string3
())
599
                    ))
600
8
                    .send()
601
8
                    .await;
602
603
8
                let 
mut s3_in_stream6
= match result {
604
6
                    Ok(head_object_output) => head_object_output.body,
605
2
                    Err(sdk_error) => match sdk_error.into_service_error() {
606
1
                        GetObjectError::NoSuchKey(e) => {
607
1
                            return Some((
608
1
                                RetryResult::Err(
609
1
                                    Error::from_std_err(Code::NotFound, &e)
610
1
                                        .append("No such key in S3"),
611
1
                                ),
612
1
                                writer,
613
1
                            ));
614
                        }
615
1
                        other => {
616
1
                            return Some((
617
1
                                RetryResult::Retry(
618
1
                                    Error::from_std_err(Code::Unavailable, &other)
619
1
                                        .append("Unhandled GetObjectError in S3"),
620
1
                                ),
621
1
                                writer,
622
1
                            ));
623
                        }
624
                    },
625
                };
626
627
                // Copy data from s3 input stream to the writer stream.
628
11
                while let Some(
maybe_bytes5
) = s3_in_stream.next().await {
629
5
                    match maybe_bytes {
630
5
                        Ok(bytes) => {
631
5
                            if bytes.is_empty() {
632
                                // Ignore possible EOF. Different implementations of S3 may or may not
633
                                // send EOF this way.
634
1
                                continue;
635
4
                            }
636
4
                            if let Err(
e0
) = writer.send(bytes).await {
637
0
                                return Some((
638
0
                                    RetryResult::Err(
639
0
                                        Error::from_std_err(Code::Aborted, &e)
640
0
                                            .append("Error sending bytes to consumer in S3"),
641
0
                                    ),
642
0
                                    writer,
643
0
                                ));
644
4
                            }
645
                        }
646
0
                        Err(e) => {
647
0
                            return Some((
648
0
                                RetryResult::Retry(
649
0
                                    Error::from_std_err(Code::Aborted, &e)
650
0
                                        .append("Bad bytestream element in S3"),
651
0
                                ),
652
0
                                writer,
653
0
                            ));
654
                        }
655
                    }
656
                }
657
6
                if let Err(
e0
) = writer.send_eof() {
658
0
                    return Some((
659
0
                        RetryResult::Err(
660
0
                            Error::from_std_err(Code::Aborted, &e)
661
0
                                .append("Failed to send EOF to consumer in S3"),
662
0
                        ),
663
0
                        writer,
664
0
                    ));
665
6
                }
666
6
                Some((RetryResult::Ok(()), writer))
667
16
            }))
668
            .await
669
8
    }
670
671
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &'_ dyn StoreDriver {
672
0
        self
673
0
    }
674
675
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
676
0
        self
677
0
    }
678
679
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
680
0
        self
681
0
    }
682
683
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
684
0
        registry.register_indicator(self);
685
0
    }
686
687
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
688
0
        self.remove_callbacks.lock().push(callback);
689
0
        Ok(())
690
0
    }
691
}
692
693
#[async_trait]
694
impl<I, NowFn> HealthStatusIndicator for S3Store<NowFn>
695
where
696
    I: InstantWrapper,
697
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
698
{
699
0
    fn get_name(&self) -> &'static str {
700
0
        "S3Store"
701
0
    }
702
703
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
704
        StoreDriver::check_health(Pin::new(self), namespace).await
705
0
    }
706
}