Coverage Report

Created: 2026-11-13 23:29

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 Trace Machina, Inc. 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
11
    pub async fn new_with_http_clients<C>(
125
11
        spec: &ExperimentalAwsSpec,
126
11
        http_client: C,
127
11
        credential_http_client: C,
128
11
        now_fn: NowFn,
129
11
    ) -> Result<Arc<Self>, Error>
130
11
    where
131
11
        C: SmithyHttpClient + Clone + 'static,
132
11
    {
133
11
        let jitter_fn = spec.common.retry.make_jitter_fn();
134
11
        let s3_client = {
135
11
            let region = Region::new(Cow::Owned(spec.region.clone()));
136
137
11
            let loader = aws_config::defaults(BehaviorVersion::latest())
138
11
                .app_name(AppName::new("nativelink").expect("valid app name"))
139
11
                .timeout_config(
140
11
                    aws_config::timeout::TimeoutConfig::builder()
141
11
                        .connect_timeout(Duration::from_secs(15))
142
11
                        .build(),
143
                )
144
11
                .region(region.clone())
145
11
                .http_client(http_client);
146
147
11
            let loader = if let Some(key_id) = &spec.access_key_id
148
11
                && let Some(secret) = &spec.secret_access_key
149
            {
150
11
                loader.credentials_provider(Credentials::new(
151
11
                    key_id,
152
11
                    secret,
153
11
                    None,
154
11
                    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
11
            let config = loader.load().await;
171
172
11
            let mut config_builder =
173
11
                aws_sdk_s3::config::Builder::from(&config).force_path_style(spec.force_path_style);
174
175
11
            if let Some(endpoint) = &spec.endpoint {
176
11
                config_builder = config_builder.endpoint_url(endpoint);
177
11
            
}0
178
179
11
            Client::from_conf(config_builder.build())
180
        };
181
11
        Self::new_with_client_and_jitter(spec, s3_client, jitter_fn, now_fn)
182
11
    }
183
184
30
    pub fn new_with_client_and_jitter(
185
30
        spec: &ExperimentalAwsSpec,
186
30
        s3_client: Client,
187
30
        jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>,
188
30
        now_fn: NowFn,
189
30
    ) -> Result<Arc<Self>, Error> {
190
30
        Ok(Arc::new(Self {
191
30
            s3_client: Arc::new(s3_client),
192
30
            now_fn,
193
30
            bucket: spec.bucket.clone(),
194
30
            key_prefix: spec
195
30
                .common
196
30
                .key_prefix
197
30
                .as_ref()
198
30
                .unwrap_or(&String::new())
199
30
                .clone(),
200
30
            retrier: Retrier::new(
201
30
                Arc::new(|duration| 
Box::pin2
(
sleep2
(
duration2
))),
202
30
                jitter_fn,
203
30
                spec.common.retry.clone(),
204
            )
205
30
            .with_client("s3"),
206
30
            consider_expired_after_s: i64::from(spec.common.consider_expired_after_s),
207
30
            max_retry_buffer_per_request: spec
208
30
                .common
209
30
                .max_retry_buffer_per_request
210
30
                .unwrap_or(DEFAULT_MAX_RETRY_BUFFER_PER_REQUEST),
211
30
            multipart_max_concurrent_uploads: spec
212
30
                .common
213
30
                .multipart_max_concurrent_uploads
214
30
                .unwrap_or(DEFAULT_MULTIPART_MAX_CONCURRENT_UPLOADS),
215
30
            remove_callbacks: Mutex::new(Vec::new()),
216
        }))
217
30
    }
218
219
30
    fn make_s3_path(&self, key: &StoreKey<'_>) -> String {
220
30
        format!("{}{}", self.key_prefix, key.as_str())
221
30
    }
222
223
7
    async fn has(self: Pin<&Self>, digest: StoreKey<'_>) -> Result<Option<u64>, Error> {
224
7
        let digest_clone = digest.into_owned();
225
7
        self.retrier
226
8
            .
retry7
(
unfold7
(
()7
, move |state| {
227
8
                let local_digest = digest_clone.clone();
228
8
                async move {
229
8
                    let result = self
230
8
                        .s3_client
231
8
                        .head_object()
232
8
                        .bucket(&self.bucket)
233
8
                        .key(self.make_s3_path(&local_digest))
234
8
                        .send()
235
8
                        .await;
236
237
8
                    match result {
238
5
                        Ok(head_object_output) => {
239
5
                            if self.consider_expired_after_s != 0
240
2
                                && let Some(last_modified) = head_object_output.last_modified
241
                            {
242
2
                                let now_s = (self.now_fn)()
243
2
                                    .unix_timestamp()
244
2
                                    .try_into()
245
2
                                    .unwrap_or(i64::MAX);
246
2
                                if last_modified.secs() + self.consider_expired_after_s <= now_s {
247
1
                                    let remove_callbacks = self.remove_callbacks.lock().clone();
248
1
                                    let mut callbacks: FuturesUnordered<_> = remove_callbacks
249
1
                                        .iter()
250
1
                                        .map(|callback| 
callback0
.
callback0
(
local_digest0
.
borrow0
()))
251
1
                                        .collect();
252
1
                                    while callbacks.next().await.is_some() 
{}0
253
1
                                    return Some((RetryResult::Ok(None), state));
254
1
                                }
255
3
                            }
256
4
                            let Some(length) = head_object_output.content_length else {
257
0
                                return Some((RetryResult::Ok(None), state));
258
                            };
259
4
                            if length >= 0 {
260
4
                                return Some((RetryResult::Ok(Some(length as u64)), state));
261
0
                            }
262
0
                            Some((
263
0
                                RetryResult::Err(make_err!(
264
0
                                    Code::InvalidArgument,
265
0
                                    "Negative content length in S3: {length:?}",
266
0
                                )),
267
0
                                state,
268
0
                            ))
269
                        }
270
3
                        Err(sdk_error) => match sdk_error.into_service_error() {
271
2
                            HeadObjectError::NotFound(_) => Some((RetryResult::Ok(None), state)),
272
1
                            other => Some((
273
1
                                RetryResult::Retry(
274
1
                                    Error::from_std_err(Code::Unavailable, &other)
275
1
                                        .append("Unhandled HeadObjectError in S3"),
276
1
                                ),
277
1
                                state,
278
1
                            )),
279
                        },
280
                    }
281
8
                }
282
8
            }))
283
7
            .await
284
7
    }
285
}
286
287
#[async_trait]
288
impl<I, NowFn> StoreDriver for S3Store<NowFn>
289
where
290
    I: InstantWrapper,
291
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
292
{
293
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
294
        Ok(())
295
0
    }
296
297
    async fn has_with_results(
298
        self: Pin<&Self>,
299
        keys: &[StoreKey<'_>],
300
        results: &mut [Option<u64>],
301
8
    ) -> Result<(), Error> {
302
        keys.iter()
303
            .zip(results.iter_mut())
304
8
            .map(|(key, result)| async move {
305
                // We need to do a special pass to ensure our zero key exist.
306
8
                if is_zero_digest(key.borrow()) {
307
1
                    *result = Some(0);
308
1
                    return Ok::<_, Error>(());
309
7
                }
310
7
                *result = self.has(key.borrow()).await
?0
;
311
7
                Ok::<_, Error>(())
312
16
            })
313
            .collect::<FuturesUnordered<_>>()
314
            .try_collect()
315
            .await
316
8
    }
317
318
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
319
0
        matches!(optimization, StoreOptimizations::LazyExistenceOnSync)
320
0
    }
321
322
1
    fn max_upload_size(&self) -> u64 {
323
1
        MAX_UPLOAD_SIZE
324
1
    }
325
326
    async fn update(
327
        self: Pin<&Self>,
328
        digest: StoreKey<'_>,
329
        mut reader: DropCloserReadHalf,
330
        upload_size: UploadSizeInfo,
331
15
    ) -> Result<u64, Error> {
332
        let s3_path = &self.make_s3_path(&digest);
333
334
        let max_size = match upload_size {
335
            UploadSizeInfo::ExactSize(sz) | UploadSizeInfo::MaxSize(sz) => sz,
336
        };
337
338
        if is_zero_digest(digest.borrow()) {
339
            let size = reader.drain().await?;
340
            UploadSizeInfo::ExactSize(0).validate_size(size)?;
341
            upload_size.validate_size(size)?;
342
            return Ok(0);
343
        }
344
345
        // Sanity check S3 maximum upload size.
346
        if max_size > MAX_UPLOAD_SIZE {
347
            return Err(make_err!(
348
                Code::FailedPrecondition,
349
                "File size exceeds max of {MAX_UPLOAD_SIZE}"
350
            ));
351
        }
352
353
        // Note(aaronmondal) It might be more optimal to use a different
354
        // heuristic here, but for simplicity we use a hard coded value.
355
        // Anything going down this if-statement will have the advantage of only
356
        // 1 network request for the upload instead of minimum of 3 required for
357
        // multipart upload requests.
358
        //
359
        // Note(aaronmondal) If the upload size is not known, we go down the multipart upload path.
360
        // This is not very efficient, but it greatly reduces the complexity of the code.
361
        if max_size < MIN_MULTIPART_SIZE && matches!(upload_size, UploadSizeInfo::ExactSize(_)) {
362
            let UploadSizeInfo::ExactSize(sz) = upload_size else {
363
                unreachable!("upload_size must be UploadSizeInfo::ExactSize here");
364
            };
365
            reader.set_max_recent_data_size(
366
                u64::try_from(self.max_retry_buffer_per_request)
367
                    .err_tip(|| "Could not convert max_retry_buffer_per_request to u64")?,
368
            );
369
            return self
370
                .retrier
371
8
                .retry(unfold(reader, move |mut reader| async move {
372
8
                    let content = match reader
373
8
                        .consume(Some(usize::try_from(sz).unwrap_or(usize::MAX).saturating_add(1)))
374
8
                        .await
375
                    {
376
8
                        Ok(content) => content,
377
0
                        Err(err) => return Some((RetryResult::Err(err), reader)),
378
                    };
379
8
                    if let Err(
err3
) = upload_size.validate_size(content.len() as u64) {
380
3
                        return Some((RetryResult::Err(err), reader));
381
5
                    }
382
5
                    let (mut tx, rx) = make_buf_channel_pair();
383
5
                    if !content.is_empty()
384
5
                        && let Err(
err0
) = tx.send(content).await
385
                    {
386
0
                        return Some((RetryResult::Err(err), reader));
387
5
                    }
388
5
                    if let Err(
err0
) = tx.send_eof() {
389
0
                        return Some((RetryResult::Err(err), reader));
390
5
                    }
391
5
                    let result = self.s3_client
392
5
                        .put_object()
393
5
                        .bucket(&self.bucket)
394
5
                        .key(s3_path.clone())
395
5
                        .content_length(sz.try_into().unwrap_or(i64::MAX))
396
5
                        .body(ByteStream::from_body_1_x(BodyWrapper { reader: rx, size: sz }))
397
5
                        .send()
398
5
                        .map_ok_or_else(|e| Err(
Error::from_std_err0
(
Code::Aborted0
,
&e0
)), |_| Ok(sz))
399
5
                        .await;
400
401
                    // If we failed to upload the file, check to see if we can retry.
402
5
                    let retry_result = result.map_or_else(|mut err| 
{0
403
                        // Ensure our code is Code::Aborted, so the client can retry if possible.
404
0
                        err.code = Code::Aborted;
405
0
                        let bytes_received = reader.get_bytes_received();
406
0
                        if let Err(try_reset_err) = reader.try_reset_stream() {
407
0
                            error!(
408
                                ?bytes_received,
409
                                err = ?try_reset_err,
410
                                "Unable to reset stream after failed upload in S3Store::update"
411
                            );
412
0
                            return RetryResult::Err(err
413
0
                                .merge(try_reset_err)
414
0
                                .append(format!("Failed to retry upload with {bytes_received} bytes received in S3Store::update")));
415
0
                        }
416
0
                        let err = err.append(format!("Retry on upload happened with {bytes_received} bytes received in S3Store::update"));
417
0
                        info!(
418
                            ?err,
419
                            ?bytes_received,
420
                            "Retryable S3 error"
421
                        );
422
0
                        RetryResult::Retry(err)
423
0
                    }, RetryResult::Ok);
424
5
                    Some((retry_result, reader))
425
16
                }))
426
                .await;
427
        }
428
429
        let upload_id = &self
430
            .retrier
431
4
            .retry(unfold((), move |()| async move {
432
4
                let retry_result = self
433
4
                    .s3_client
434
4
                    .create_multipart_upload()
435
4
                    .bucket(&self.bucket)
436
4
                    .key(s3_path)
437
4
                    .send()
438
4
                    .await
439
4
                    .map_or_else(
440
0
                        |e| {
441
0
                            RetryResult::Retry(
442
0
                                Error::from_std_err(Code::Aborted, &e)
443
0
                                    .append("Failed to create multipart upload to s3"),
444
0
                            )
445
0
                        },
446
4
                        |CreateMultipartUploadOutput { upload_id, .. }| {
447
4
                            upload_id.map_or_else(
448
0
                                || {
449
0
                                    RetryResult::Err(make_err!(
450
0
                                        Code::Internal,
451
0
                                        "Expected upload_id to be set by s3 response"
452
0
                                    ))
453
0
                                },
454
                                RetryResult::Ok,
455
                            )
456
4
                        },
457
                    );
458
4
                Some((retry_result, ()))
459
8
            }))
460
            .await?;
461
462
        // S3 requires us to upload in parts if the size is greater than 5GB. The part size must be at least
463
        // 5MB (except last part) and can have up to 10,000 parts.
464
465
        // Calculate of number of chunks if we upload in 5MB chucks (min chunk size), clamping to
466
        // 10,000 parts and correcting for lossy integer division. This provides the
467
        let chunk_count = (max_size / MIN_MULTIPART_SIZE).clamp(0, MAX_UPLOAD_PARTS - 1) + 1;
468
469
        // Using clamped first approximation of number of chunks, calculate byte count of each
470
        // chunk, excluding last chunk, clamping to min/max upload size 5MB, 5GB.
471
        let bytes_per_upload_part =
472
            (max_size / chunk_count).clamp(MIN_MULTIPART_SIZE, MAX_MULTIPART_SIZE);
473
474
        // Sanity check before continuing.
475
        if !(MIN_MULTIPART_SIZE..MAX_MULTIPART_SIZE).contains(&bytes_per_upload_part) {
476
            return Err(make_err!(
477
                Code::FailedPrecondition,
478
                "Failed to calculate file chuck size (min, max, calc): {MIN_MULTIPART_SIZE}, {MAX_MULTIPART_SIZE}, {bytes_per_upload_part}",
479
            ));
480
        }
481
482
4
        let upload_parts = move || async move {
483
            // This will ensure we only have `multipart_max_concurrent_uploads` * `bytes_per_upload_part`
484
            // bytes in memory at any given time waiting to be uploaded.
485
4
            let (tx, mut rx) = mpsc::channel(self.multipart_max_concurrent_uploads);
486
487
4
            let read_stream_fut = async move {
488
4
                let retrier = &Pin::get_ref(self).retrier;
489
4
                let mut total_uploaded = 0;
490
                // Note: Our break condition is when we reach EOF.
491
12
                for part_number in 
1..i32::MAX4
{
492
12
                    let write_buf = reader
493
12
                        .consume(Some(usize::try_from(bytes_per_upload_part).err_tip(
494
                            || "Could not convert bytes_per_upload_part to usize",
495
0
                        )?))
496
12
                        .await
497
12
                        .err_tip(|| "Failed to read chunk in s3_store")
?0
;
498
12
                    if write_buf.is_empty() {
499
4
                        break; // Reached EOF.
500
8
                    }
501
502
8
                    total_uploaded += write_buf.len() as u64;
503
504
8
                    tx.send(retrier.retry(unfold(write_buf, move |write_buf| {
505
8
                        async move {
506
8
                            let retry_result = self
507
8
                                .s3_client
508
8
                                .upload_part()
509
8
                                .bucket(&self.bucket)
510
8
                                .key(s3_path)
511
8
                                .upload_id(upload_id)
512
8
                                .body(ByteStream::new(SdkBody::from(write_buf.clone())))
513
8
                                .part_number(part_number)
514
8
                                .send()
515
8
                                .await
516
8
                                .map_or_else(
517
0
                                    |e| {
518
0
                                        RetryResult::Retry(
519
0
                                            Error::from_std_err(Code::Aborted, &e).append(format!(
520
0
                                                "Failed to upload part {part_number} in S3 store"
521
0
                                            )),
522
0
                                        )
523
0
                                    },
524
8
                                    |mut response| {
525
8
                                        RetryResult::Ok(
526
8
                                            CompletedPartBuilder::default()
527
8
                                                // Only set an entity tag if it exists. This saves
528
8
                                                // 13 bytes per part on the final request if it can
529
8
                                                // omit the `<ETAG><ETAG/>` string.
530
8
                                                .set_e_tag(response.e_tag.take())
531
8
                                                .part_number(part_number)
532
8
                                                .build(),
533
8
                                        )
534
8
                                    },
535
                                );
536
8
                            Some((retry_result, write_buf))
537
8
                        }
538
8
                    })))
539
8
                    .await
540
8
                    .map_err(|err| 
{0
541
0
                        Error::from_std_err(Code::Internal, &err)
542
0
                            .append("Failed to send part to channel in s3_store")
543
0
                    })?;
544
                }
545
4
                Result::<_, Error>::Ok(total_uploaded)
546
4
            }
547
4
            .fuse();
548
549
4
            let mut upload_futures = FuturesUnordered::new();
550
4
            let mut total_uploaded = 0;
551
552
4
            let mut completed_parts = Vec::with_capacity(
553
4
                usize::try_from(cmp::min(MAX_UPLOAD_PARTS, chunk_count))
554
4
                    .err_tip(|| "Could not convert u64 to usize")
?0
,
555
            );
556
4
            tokio::pin!(read_stream_fut);
557
            loop {
558
24
                if read_stream_fut.is_terminated() && 
rx20
.
is_empty20
() &&
upload_futures7
.
is_empty7
() {
559
4
                    break; // No more data to process.
560
20
                }
561
20
                tokio::select! {
562
20
                    
result4
= &mut read_stream_fut => {
563
4
                        total_uploaded = result
?0
;
564
                    }, // Return error or wait for other futures.
565
20
                    Some(
upload_result8
) = upload_futures.next() =>
completed_parts8
.
push8
(
upload_result8
?0
),
566
20
                    Some(
fut8
) = rx.recv() =>
upload_futures8
.
push8
(
fut8
),
567
                }
568
            }
569
570
4
            upload_size.validate_size(total_uploaded)
?1
;
571
572
            // Even though the spec does not require parts to be sorted by number, we do it just in case
573
            // there's an S3 implementation that requires it.
574
3
            completed_parts.sort_unstable_by_key(|part| part.part_number);
575
576
3
            self.retrier
577
3
                .retry(unfold(completed_parts, move |completed_parts| async move {
578
                    Some((
579
3
                        self.s3_client
580
3
                            .complete_multipart_upload()
581
3
                            .bucket(&self.bucket)
582
3
                            .key(s3_path)
583
3
                            .multipart_upload(
584
3
                                CompletedMultipartUploadBuilder::default()
585
3
                                    .set_parts(Some(completed_parts.clone()))
586
3
                                    .build(),
587
3
                            )
588
3
                            .upload_id(upload_id)
589
3
                            .send()
590
3
                            .await
591
3
                            .map_or_else(
592
0
                                |e| {
593
0
                                    RetryResult::Retry(
594
0
                                        Error::from_std_err(Code::Aborted, &e).append(
595
0
                                            "Failed to complete multipart upload in S3 store",
596
0
                                        ),
597
0
                                    )
598
0
                                },
599
3
                                |_| RetryResult::Ok(total_uploaded),
600
                            ),
601
3
                        completed_parts,
602
                    ))
603
6
                }))
604
3
                .await
605
8
        };
606
        // Upload our parts and complete the multipart upload.
607
        // If we fail attempt to abort the multipart upload (cleanup).
608
        upload_parts()
609
1
            .or_else(move |mut e| async move {
610
1
                let abort_res = self
611
1
                    .s3_client
612
1
                    .abort_multipart_upload()
613
1
                    .bucket(&self.bucket)
614
1
                    .key(s3_path)
615
1
                    .upload_id(upload_id)
616
1
                    .send()
617
1
                    .await;
618
1
                if let Err(
abort_err0
) = abort_res {
619
0
                    let err = Error::from_std_err(Code::Aborted, &abort_err)
620
0
                        .append("Failed to abort multipart upload in S3 store");
621
0
                    info!(?err, "Multipart upload error");
622
0
                    e = e.merge(err);
623
1
                }
624
1
                Err(e)
625
2
            })
626
            .await
627
15
    }
628
629
    async fn get_part(
630
        self: Pin<&Self>,
631
        key: StoreKey<'_>,
632
        writer: &mut DropCloserWriteHalf,
633
        offset: u64,
634
        length: Option<u64>,
635
8
    ) -> Result<(), Error> {
636
        if is_zero_digest(key.borrow()) {
637
            writer
638
                .send_eof()
639
                .err_tip(|| "Failed to send zero EOF in filesystem store get_part")?;
640
            return Ok(());
641
        }
642
643
        let s3_path = &self.make_s3_path(&key);
644
        let end_read_byte = length
645
3
            .map_or(Some(None), |length| Some(offset.checked_add(length)))
646
            .err_tip(|| "Integer overflow protection triggered")?;
647
648
        self.retrier
649
8
            .retry(unfold(writer, move |writer| async move {
650
8
                let result = self
651
8
                    .s3_client
652
8
                    .get_object()
653
8
                    .bucket(&self.bucket)
654
8
                    .key(s3_path)
655
8
                    .range(format!(
656
                        "bytes={}-{}",
657
8
                        offset + writer.get_bytes_written(),
658
8
                        end_read_byte.map_or_else(String::new, |v| 
v3
.
to_string3
())
659
                    ))
660
8
                    .send()
661
8
                    .await;
662
663
8
                let 
mut s3_in_stream6
= match result {
664
6
                    Ok(head_object_output) => head_object_output.body,
665
2
                    Err(sdk_error) => match sdk_error.into_service_error() {
666
1
                        GetObjectError::NoSuchKey(e) => {
667
1
                            return Some((
668
1
                                RetryResult::Err(
669
1
                                    Error::from_std_err(Code::NotFound, &e)
670
1
                                        .append("No such key in S3"),
671
1
                                ),
672
1
                                writer,
673
1
                            ));
674
                        }
675
1
                        other => {
676
1
                            return Some((
677
1
                                RetryResult::Retry(
678
1
                                    Error::from_std_err(Code::Unavailable, &other)
679
1
                                        .append("Unhandled GetObjectError in S3"),
680
1
                                ),
681
1
                                writer,
682
1
                            ));
683
                        }
684
                    },
685
                };
686
687
                // Copy data from s3 input stream to the writer stream.
688
11
                while let Some(
maybe_bytes5
) = s3_in_stream.next().await {
689
5
                    match maybe_bytes {
690
5
                        Ok(bytes) => {
691
5
                            if bytes.is_empty() {
692
                                // Ignore possible EOF. Different implementations of S3 may or may not
693
                                // send EOF this way.
694
1
                                continue;
695
4
                            }
696
4
                            if let Err(
e0
) = writer.send(bytes).await {
697
0
                                return Some((
698
0
                                    RetryResult::Err(
699
0
                                        Error::from_std_err(Code::Aborted, &e)
700
0
                                            .append("Error sending bytes to consumer in S3"),
701
0
                                    ),
702
0
                                    writer,
703
0
                                ));
704
4
                            }
705
                        }
706
0
                        Err(e) => {
707
0
                            return Some((
708
0
                                RetryResult::Retry(
709
0
                                    Error::from_std_err(Code::Aborted, &e)
710
0
                                        .append("Bad bytestream element in S3"),
711
0
                                ),
712
0
                                writer,
713
0
                            ));
714
                        }
715
                    }
716
                }
717
6
                if let Err(
e0
) = writer.send_eof() {
718
0
                    return Some((
719
0
                        RetryResult::Err(
720
0
                            Error::from_std_err(Code::Aborted, &e)
721
0
                                .append("Failed to send EOF to consumer in S3"),
722
0
                        ),
723
0
                        writer,
724
0
                    ));
725
6
                }
726
6
                Some((RetryResult::Ok(()), writer))
727
16
            }))
728
            .await
729
8
    }
730
731
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &'_ dyn StoreDriver {
732
0
        self
733
0
    }
734
735
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
736
0
        self
737
0
    }
738
739
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
740
0
        self
741
0
    }
742
743
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
744
0
        registry.register_indicator(self);
745
0
    }
746
747
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
748
0
        self.remove_callbacks.lock().push(callback);
749
0
        Ok(())
750
0
    }
751
}
752
753
#[async_trait]
754
impl<I, NowFn> HealthStatusIndicator for S3Store<NowFn>
755
where
756
    I: InstantWrapper,
757
    NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
758
{
759
0
    fn get_name(&self) -> &'static str {
760
0
        "S3Store"
761
0
    }
762
763
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
764
        StoreDriver::check_health(Pin::new(self), namespace).await
765
0
    }
766
}