Skip to content

Commit caa841c

Browse files
committed
make max_concurrent_chunks for PutObjectStreamRequest configurable
- move concurrent chunk upload implementation to helper function _put_object_stream_chunks_concurrent - add helper function _put_object_stream_chunks_sequential for sequential chunk upload if max_concurrent_chunks is = 1
1 parent b584ce7 commit caa841c

2 files changed

Lines changed: 137 additions & 48 deletions

File tree

s3/src/bucket.rs

Lines changed: 127 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,8 +1598,14 @@ impl Bucket {
15981598
s3_path: &str,
15991599
content_type: &str,
16001600
) -> Result<PutStreamResponse, S3Error> {
1601-
self._put_object_stream_with_content_type_and_headers(reader, s3_path, content_type, None)
1602-
.await
1601+
self._put_object_stream_with_content_type_and_headers(
1602+
reader,
1603+
s3_path,
1604+
content_type,
1605+
None,
1606+
None,
1607+
)
1608+
.await
16031609
}
16041610

16051611
/// Calculate the maximum number of concurrent chunks based on available memory.
@@ -1642,6 +1648,7 @@ impl Bucket {
16421648
s3_path: &str,
16431649
content_type: &str,
16441650
custom_headers: Option<http::HeaderMap>,
1651+
max_concurrent_chunks: Option<std::num::NonZeroUsize>,
16451652
) -> Result<PutStreamResponse, S3Error> {
16461653
// If the file is smaller CHUNK_SIZE, just do a regular upload.
16471654
// Otherwise perform a multi-part upload.
@@ -1675,9 +1682,117 @@ impl Bucket {
16751682
let path = msg.key;
16761683
let upload_id = &msg.upload_id;
16771684

1678-
// Determine max concurrent chunks based on available memory
1679-
let max_concurrent_chunks = Self::calculate_max_concurrent_chunks();
1685+
// use configured max_concurrent_chunks or determine max concurrent chunks based on available memory
1686+
let max_concurrent_chunks = max_concurrent_chunks.map_or_else(
1687+
Self::calculate_max_concurrent_chunks,
1688+
std::num::NonZeroUsize::get,
1689+
);
1690+
1691+
let (total_size, mut etags) = if max_concurrent_chunks == 1 {
1692+
self._put_object_stream_chunks_sequential(
1693+
reader,
1694+
first_chunk,
1695+
&path,
1696+
upload_id,
1697+
content_type,
1698+
)
1699+
.await?
1700+
} else {
1701+
self._put_object_stream_chunks_concurrent(
1702+
reader,
1703+
first_chunk,
1704+
&path,
1705+
upload_id,
1706+
content_type,
1707+
max_concurrent_chunks,
1708+
)
1709+
.await?
1710+
};
1711+
1712+
// Sort etags by part number to ensure correct order
1713+
etags.sort_by_key(|k| k.0);
1714+
let etags: Vec<String> = etags.into_iter().map(|(_, etag)| etag).collect();
1715+
1716+
// Finish the upload
1717+
let inner_data = etags
1718+
.into_iter()
1719+
.enumerate()
1720+
.map(|(i, x)| Part {
1721+
etag: x,
1722+
part_number: i as u32 + 1,
1723+
})
1724+
.collect::<Vec<Part>>();
1725+
let response_data = self
1726+
.complete_multipart_upload(&path, &msg.upload_id, inner_data)
1727+
.await?;
1728+
1729+
Ok(PutStreamResponse::new(
1730+
response_data.status_code(),
1731+
total_size,
1732+
))
1733+
}
1734+
1735+
#[maybe_async::async_impl]
1736+
async fn _put_object_stream_chunks_sequential<R: AsyncRead + Unpin + ?Sized>(
1737+
&self,
1738+
reader: &mut R,
1739+
first_chunk: Vec<u8>,
1740+
path: &str,
1741+
upload_id: &str,
1742+
content_type: &str,
1743+
) -> Result<(usize, Vec<(u32, String)>), S3Error> {
1744+
let mut chunk = first_chunk;
1745+
let mut part_number: u32 = 0;
1746+
let mut total_size = 0;
1747+
let mut etags = Vec::new();
1748+
1749+
loop {
1750+
let chunk_len = chunk.len();
1751+
1752+
if chunk_len == 0 {
1753+
break;
1754+
}
1755+
1756+
part_number += 1;
1757+
total_size += chunk_len;
1758+
1759+
let current_part = part_number;
1760+
let is_last_chunk = chunk_len < CHUNK_SIZE;
1761+
1762+
let response_data = self
1763+
.make_multipart_request(path, chunk, current_part, upload_id, content_type)
1764+
.await?;
1765+
1766+
if !(200..300).contains(&response_data.status_code()) {
1767+
// it chunk upload failed - abort the upload
1768+
return match self.abort_upload(path, upload_id).await {
1769+
Ok(_) => Err(error_from_response_data(response_data)?),
1770+
Err(error) => Err(error),
1771+
};
1772+
}
1773+
1774+
etags.push((current_part, response_data.as_str()?.to_string()));
16801775

1776+
if is_last_chunk {
1777+
break;
1778+
}
1779+
1780+
chunk = crate::utils::read_chunk_async(reader).await?;
1781+
}
1782+
1783+
Ok((total_size, etags))
1784+
}
1785+
1786+
#[maybe_async::async_impl]
1787+
async fn _put_object_stream_chunks_concurrent<R: AsyncRead + Unpin + ?Sized>(
1788+
&self,
1789+
reader: &mut R,
1790+
first_chunk: Vec<u8>,
1791+
path: &str,
1792+
upload_id: &str,
1793+
content_type: &str,
1794+
max_concurrent_chunks: usize,
1795+
) -> Result<(usize, Vec<(u32, String)>), S3Error> {
16811796
// Use FuturesUnordered for bounded parallelism
16821797
use futures_util::FutureExt;
16831798
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -1697,21 +1812,10 @@ impl Bucket {
16971812
reading_done = true;
16981813
}
16991814

1700-
let path_clone = path.clone();
1701-
let upload_id_clone = upload_id.clone();
1702-
let content_type_clone = content_type.to_string();
1703-
let bucket_clone = self.clone();
1704-
17051815
active_uploads.push(
17061816
async move {
1707-
let result = bucket_clone
1708-
.make_multipart_request(
1709-
&path_clone,
1710-
first_chunk,
1711-
1,
1712-
&upload_id_clone,
1713-
&content_type_clone,
1714-
)
1817+
let result = self
1818+
.make_multipart_request(path, first_chunk, 1, upload_id, content_type)
17151819
.await;
17161820
(1, result)
17171821
}
@@ -1738,20 +1842,16 @@ impl Bucket {
17381842
}
17391843

17401844
let current_part = part_number;
1741-
let path_clone = path.clone();
1742-
let upload_id_clone = upload_id.clone();
1743-
let content_type_clone = content_type.to_string();
1744-
let bucket_clone = self.clone();
17451845

17461846
active_uploads.push(
17471847
async move {
1748-
let result = bucket_clone
1848+
let result = self
17491849
.make_multipart_request(
1750-
&path_clone,
1850+
path,
17511851
chunk,
17521852
current_part,
1753-
&upload_id_clone,
1754-
&content_type_clone,
1853+
upload_id,
1854+
content_type,
17551855
)
17561856
.await;
17571857
(current_part, result)
@@ -1765,7 +1865,7 @@ impl Bucket {
17651865
let response_data = result?;
17661866
if !(200..300).contains(&response_data.status_code()) {
17671867
// if chunk upload failed - abort the upload
1768-
match self.abort_upload(&path, upload_id).await {
1868+
match self.abort_upload(path, upload_id).await {
17691869
Ok(_) => {
17701870
return Err(error_from_response_data(response_data)?);
17711871
}
@@ -1781,28 +1881,7 @@ impl Bucket {
17811881
}
17821882
}
17831883

1784-
// Sort etags by part number to ensure correct order
1785-
etags.sort_by_key(|k| k.0);
1786-
let etags: Vec<String> = etags.into_iter().map(|(_, etag)| etag).collect();
1787-
1788-
// Finish the upload
1789-
let inner_data = etags
1790-
.clone()
1791-
.into_iter()
1792-
.enumerate()
1793-
.map(|(i, x)| Part {
1794-
etag: x,
1795-
part_number: i as u32 + 1,
1796-
})
1797-
.collect::<Vec<Part>>();
1798-
let response_data = self
1799-
.complete_multipart_upload(&path, &msg.upload_id, inner_data)
1800-
.await?;
1801-
1802-
Ok(PutStreamResponse::new(
1803-
response_data.status_code(),
1804-
total_size,
1805-
))
1884+
Ok((total_size, etags))
18061885
}
18071886

18081887
#[maybe_async::sync_impl]

s3/src/put_object_request.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ pub struct PutObjectStreamRequest<'a> {
208208
path: String,
209209
content_type: String,
210210
custom_headers: HeaderMap,
211+
max_concurrent_chunks: Option<std::num::NonZeroUsize>,
211212
}
212213

213214
#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
@@ -219,6 +220,7 @@ impl<'a> PutObjectStreamRequest<'a> {
219220
path: path.as_ref().to_string(),
220221
content_type: "application/octet-stream".to_string(),
221222
custom_headers: HeaderMap::new(),
223+
max_concurrent_chunks: None,
222224
}
223225
}
224226

@@ -286,6 +288,12 @@ impl<'a> PutObjectStreamRequest<'a> {
286288
Ok(self)
287289
}
288290

291+
/// Set the maximum number of concurrent chunks for multipart upload, setting it to 0 falls back to the default value based on available memory
292+
pub fn with_max_concurrent_chunks(mut self, max: usize) -> Self {
293+
self.max_concurrent_chunks = std::num::NonZeroUsize::new(max);
294+
self
295+
}
296+
289297
/// Execute the streaming PUT request
290298
#[cfg(feature = "with-tokio")]
291299
pub async fn execute_stream<R: AsyncRead + Unpin + ?Sized>(
@@ -304,6 +312,7 @@ impl<'a> PutObjectStreamRequest<'a> {
304312
} else {
305313
Some(self.custom_headers)
306314
},
315+
self.max_concurrent_chunks,
307316
)
308317
.await
309318
}
@@ -323,6 +332,7 @@ impl<'a> PutObjectStreamRequest<'a> {
323332
} else {
324333
Some(self.custom_headers)
325334
},
335+
self.max_concurrent_chunks,
326336
)
327337
.await
328338
}

0 commit comments

Comments
 (0)