Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions include/aws/s3/private/s3_auto_ranged_get.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ struct aws_s3_auto_ranged_get {

uint32_t initial_message_has_range_header : 1;
uint32_t initial_message_has_if_match_header : 1;

/* If true, parts will be fetched in a shuffled order instead of sequentially. */
bool randomize_part_order;
/* Shuffled array of part numbers [1..total_num_parts], allocated after total_num_parts is known. */
uint32_t *shuffled_part_order;
};

AWS_EXTERN_C_BEGIN
Expand All @@ -89,6 +94,13 @@ AWS_S3_API struct aws_s3_meta_request *aws_s3_meta_request_auto_ranged_get_new(
bool part_size_set,
const struct aws_s3_meta_request_options *options);

/**
* Shuffle a uint32_t array in place using Fisher-Yates.
* Returns AWS_OP_SUCCESS, or an error if the random source is unavailable.
*/
AWS_S3_API
int aws_s3_shuffle_uint32_array(uint32_t *array, uint32_t count);

AWS_EXTERN_C_END

#endif /* AWS_S3_AUTO_RANGED_GET_H */
10 changes: 10 additions & 0 deletions include/aws/s3/s3_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,16 @@ struct aws_s3_meta_request_options {
*/
bool force_dynamic_part_size;

/**
* Optional.
* If set to true, the auto-ranged GET will fetch parts in a randomized (shuffled) order instead of
* sequentially. The delivery order to the caller is unaffected—parts are still delivered in sequential
* order by the streaming layer. This can improve throughput when many consumers download the same
* object concurrently, by reducing contention on sequential byte ranges in caching layers.
* Only applies to AWS_S3_META_REQUEST_TYPE_GET_OBJECT.
*/
bool randomize_get_part_order;

/**
* Optional.
* The size threshold in bytes for when to use multipart uploads.
Expand Down
79 changes: 78 additions & 1 deletion source/s3_auto_ranged_get.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "aws/s3/private/s3_meta_request_impl.h"
#include "aws/s3/private/s3_request_messages.h"
#include "aws/s3/private/s3_util.h"
#include <aws/common/device_random.h>
#include <aws/common/string.h>
#include <inttypes.h>

Expand All @@ -19,6 +20,23 @@ const uint64_t s_min_size_response_for_pooling = 1 * 1024 * 1024;
const uint32_t s_conservative_max_requests_in_flight = 8;
const struct aws_byte_cursor g_application_xml_value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("application/xml");

int aws_s3_shuffle_uint32_array(uint32_t *array, uint32_t count) {
if (count <= 1) {
return AWS_OP_SUCCESS;
}
for (uint32_t i = count - 1; i > 0; --i) {
uint64_t rand_val = 0;
if (aws_device_random_u64(&rand_val)) {
return aws_raise_error(aws_last_error());
}
uint32_t j = (uint32_t)(rand_val % ((uint64_t)i + 1));
uint32_t tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
return AWS_OP_SUCCESS;
}

static void s_s3_meta_request_auto_ranged_get_destroy(struct aws_s3_meta_request *meta_request);

static bool s_s3_auto_ranged_get_update(
Expand Down Expand Up @@ -94,6 +112,7 @@ struct aws_s3_meta_request *aws_s3_meta_request_auto_ranged_get_new(

auto_ranged_get->part_size_set = part_size_set;
auto_ranged_get->force_dynamic_part_size = options->force_dynamic_part_size;
auto_ranged_get->randomize_part_order = options->randomize_get_part_order;
struct aws_http_headers *headers = aws_http_message_get_headers(auto_ranged_get->base.initial_request_message);
AWS_ASSERT(headers != NULL);

Expand Down Expand Up @@ -136,6 +155,7 @@ static void s_s3_meta_request_auto_ranged_get_destroy(struct aws_s3_meta_request

struct aws_s3_auto_ranged_get *auto_ranged_get = meta_request->impl;
aws_string_destroy(auto_ranged_get->etag);
aws_mem_release(meta_request->allocator, auto_ranged_get->shuffled_part_order);
aws_mem_release(meta_request->allocator, auto_ranged_get);
}

Expand Down Expand Up @@ -173,6 +193,12 @@ static enum aws_s3_auto_ranged_get_request_type s_s3_get_request_type_for_discov
: AWS_S3_AUTO_RANGE_GET_REQUEST_TYPE_HEAD_OBJECT;
}

/* When randomizing part order, use HEAD to discover object size without fetching any data.
* This allows all parts (including part 1) to participate in the shuffle. */
if (auto_ranged_get->randomize_part_order) {
return AWS_S3_AUTO_RANGE_GET_REQUEST_TYPE_HEAD_OBJECT;
}

/* If we don't need checksum validation, then discover the size of the object while trying to get the first part. */
if (!meta_request->checksum_config.validate_response_checksum) {
return AWS_S3_AUTO_RANGE_GET_REQUEST_TYPE_GET_OBJECT_WITH_RANGE;
Expand Down Expand Up @@ -363,7 +389,9 @@ static bool s_s3_auto_ranged_get_update(
meta_request,
AWS_S3_AUTO_RANGE_GET_REQUEST_TYPE_GET_OBJECT_WITH_RANGE,
AWS_S3_REQUEST_TYPE_GET_OBJECT,
auto_ranged_get->synced_data.num_parts_requested + 1 /*part_number*/,
auto_ranged_get->shuffled_part_order
? auto_ranged_get->shuffled_part_order[auto_ranged_get->synced_data.num_parts_requested]
: auto_ranged_get->synced_data.num_parts_requested + 1 /*part_number*/,
AWS_S3_REQUEST_FLAG_ALLOCATE_BUFFER_FROM_POOL);

aws_s3_calculate_auto_ranged_get_part_range(
Expand Down Expand Up @@ -970,6 +998,55 @@ static void s_s3_auto_ranged_get_request_finished(
auto_ranged_get->synced_data.first_part_size,
object_range_start,
object_range_end);

/* If randomize_part_order is enabled and there are multiple parts, shuffle the fetch order */
if (auto_ranged_get->randomize_part_order &&
auto_ranged_get->synced_data.total_num_parts > 1) {
uint32_t total = auto_ranged_get->synced_data.total_num_parts;
auto_ranged_get->shuffled_part_order =
aws_mem_calloc(meta_request->allocator, total, sizeof(uint32_t));

if (auto_ranged_get->shuffled_part_order == NULL) {
AWS_LOGF_WARN(
AWS_LS_S3_META_REQUEST,
"id=%p: Failed to allocate shuffled part order array, falling back to sequential.",
(void *)meta_request);
} else {
for (uint32_t i = 0; i < total; ++i) {
auto_ranged_get->shuffled_part_order[i] = i + 1;
}
/* Shuffle within adjacent windows of s_conservative_max_requests_in_flight parts.
* This ensures delivery can always make sequential progress within the pipeline's
* buffering capacity, while still randomizing fetch order across concurrent consumers. */
bool shuffle_failed = false;
for (uint32_t window_start = 0; window_start < total;
window_start += s_conservative_max_requests_in_flight) {
uint32_t window_size = s_conservative_max_requests_in_flight;
if (window_start + window_size >= total) {
window_size = total - window_start;
}
if (aws_s3_shuffle_uint32_array(
&auto_ranged_get->shuffled_part_order[window_start], window_size)) {
shuffle_failed = true;
break;
}
}
if (shuffle_failed) {
AWS_LOGF_WARN(
AWS_LS_S3_META_REQUEST,
"id=%p: Failed to shuffle part order, falling back to sequential.",
(void *)meta_request);
aws_mem_release(meta_request->allocator, auto_ranged_get->shuffled_part_order);
auto_ranged_get->shuffled_part_order = NULL;
} else {
AWS_LOGF_DEBUG(
AWS_LS_S3_META_REQUEST,
"id=%p: Shuffled part fetch order for %" PRIu32 " parts.",
(void *)meta_request,
total);
}
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ add_net_test_case(test_s3_round_trip_mpu_multipart_get_full_object_checksum_via_
add_net_test_case(test_s3_round_trip_dynamic_range_size_download_multipart)
add_net_test_case(test_s3_round_trip_dynamic_range_size_download_multipart_with_buffer_pool_optimize)
add_net_test_case(test_s3_round_trip_dynamic_range_size_download_single_part)
add_net_test_case(test_s3_get_object_randomize_part_order)
add_net_test_case(test_s3_chunked_then_unchunked)

add_net_test_case(test_s3_cancel_mpu_one_part_completed_fc)
Expand Down Expand Up @@ -417,6 +418,8 @@ add_net_test_case(s3express_client_copy_object_multipart)
add_net_test_case(s3express_hash_key_test)

add_net_test_case(meta_request_auto_ranged_get_new_error_handling)
add_net_test_case(meta_request_auto_ranged_get_randomize_part_order)
add_net_test_case(meta_request_shuffle_edge_cases)
add_net_test_case(meta_request_auto_ranged_put_new_error_handling)
add_net_test_case(bad_request_error_handling)
add_net_test_case(make_meta_request_error_handling)
Expand Down
94 changes: 94 additions & 0 deletions tests/s3_data_plane_tests.c
Original file line number Diff line number Diff line change
Expand Up @@ -5654,6 +5654,100 @@ static int s_test_s3_round_trip_dynamic_range_size_download_single_part(struct a
return 0;
}

AWS_TEST_CASE(test_s3_get_object_randomize_part_order, s_test_s3_get_object_randomize_part_order)
static int s_test_s3_get_object_randomize_part_order(struct aws_allocator *allocator, void *ctx) {
Comment thread
erikfuller marked this conversation as resolved.
(void)ctx;

/* Download a 10MB object using 1MB parts with randomized fetch order.
* Verify the download succeeds and parts were issued non-sequentially. */
const size_t expected_num_parts = 10;

struct aws_s3_tester tester;
ASSERT_SUCCESS(aws_s3_tester_init(allocator, &tester));

struct aws_s3_client *client = NULL;
struct aws_s3_tester_client_options client_options = {.part_size = 1 * 1024 * 1024};
ASSERT_SUCCESS(aws_s3_tester_client_new(&tester, &client_options, &client));

struct aws_s3_meta_request_test_results test_results;
aws_s3_meta_request_test_results_init(&test_results, allocator);

struct aws_s3_tester_meta_request_options get_options = {
.allocator = allocator,
.meta_request_type = AWS_S3_META_REQUEST_TYPE_GET_OBJECT,
.validate_type = AWS_S3_TESTER_VALIDATE_TYPE_EXPECT_SUCCESS,
.client = client,
.part_size = 1 * 1024 * 1024,
.get_options =
{
.object_path = g_pre_existing_object_10MB,
.randomize_get_part_order = true,
},
};
ASSERT_SUCCESS(aws_s3_tester_send_meta_request_with_options(&tester, &get_options, &test_results));
ASSERT_TRUE(test_results.finished_response_status == 200);

/* Extract GET_OBJECT part numbers ordered by when they were actually sent on the wire */
struct {
uint32_t part_number;
uint64_t send_start;
} get_requests[10];
size_t get_count = 0;

size_t num_metrics = aws_array_list_length(&test_results.synced_data.metrics);
for (size_t i = 0; i < num_metrics && get_count < expected_num_parts; ++i) {
struct aws_s3_request_metrics *metrics = NULL;
aws_array_list_get_at(&test_results.synced_data.metrics, (void **)&metrics, i);
enum aws_s3_request_type type = AWS_S3_REQUEST_TYPE_UNKNOWN;
aws_s3_request_metrics_get_request_type(metrics, &type);
if (type != AWS_S3_REQUEST_TYPE_GET_OBJECT) {
continue;
}
aws_s3_request_metrics_get_part_number(metrics, &get_requests[get_count].part_number);
aws_s3_request_metrics_get_send_start_timestamp_ns(metrics, &get_requests[get_count].send_start);
get_count++;
}
ASSERT_UINT_EQUALS(expected_num_parts, get_count);

/* Sort by send timestamp to reconstruct actual issue order */
for (size_t i = 0; i < get_count - 1; ++i) {
for (size_t j = i + 1; j < get_count; ++j) {
if (get_requests[j].send_start < get_requests[i].send_start) {
uint32_t tmp_part = get_requests[i].part_number;
uint64_t tmp_time = get_requests[i].send_start;
get_requests[i].part_number = get_requests[j].part_number;
get_requests[i].send_start = get_requests[j].send_start;
get_requests[j].part_number = tmp_part;
get_requests[j].send_start = tmp_time;
}
}
}

/* All part numbers 1..10 must be present exactly once (ignore index 0) */
bool part_seen[11] = {false};
for (size_t i = 0; i < get_count; ++i) {
ASSERT_TRUE(get_requests[i].part_number >= 1 && get_requests[i].part_number <= expected_num_parts);
ASSERT_FALSE(part_seen[get_requests[i].part_number]);
part_seen[get_requests[i].part_number] = true;
}

/* Parts must NOT have been issued in sequential order (1, 2, 3, ..., 10) */
/* Randomized, this is very unlikely (~1/8! or ~1/40K) */
bool is_sequential = true;
for (size_t i = 1; i < get_count; ++i) {
if (get_requests[i].part_number != get_requests[i - 1].part_number + 1) {
is_sequential = false;
break;
}
}
ASSERT_FALSE(is_sequential);

aws_s3_meta_request_test_results_clean_up(&test_results);
aws_s3_client_release(client);
aws_s3_tester_clean_up(&tester);
return AWS_OP_SUCCESS;
}

/* When streaming, the data will be read after the headers are sent, so, we don't support set the parts level checksum
* via headers when streaming. */
AWS_TEST_CASE(
Expand Down
49 changes: 49 additions & 0 deletions tests/s3_meta_request_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,55 @@ TEST_CASE(meta_request_auto_ranged_get_new_error_handling) {
return AWS_OP_SUCCESS;
}

TEST_CASE(meta_request_auto_ranged_get_randomize_part_order) {
(void)ctx;
(void)allocator;

/* Test: shuffle produces a valid permutation of 20 elements */
uint32_t array[20];
for (uint32_t i = 0; i < 20; ++i) {
array[i] = i + 1;
}

ASSERT_SUCCESS(aws_s3_shuffle_uint32_array(array, 20));

/* Verify all values 1..20 present exactly once */
bool seen[21] = {false};
bool is_sequential = true;
for (uint32_t i = 0; i < 20; ++i) {
ASSERT_TRUE(array[i] >= 1 && array[i] <= 20);
ASSERT_FALSE(seen[array[i]]);
seen[array[i]] = true;
if (array[i] != i + 1) {
is_sequential = false;
}
}
/* With 20 elements, probability of identity permutation is 1/20! ~ 0 */
ASSERT_FALSE(is_sequential);

return AWS_OP_SUCCESS;
}

TEST_CASE(meta_request_shuffle_edge_cases) {
(void)ctx;
(void)allocator;

/* Empty array: no-op */
ASSERT_SUCCESS(aws_s3_shuffle_uint32_array(NULL, 0));

/* Single element: unchanged */
uint32_t single[] = {42};
ASSERT_SUCCESS(aws_s3_shuffle_uint32_array(single, 1));
ASSERT_UINT_EQUALS(42, single[0]);

/* Two elements: valid swap or no swap */
uint32_t pair[] = {1, 2};
ASSERT_SUCCESS(aws_s3_shuffle_uint32_array(pair, 2));
ASSERT_TRUE((pair[0] == 1 && pair[1] == 2) || (pair[0] == 2 && pair[1] == 1));

return AWS_OP_SUCCESS;
}

TEST_CASE(meta_request_auto_ranged_put_new_error_handling) {
(void)ctx;

Expand Down
1 change: 1 addition & 0 deletions tests/s3_tester.c
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,7 @@ int aws_s3_tester_send_meta_request_with_options(
}
meta_request_options.message = message;
meta_request_options.force_dynamic_part_size = options->get_options.force_dynamic_part_size;
meta_request_options.randomize_get_part_order = options->get_options.randomize_get_part_order;
} else if (
meta_request_options.type == AWS_S3_META_REQUEST_TYPE_PUT_OBJECT ||
(meta_request_options.type == AWS_S3_META_REQUEST_TYPE_DEFAULT &&
Expand Down
1 change: 1 addition & 0 deletions tests/s3_tester.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ struct aws_s3_tester_meta_request_options {
/* If larger than 0, create a pre-exist file with the length */
uint64_t pre_exist_file_length;
bool force_dynamic_part_size;
bool randomize_get_part_order;
} get_options;

/* Put Object Meta request specific options. */
Expand Down