Skip to content

Commit 7397f2f

Browse files
authored
fix the try-trim logic (#621)
1 parent 8eb717a commit 7397f2f

3 files changed

Lines changed: 105 additions & 40 deletions

File tree

source/s3_default_buffer_pool.c

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -434,26 +434,27 @@ static void s_aws_ticket_wrapper_destroy(void *data) {
434434
if (ticket->reserved_from == AWS_S3_BUFFER_POOL_RESERVED_FROM_SPECIAL) {
435435
struct aws_hash_element *elem = NULL;
436436
aws_hash_table_find(&buffer_pool->special_blocks, (void *)ticket->size, &elem);
437-
/* TODO: the lifetime between the ticket and the special block, let's assume the block will always be
438-
* available first. */
439-
AWS_FATAL_ASSERT(elem != NULL);
440-
special_list = elem->value;
441-
442-
bool found = false;
443-
for (size_t i = 0; i < aws_array_list_length(&special_list->blocks); ++i) {
444-
struct s3_buffer_pool_block *block;
445-
aws_array_list_get_at_ptr(&special_list->blocks, (void **)&block, i);
446-
if (block->block_ptr == ticket->ptr) {
447-
/* Make sure the block is marked as used. */
448-
AWS_FATAL_ASSERT(block->alloc_bit_mask == UINT16_MAX);
449-
block->alloc_bit_mask = 0;
450-
found = true;
451-
break;
437+
if (elem) {
438+
special_list = elem->value;
439+
440+
bool found = false;
441+
for (size_t i = 0; i < aws_array_list_length(&special_list->blocks); ++i) {
442+
struct s3_buffer_pool_block *block;
443+
aws_array_list_get_at_ptr(&special_list->blocks, (void **)&block, i);
444+
if (block->block_ptr == ticket->ptr) {
445+
/* Make sure the block is marked as used. */
446+
if (block->alloc_bit_mask == UINT16_MAX) {
447+
block->alloc_bit_mask = 0;
448+
found = true;
449+
break;
450+
}
451+
}
452+
}
453+
/* Make sure we do find where the ticket buffer from the list. */
454+
if (found) {
455+
buffer_pool->special_blocks_used -= ticket->size;
452456
}
453457
}
454-
/* Make sure we do find where the ticket buffer from the list. */
455-
AWS_FATAL_ASSERT(found);
456-
buffer_pool->special_blocks_used -= ticket->size;
457458
} else if (ticket->reserved_from == AWS_S3_BUFFER_POOL_RESERVED_FROM_PRIMARY) {
458459

459460
size_t chunks_used = ticket->size / buffer_pool->chunk_size;
@@ -557,37 +558,31 @@ static void s_aws_ticket_wrapper_destroy(void *data) {
557558
static bool s_should_trim_for_reserve_synced(
558559
bool from_special,
559560
size_t to_reserve,
560-
struct aws_s3_default_buffer_pool *buffer_pool) {
561+
struct aws_s3_default_buffer_pool *buffer_pool,
562+
size_t overall_taken) {
561563
if (from_special || to_reserve <= buffer_pool->primary_size_cutoff) {
562564
/* Only trim when it will be allocated from secondary, which is directly from malloc. */
563565
return false;
564566
}
565-
/* The tracked usage SHOULD NOT overflow. */
566-
size_t total_allocated =
567-
buffer_pool->primary_allocated + buffer_pool->secondary_used + buffer_pool->special_blocks_allocated;
567+
568568
size_t total_allocation_needs = 0;
569-
if (aws_add_size_checked(total_allocated, to_reserve, &total_allocation_needs)) {
570-
/* Will overflow, trim it. */
571-
return true;
569+
if (aws_add_size_checked(overall_taken, to_reserve, &total_allocation_needs)) {
570+
/* Will overflow, ignores it, which should not happen. */
571+
return false;
572572
}
573573
if (total_allocation_needs < buffer_pool->mem_limit) {
574574
/* No need to trim as we still have space to allocate the new block. */
575575
return false;
576576
}
577577

578-
size_t primary_overallocation = 0;
579-
int overflow = 0;
580-
overflow |=
581-
aws_sub_size_checked(buffer_pool->primary_allocated, buffer_pool->primary_used, &primary_overallocation);
582-
overflow |= aws_sub_size_checked(primary_overallocation, buffer_pool->primary_reserved, &primary_overallocation);
583-
size_t special_overallocation = 0;
584-
overflow |= aws_sub_size_checked(
585-
buffer_pool->special_blocks_allocated, buffer_pool->special_blocks_used, &special_overallocation);
586-
overflow |=
587-
aws_sub_size_checked(special_overallocation, buffer_pool->special_blocks_reserved, &special_overallocation);
588-
/* The already allocated should be reasonable and not cause overflow. Otherwise, bugs in the code. */
589-
AWS_FATAL_ASSERT(!overflow);
590-
/* Use max if overflow */
578+
size_t primary_overallocation = aws_sub_size_saturating(buffer_pool->primary_allocated, buffer_pool->primary_used);
579+
/* Reserved can be more tha allocated */
580+
primary_overallocation = aws_sub_size_saturating(primary_overallocation, buffer_pool->primary_reserved);
581+
size_t special_overallocation =
582+
aws_sub_size_saturating(buffer_pool->special_blocks_allocated, buffer_pool->special_blocks_used);
583+
/* Reserved can be more tha allocated */
584+
special_overallocation = aws_sub_size_saturating(special_overallocation, buffer_pool->special_blocks_reserved);
585+
591586
size_t total_overallocation = aws_add_size_saturating(special_overallocation, primary_overallocation);
592587
if (total_overallocation < to_reserve) {
593588
/* If the overallocation is less than the new block, trim it won't help, skip trimming. */
@@ -616,7 +611,7 @@ struct aws_s3_default_buffer_ticket *s_try_reserve_synced(
616611
* the blocks, trim the blocks in hopes we can free up enough memory.
617612
* TODO: something smarter, like partial trim?
618613
*/
619-
if (s_should_trim_for_reserve_synced(from_special, meta.size, buffer_pool)) {
614+
if (s_should_trim_for_reserve_synced(from_special, meta.size, buffer_pool, overall_taken)) {
620615
s_buffer_pool_trim_synced(buffer_pool);
621616
overall_taken = buffer_pool->primary_used + buffer_pool->primary_reserved + buffer_pool->secondary_used +
622617
buffer_pool->secondary_reserved + buffer_pool->special_blocks_reserved +
@@ -627,7 +622,7 @@ struct aws_s3_default_buffer_ticket *s_try_reserve_synced(
627622
const size_t max_impact_of_forced_on_limit =
628623
(size_t)(buffer_pool->mem_limit * (s_max_impact_of_forced_buffers_on_memory_limit_as_percentage / 100.0));
629624
if (buffer_pool->forced_used > max_impact_of_forced_on_limit) {
630-
overall_taken -= buffer_pool->forced_used - max_impact_of_forced_on_limit;
625+
overall_taken = (overall_taken - buffer_pool->forced_used) + max_impact_of_forced_on_limit;
631626
}
632627

633628
if ((meta.size + overall_taken) <= buffer_pool->mem_limit) {

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ add_test_case(test_s3_buffer_pool_special_size_with_limits)
448448
add_test_case(test_s3_buffer_pool_special_size_mixed)
449449
add_test_case(test_s3_buffer_pool_special_size_trim)
450450
add_test_case(test_s3_buffer_pool_reserve_tiny_chunks)
451+
add_test_case(test_s3_buffer_pool_trim_reserved_but_unallocated)
451452

452453
add_net_test_case(client_update_upload_part_timeout)
453454
add_net_test_case(client_meta_request_override_part_size)

tests/s3_default_buffer_pool_tests.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,72 @@ static int s_test_s3_buffer_pool_reserve_tiny_chunks(struct aws_allocator *alloc
679679
return 0;
680680
}
681681
AWS_TEST_CASE(test_s3_buffer_pool_reserve_tiny_chunks, s_test_s3_buffer_pool_reserve_tiny_chunks)
682+
683+
/*
684+
* Regression test for s_should_trim_for_reserve_synced when buffers are reserved but not yet claimed.
685+
*
686+
* When tickets are reserved but never claimed, primary_allocated stays 0 while primary_reserved > 0.
687+
* The overallocation calculation (primary_allocated - primary_used - primary_reserved) must not
688+
* underflow -- it should saturate to 0. This also verifies that a subsequent secondary reserve
689+
* still succeeds (i.e. the function correctly returns false / no crash).
690+
*/
691+
static int s_test_s3_buffer_pool_trim_reserved_but_unallocated(struct aws_allocator *allocator, void *ctx) {
692+
(void)ctx;
693+
const size_t chunk_size = MB_TO_BYTES(8);
694+
const size_t mem_limit = GB_TO_BYTES(1);
695+
struct aws_s3_buffer_pool *buffer_pool = aws_s3_default_buffer_pool_new(
696+
allocator,
697+
(struct aws_s3_buffer_pool_config){
698+
.part_size = chunk_size,
699+
.memory_limit = mem_limit,
700+
});
701+
ASSERT_NOT_NULL(buffer_pool);
702+
703+
/* Reserve primary-sized tickets WITHOUT claiming them.
704+
* This leaves primary_reserved > 0 but primary_allocated == 0. */
705+
const size_t num_reserved = 50;
706+
struct aws_future_s3_buffer_ticket *reserved_futures[50];
707+
struct aws_s3_buffer_ticket *reserved_tickets[50];
708+
for (size_t i = 0; i < num_reserved; ++i) {
709+
reserved_futures[i] = aws_s3_default_buffer_pool_reserve(
710+
buffer_pool, (struct aws_s3_buffer_pool_reserve_meta){.size = chunk_size});
711+
ASSERT_TRUE(aws_future_s3_buffer_ticket_is_done(reserved_futures[i]));
712+
ASSERT_INT_EQUALS(aws_future_s3_buffer_ticket_get_error(reserved_futures[i]), AWS_OP_SUCCESS);
713+
reserved_tickets[i] = aws_future_s3_buffer_ticket_get_result_by_move(reserved_futures[i]);
714+
ASSERT_NOT_NULL(reserved_tickets[i]);
715+
/* Intentionally do NOT call aws_s3_buffer_ticket_claim() -- ticket stays in reserve stage. */
716+
}
717+
718+
/* Sanity check: reserved > 0, allocated == 0 */
719+
struct aws_s3_default_buffer_pool_usage_stats stats = aws_s3_default_buffer_pool_get_usage(buffer_pool);
720+
ASSERT_TRUE(stats.primary_reserved > 0);
721+
ASSERT_UINT_EQUALS(0, stats.primary_allocated);
722+
723+
/* Now reserve a large secondary buffer. This triggers s_should_trim_for_reserve_synced.
724+
* The overallocation calculation must saturate to 0 (not crash/underflow).
725+
* The reserve must succeed since mem_limit is not exhausted. */
726+
struct aws_future_s3_buffer_ticket *secondary_future = aws_s3_default_buffer_pool_reserve(
727+
buffer_pool,
728+
(struct aws_s3_buffer_pool_reserve_meta){
729+
.size = MB_TO_BYTES(800),
730+
});
731+
ASSERT_NOT_NULL(secondary_future);
732+
/* Release all the reserved ones. */
733+
for (size_t i = 0; i < num_reserved; ++i) {
734+
aws_s3_buffer_ticket_release(reserved_tickets[i]);
735+
aws_future_s3_buffer_ticket_release(reserved_futures[i]);
736+
}
737+
ASSERT_TRUE(aws_future_s3_buffer_ticket_is_done(secondary_future));
738+
ASSERT_INT_EQUALS(aws_future_s3_buffer_ticket_get_error(secondary_future), AWS_OP_SUCCESS);
739+
struct aws_s3_buffer_ticket *secondary_ticket = aws_future_s3_buffer_ticket_get_result_by_move(secondary_future);
740+
ASSERT_NOT_NULL(secondary_ticket);
741+
struct aws_byte_buf buf = aws_s3_buffer_ticket_claim(secondary_ticket);
742+
ASSERT_NOT_NULL(buf.buffer);
743+
744+
/* Cleanup */
745+
aws_s3_buffer_ticket_release(secondary_ticket);
746+
aws_future_s3_buffer_ticket_release(secondary_future);
747+
aws_s3_default_buffer_pool_destroy(buffer_pool);
748+
return 0;
749+
}
750+
AWS_TEST_CASE(test_s3_buffer_pool_trim_reserved_but_unallocated, s_test_s3_buffer_pool_trim_reserved_but_unallocated)

0 commit comments

Comments
 (0)