Skip to content

Commit d65aaa4

Browse files
authored
capi: Fix batch size control (#2308)
On db::Buffer::MemoryLimitError the block has already been popped from the block_buffer. Then in the next iteration we would get the next block and the assert SILKWORM_ASSERT(block->header.number == block_number) would fail. For this to work we need to re-insert the failed block in the exception handler.
1 parent ffdc93b commit d65aaa4

File tree

4 files changed

+195
-6
lines changed

4 files changed

+195
-6
lines changed

silkworm/capi/silkworm.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ int silkworm_execute_blocks_ephemeral(SilkwormHandle handle, MDBX_txn* mdbx_txn,
491491
auto signal_check_time{std::chrono::steady_clock::now()};
492492

493493
BlockNum block_number{start_block};
494+
BlockNum batch_start_block_number{start_block};
494495
BlockNum last_block_number = 0;
495496
db::DataModel da_layer{txn};
496497

@@ -522,7 +523,14 @@ int silkworm_execute_blocks_ephemeral(SilkwormHandle handle, MDBX_txn* mdbx_txn,
522523
last_exec_result = block_executor.execute_single(block, state_buffer, analysis_cache, state_pool);
523524
update_execution_progress(execution_progress, block, state_buffer, max_batch_size);
524525
} catch (const db::Buffer::MemoryLimitError&) {
526+
// infinite loop detection, buffer memory limit reached but no progress
527+
if (batch_start_block_number == block_number) {
528+
SILK_ERROR << "Buffer memory limit too small to execute a single block (block_number=" << block_number << ")";
529+
return SILKWORM_INTERNAL_ERROR;
530+
}
531+
525532
// batch done
533+
batch_start_block_number = block_number;
526534
break;
527535
}
528536
if (last_exec_result != ValidationResult::kOk) {
@@ -609,6 +617,7 @@ int silkworm_execute_blocks_perpetual(SilkwormHandle handle, MDBX_env* mdbx_env,
609617

610618
std::optional<Block> block;
611619
BlockNum block_number{start_block};
620+
BlockNum batch_start_block_number{start_block};
612621
BlockNum last_block_number = 0;
613622
AnalysisCache analysis_cache{execution::block::BlockExecutor::kDefaultAnalysisCacheSize};
614623
ObjectPool<evmone::ExecutionState> state_pool;
@@ -619,7 +628,7 @@ int silkworm_execute_blocks_perpetual(SilkwormHandle handle, MDBX_env* mdbx_env,
619628

620629
while (block_number <= max_block) {
621630
while (block_number <= max_block) {
622-
block_buffer.pop_back(&block);
631+
block_buffer.peek_back(&block);
623632
if (!block) {
624633
return SILKWORM_BLOCK_NOT_FOUND;
625634
}
@@ -629,9 +638,17 @@ int silkworm_execute_blocks_perpetual(SilkwormHandle handle, MDBX_env* mdbx_env,
629638
last_exec_result = block_executor.execute_single(*block, state_buffer, analysis_cache, state_pool);
630639
update_execution_progress(execution_progress, *block, state_buffer, max_batch_size);
631640
} catch (const db::Buffer::MemoryLimitError&) {
641+
// infinite loop detection, buffer memory limit reached but no progress
642+
if (batch_start_block_number == block_number) {
643+
SILK_ERROR << "Buffer memory limit too small to execute a single block (block_number=" << block_number << ")";
644+
return SILKWORM_INTERNAL_ERROR;
645+
}
646+
632647
// batch done
648+
batch_start_block_number = block_number;
633649
break;
634650
}
651+
635652
if (last_exec_result != ValidationResult::kOk) {
636653
// firstly, persist the work done so far, then return SILKWORM_INVALID_BLOCK
637654
break;
@@ -643,6 +660,7 @@ int silkworm_execute_blocks_perpetual(SilkwormHandle handle, MDBX_env* mdbx_env,
643660

644661
last_block_number = block_number;
645662
++block_number;
663+
block_buffer.pop_back(&block);
646664
}
647665

648666
StopWatch sw{/*auto_start=*/true};

silkworm/capi/silkworm_test.cpp

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_ephemeral multiple bloc
486486
SilkwormLibrary silkworm_lib{env_path()};
487487

488488
const int chain_id{1};
489-
const uint64_t batch_size{256 * kMebi};
489+
const uint64_t batch_size{3000}; // Small batch size to force multiple iterations
490490
const bool write_change_sets{false}; // We CANNOT write changesets here, TestDatabaseContext db already has them
491491
const bool write_receipts{false}; // We CANNOT write receipts here, TestDatabaseContext db already has them
492492
const bool write_call_traces{false}; // For coherence but don't care
@@ -583,7 +583,7 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple bloc
583583
SilkwormLibrary silkworm_lib{env_path()};
584584

585585
const int chain_id{1};
586-
const uint64_t batch_size{256 * kMebi};
586+
const uint64_t batch_size{3000}; // Small batch size to force multiple iterations
587587
const bool write_change_sets{false}; // We CANNOT write changesets here, TestDatabaseContext db already has them
588588
const bool write_receipts{false}; // We CANNOT write receipts here, TestDatabaseContext db already has them
589589
const bool write_call_traces{false}; // For coherence but don't care
@@ -603,7 +603,7 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple bloc
603603

604604
// Prepare block template (just 1 tx w/ value transfer)
605605
evmc::address from{0x658bdf435d810c91414ec09147daa6db62406379_address}; // funded in genesis
606-
evmc::address to{0x8b299e2b7d7f43c0ce3068263545309ff4ffb521_address}; // untouched address
606+
evmc::address to{0x8b299e2b7d7f43c0ce3068263545309ff4ffb500_address}; // untouched address(es)
607607
intx::uint256 value{1};
608608

609609
Block block{};
@@ -635,6 +635,7 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple bloc
635635
block.transactions.erase(block.transactions.cbegin());
636636
block.transactions.pop_back();
637637
block.transactions[0].nonce++;
638+
block.transactions[0].to->bytes[19]++; // change recipient address to force batch size growth
638639
}
639640

640641
// Execute N blocks using an *internal* txn
@@ -646,16 +647,18 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple bloc
646647

647648
db::ROTxnManaged ro_txn{env};
648649
REQUIRE(db::read_account(ro_txn, to));
649-
CHECK(db::read_account(ro_txn, to)->balance == kBlocks * value);
650+
CHECK(db::read_account(ro_txn, to)->balance == value);
650651
ro_txn.abort();
651652

652653
// Insert N blocks again
654+
block.transactions[0].to = to;
653655
for (size_t i{10 + kBlocks}; i < (10 + 2 * kBlocks); ++i) {
654656
block.header.number = i;
655657
insert_block(env, block);
656658
block.transactions.erase(block.transactions.cbegin());
657659
block.transactions.pop_back();
658660
block.transactions[0].nonce++;
661+
block.transactions[0].to->bytes[19]++; // change recipient address to force batch size growth
659662
}
660663

661664
// Execute N blocks using an *internal* txn, then commit
@@ -667,7 +670,140 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple bloc
667670

668671
ro_txn = db::ROTxnManaged{env};
669672
REQUIRE(db::read_account(ro_txn, to));
670-
CHECK(db::read_account(ro_txn, to)->balance == 2 * kBlocks * value);
673+
CHECK(db::read_account(ro_txn, to)->balance == 2 * value);
674+
}
675+
676+
TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_ephemeral multiple blocks: insufficient buffer memory", "[silkworm][capi]") {
677+
// Use Silkworm as a library with silkworm_init/silkworm_fini automated by RAII
678+
SilkwormLibrary silkworm_lib{env_path()};
679+
680+
const int chain_id{1};
681+
const uint64_t batch_size{170}; // Small batch size to force multiple iterations
682+
const bool write_change_sets{false}; // We CANNOT write changesets here, TestDatabaseContext db already has them
683+
const bool write_receipts{false}; // We CANNOT write receipts here, TestDatabaseContext db already has them
684+
const bool write_call_traces{false}; // For coherence but don't care
685+
686+
auto execute_blocks = [&](auto tx, auto start_block, auto end_block) {
687+
return silkworm_lib.execute_blocks(tx,
688+
chain_id,
689+
start_block,
690+
end_block,
691+
batch_size,
692+
write_change_sets,
693+
write_receipts,
694+
write_call_traces);
695+
};
696+
697+
/* TestDatabaseContext db contains a test chain made up of 9 blocks */
698+
699+
// Prepare block template (just 1 tx w/ value transfer)
700+
evmc::address from{0x658bdf435d810c91414ec09147daa6db62406379_address}; // funded in genesis
701+
evmc::address to{0x8b299e2b7d7f43c0ce3068263545309ff4ffb521_address}; // untouched address
702+
intx::uint256 value{1};
703+
704+
Block block{};
705+
block.header.gas_limit = 5'000'000;
706+
block.header.gas_used = 21'000;
707+
708+
static constexpr auto kEncoder = [](Bytes& dest, const Receipt& r) { rlp::encode(dest, r); };
709+
std::vector<Receipt> receipts{
710+
{TransactionType::kLegacy, true, block.header.gas_used, {}, {}},
711+
};
712+
block.header.receipts_root = trie::root_hash(receipts, kEncoder);
713+
block.transactions.resize(1);
714+
block.transactions[0].to = to;
715+
block.transactions[0].gas_limit = block.header.gas_limit;
716+
block.transactions[0].type = TransactionType::kLegacy;
717+
block.transactions[0].max_priority_fee_per_gas = 0;
718+
block.transactions[0].max_fee_per_gas = 20 * kGiga;
719+
block.transactions[0].value = value;
720+
block.transactions[0].r = 1; // dummy
721+
block.transactions[0].s = 1; // dummy
722+
block.transactions[0].set_sender(from);
723+
724+
constexpr size_t kBlocks{130};
725+
726+
// Insert N blocks
727+
for (size_t i{10}; i < 10 + kBlocks; ++i) {
728+
block.header.number = i;
729+
insert_block(env, block);
730+
block.transactions.erase(block.transactions.cbegin());
731+
block.transactions.pop_back();
732+
block.transactions[0].nonce++;
733+
}
734+
735+
// Execute N blocks using an *external* txn, then commit
736+
db::RWTxnManaged external_txn0{env};
737+
BlockNum start_block{10}, end_block{10 + kBlocks - 1};
738+
const auto result0{execute_blocks(*external_txn0, start_block, end_block)};
739+
CHECK_NOTHROW(external_txn0.commit_and_stop());
740+
CHECK(result0.execute_block_result == SILKWORM_INTERNAL_ERROR);
741+
}
742+
743+
TEST_CASE_METHOD(CApiTest, "CAPI silkworm_execute_blocks_perpetual multiple blocks: insufficient buffer memory", "[silkworm][capi]") {
744+
// Use Silkworm as a library with silkworm_init/silkworm_fini automated by RAII
745+
SilkwormLibrary silkworm_lib{env_path()};
746+
747+
const int chain_id{1};
748+
const uint64_t batch_size{170}; // Batch size not enough to process a single block
749+
const bool write_change_sets{false}; // We CANNOT write changesets here, TestDatabaseContext db already has them
750+
const bool write_receipts{false}; // We CANNOT write receipts here, TestDatabaseContext db already has them
751+
const bool write_call_traces{false}; // For coherence but don't care
752+
753+
auto execute_blocks = [&](auto start_block, auto end_block) {
754+
return silkworm_lib.execute_blocks_perpetual(env,
755+
chain_id,
756+
start_block,
757+
end_block,
758+
batch_size,
759+
write_change_sets,
760+
write_receipts,
761+
write_call_traces);
762+
};
763+
764+
/* TestDatabaseContext db contains a test chain made up of 9 blocks */
765+
766+
// Prepare block template (just 1 tx w/ value transfer)
767+
evmc::address from{0x658bdf435d810c91414ec09147daa6db62406379_address}; // funded in genesis
768+
evmc::address to{0x8b299e2b7d7f43c0ce3068263545309ff4ffb500_address}; // untouched address(es)
769+
intx::uint256 value{1};
770+
771+
Block block{};
772+
block.header.gas_limit = 5'000'000;
773+
block.header.gas_used = 21'000;
774+
775+
static constexpr auto kEncoder = [](Bytes& dest, const Receipt& r) { rlp::encode(dest, r); };
776+
std::vector<Receipt> receipts{
777+
{TransactionType::kLegacy, true, block.header.gas_used, {}, {}},
778+
};
779+
block.header.receipts_root = trie::root_hash(receipts, kEncoder);
780+
block.transactions.resize(1);
781+
block.transactions[0].to = to;
782+
block.transactions[0].gas_limit = block.header.gas_limit;
783+
block.transactions[0].type = TransactionType::kLegacy;
784+
block.transactions[0].max_priority_fee_per_gas = 0;
785+
block.transactions[0].max_fee_per_gas = 20 * kGiga;
786+
block.transactions[0].value = value;
787+
block.transactions[0].r = 1; // dummy
788+
block.transactions[0].s = 1; // dummy
789+
block.transactions[0].set_sender(from);
790+
791+
constexpr size_t kBlocks{130};
792+
793+
// Insert N blocks
794+
for (size_t i{10}; i < 10 + kBlocks; ++i) {
795+
block.header.number = i;
796+
insert_block(env, block);
797+
block.transactions.erase(block.transactions.cbegin());
798+
block.transactions.pop_back();
799+
block.transactions[0].nonce++;
800+
block.transactions[0].to->bytes[19]++; // change recipient address to force batch size growth
801+
}
802+
803+
// Execute N blocks using an *internal* txn
804+
BlockNum start_block{10}, end_block{10 + kBlocks - 1};
805+
const auto result0{execute_blocks(start_block, end_block)};
806+
CHECK(result0.execute_block_result == SILKWORM_INTERNAL_ERROR);
671807
}
672808

673809
TEST_CASE_METHOD(CApiTest, "CAPI silkworm_add_snapshot", "[silkworm][capi]") {

silkworm/infra/common/bounded_buffer.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ class BoundedBuffer {
6161
not_empty_.notify_one();
6262
}
6363

64+
void peek_back(value_type* item) {
65+
boost::unique_lock<boost::mutex> lock(mutex_);
66+
67+
not_empty_.wait(lock, [&] { return is_stopped() || is_not_empty(); });
68+
69+
if (is_stopped()) { // If the buffer is stopped, do not peek the item
70+
item = nullptr;
71+
return;
72+
}
73+
74+
*item = container_[unread_ - 1];
75+
lock.unlock();
76+
}
77+
6478
void pop_back(value_type* item) {
6579
boost::unique_lock<boost::mutex> lock(mutex_);
6680

silkworm/infra/common/bounded_buffer_test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,25 @@ TEST_CASE("BoundedBuffer can terminate consumer") {
185185
consume.join();
186186
}
187187

188+
TEST_CASE("BoundedBuffer peek does not remove item") {
189+
BoundedBuffer<std::string> buffer(10);
190+
buffer.push_front("Hello1");
191+
buffer.push_front("Hello2");
192+
buffer.push_front("Hello3");
193+
buffer.push_front("Hello4");
194+
195+
std::string item;
196+
buffer.peek_back(&item);
197+
CHECK(item == "Hello1");
198+
CHECK(buffer.size() == 4);
199+
200+
buffer.pop_back(&item);
201+
CHECK(item == "Hello1");
202+
CHECK(buffer.size() == 3);
203+
204+
buffer.peek_back(&item);
205+
CHECK(item == "Hello2");
206+
CHECK(buffer.size() == 3);
207+
}
208+
188209
} // namespace silkworm

0 commit comments

Comments
 (0)