Coverage Report

Created: 2026-09-18 20:40

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