Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bandwidth_scheduler) - include parent's receipts in bandwidth requests #12728

Merged
merged 8 commits into from
Jan 20, 2025

Conversation

jancionear
Copy link
Contributor

When making a bandwidth request to a child shard which has been split from a parent shard, we have to include the receipts stored in the outgoing buffer to the parent shard in the bandwidth request for sending receipts to the child shard. Forwarding receipts from the buffer to parent uses bandwidth granted for sending receipts to one of the children. Not including the parent receipts in the bandwidth request could lead to a situation where a receipt can't be sent because the grant for sending receipts to a child is too small to send out a receipt from a buffer aimed at a parent.

…quests

When making a bandwidth request to a child shard which has been split from a parent
shard, we have to include the receipts stored in the outgoing buffer to the parent shard
in the bandwidth request for sending receipts to the child shard. Forwarding receipts
from the buffer to parent uses bandwidth granted for sending receipts to one of the
children. Not including the parent receipts in the bandwidth request could lead to a
situation where a receipt can't be sent because the grant for sending receipts to a child
is too small to send out a receipt from a buffer aimed at a parent.
@jancionear jancionear requested a review from a team as a code owner January 13, 2025 16:47
@jancionear
Copy link
Contributor Author

Implementing it was the easy part, now how do I test it...
I guess I could modify some resharding test and put a lot of big receipts that require bandwidth requests in the outgoing buffer to parent?

Copy link

codecov bot commented Jan 13, 2025

Codecov Report

Attention: Patch coverage is 91.11111% with 16 lines in your changes missing coverage. Please review.

Project coverage is 70.76%. Comparing base (515b5fa) to head (af293e3).
Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...ntegration-tests/src/test_loop/utils/resharding.rs 93.57% 5 Missing and 2 partials ⚠️
...egration-tests/src/test_loop/utils/transactions.rs 70.58% 4 Missing and 1 partial ⚠️
runtime/runtime/src/congestion_control.rs 95.65% 1 Missing and 1 partial ⚠️
runtime/runtime/src/lib.rs 33.33% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #12728      +/-   ##
==========================================
+ Coverage   70.72%   70.76%   +0.04%     
==========================================
  Files         849      849              
  Lines      174675   174818     +143     
  Branches   174675   174818     +143     
==========================================
+ Hits       123539   123716     +177     
+ Misses      45957    45925      -32     
+ Partials     5179     5177       -2     
Flag Coverage Δ
backward-compatibility 0.16% <0.00%> (-0.01%) ⬇️
db-migration 0.16% <0.00%> (-0.01%) ⬇️
genesis-check 1.35% <0.00%> (-0.01%) ⬇️
linux 69.15% <11.66%> (-0.04%) ⬇️
linux-nightly 70.35% <91.11%> (+0.02%) ⬆️
pytests 1.64% <0.00%> (-0.01%) ⬇️
sanity-checks 1.46% <0.00%> (-0.01%) ⬇️
unittests 70.60% <91.11%> (+0.04%) ⬆️
upgradability 0.20% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -2053,6 +2053,7 @@ impl Runtime {
let pending_delayed_receipts = processing_state.delayed_receipts;
let processed_delayed_receipts = process_receipts_result.processed_delayed_receipts;
let promise_yield_result = process_receipts_result.promise_yield_result;
let shard_layout = epoch_info_provider.shard_layout(&apply_state.epoch_id)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might break replayability, but AFAIU we now support only the last two versions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it break replayability?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to have this here as long as we are not doing something different for past protocol versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it break replayability?

Previously the call to epoch_info_provider.shard_layout() was gated by ProtocolFeature::SimpleNightshadeV4.enabled(protocol_version), I don't know what would happen on older protocol versions, would it throw an error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I probably wouldn't be too worried 😅

Copy link
Contributor

@wacban wacban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but see the comment about chaining order

{
let parent_receipt_sizes_iter =
parent_metadata.iter_receipt_group_sizes(trie, side_effects);
receipt_sizes_iter = Box::new(receipt_sizes_iter.chain(parent_receipt_sizes_iter));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think resharding tries to empty the parent buffers first - (double check please) - so I would feel better to do it in the same order here. It may be important.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, might start mattering what exactly do we fill within the bandwidth request as it's based on the order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch, I wanted to have the parent buffer first but I did the opposite by mistake

@@ -2053,6 +2053,7 @@ impl Runtime {
let pending_delayed_receipts = processing_state.delayed_receipts;
let processed_delayed_receipts = process_receipts_result.processed_delayed_receipts;
let promise_yield_result = process_receipts_result.promise_yield_result;
let shard_layout = epoch_info_provider.shard_layout(&apply_state.epoch_id)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it break replayability?

@wacban
Copy link
Contributor

wacban commented Jan 14, 2025

Implementing it was the easy part, now how do I test it... I guess I could modify some resharding test and put a lot of big receipts that require bandwidth requests in the outgoing buffer to parent?

yeah, please see the testloop tests for resharding_v3, those should be usable for your case

Copy link
Contributor

@shreyan-gupta shreyan-gupta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks!

@jancionear
Copy link
Contributor Author

I added a resharding_v3 test which sends 3MB receipts from one shard to a shard that is split into two children. The 3MB receipts are buffered in the buffer aimed at the parent shard, and bandwidth request generation needs to generate the proper requests to children in order to forward them. The are no receipts in the buffers to children to ensure that requests to children take into account the buffer to parent.

The situation looks like this:

new block #10 shards: [5, 3, 6] chunk mask [true, true, true] block hash QnHFtFZPfSGvQHxVB9G1JAhq4ZD1G4sibtA1kdAUy1D epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 1.129s  INFO test: outgoing buffers from shard 5: {}
 1.129s  INFO test: outgoing buffers from shard 3: {}
 1.129s  INFO test: outgoing buffers from shard 6: {}
 1.129s  INFO test: Sending 3MB receipt from account1 to account4. tx_hash: GqzwERTKRzRwAXfuSm1A4ywGqTxySRnNw6sEyn2MFNMD
 1.129s  INFO test: Sending 3MB receipt from account1 to account6. tx_hash: 6sPB9k96Rh7P8GsXT1jtsoLe7DnQbNuTJYa6GjJiqrWq
new block #11 shards: [5, 3, 6] chunk mask [true, true, true] block hash 37gf65nGFyumvuWL53uaViCgu7t9xHGKtjPDgf4bW3PH epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 1.203s  INFO test: outgoing buffers from shard 5: {}
 1.203s  INFO test: outgoing buffers from shard 3: {}
 1.203s  INFO test: outgoing buffers from shard 6: {}
 1.204s  INFO test: Sending 3MB receipt from account1 to account4. tx_hash: 6ULGwQoMeg6puNTqBK9pfqrPSKgk6Hjdag2bi3wWWLFz
 1.204s  INFO test: Sending 3MB receipt from account1 to account6. tx_hash: J5p1X9NGrUFUarbB3KNm3zjn2bqcJkUgRbhxWEYit85c
new block #12 shards: [5, 3, 6] chunk mask [true, true, true] block hash 4zYVCVQJjS8Dw9tPSRXwhXnkoU4DufmJAhUwhstSAi7r epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 1.380s  INFO test: outgoing buffers from shard 5: {}
 1.380s  INFO test: outgoing buffers from shard 3: {}
 1.380s  INFO test: outgoing buffers from shard 6: {}
 1.380s  INFO test: Sending 3MB receipt from account1 to account4. tx_hash: APiSRseffDPizHykMtbrU9NsNVuLeM7aMawU8x3gfYeT
 1.380s  INFO test: Sending 3MB receipt from account1 to account6. tx_hash: Hyvop8jvYfyUEui3dgfnGdQd6VwpXZX63pZos8Vh9PLz
new block #13 shards: [5, 3, 6] chunk mask [true, true, true] block hash AZPcFxXtF97cCL7gDEVG4QYFVUQmeoLWu3TYw2AYvig5 epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 1.932s  INFO test: outgoing buffers from shard 5: {}
 1.933s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB]}
 1.933s  INFO test: outgoing buffers from shard 6: {}
new block #14 shards: [5, 3, 6] chunk mask [true, true, true] block hash 9qzQqmYPVgcck3T74HbyLjNPk8u4W1rx6BMzMZ1rU8e epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 2.759s  INFO test: outgoing buffers from shard 5: {}
 2.759s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB]}
 2.759s  INFO test: outgoing buffers from shard 6: {}
new block #15 shards: [5, 3, 6] chunk mask [true, true, true] block hash yWrM9DLwivyW97thyBX8VAjVfpkTTEEYuLUQzNCdNZa epoch id 2xnDZ56HJtq4ZkyYtqjcRoHyb6FdtMdVCAh46CUBFHhv
 3.285s  INFO test: outgoing buffers from shard 5: {}
 3.287s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB, 3.0 MB, 3.0 MB]}
 3.287s  INFO test: outgoing buffers from shard 7: {}
 3.287s  INFO test: outgoing buffers from shard 8: {}
new block #16 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash HSJ6nUBoSWRzaVkuLiT17TcfeXx7NJKKwzyyzS1VovNG epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 3.480s  INFO test: outgoing buffers from shard 5: {}
 3.482s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB, 3.0 MB, 3.0 MB]}
 3.482s  INFO test: outgoing buffers from shard 7: {}
 3.482s  INFO test: outgoing buffers from shard 8: {}
new block #17 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash 7zr4maTR9BTzHU9uU6ciphy3UAxxN5WXpw58paHCapLQ epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 3.955s  INFO test: outgoing buffers from shard 5: {}
 3.957s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB, 3.0 MB, 3.0 MB]}
 3.957s  INFO test: outgoing buffers from shard 7: {}
 3.957s  INFO test: outgoing buffers from shard 8: {}
new block #18 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash 9BQoNysb5fB8pf7cHfdnU5gfKjmwgnQodG3HZKVfC9Ae epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 4.731s  INFO test: outgoing buffers from shard 5: {}
 4.732s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB, 3.0 MB]}
 4.732s  INFO test: outgoing buffers from shard 7: {}
 4.732s  INFO test: outgoing buffers from shard 8: {}
new block #19 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash A9knKwbPMujvHzd1qGjXBgPKvB6Et9orRT1ejdfGdWZy epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 5.515s  INFO test: outgoing buffers from shard 5: {}
 5.516s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB, 3.0 MB]}
 5.516s  INFO test: outgoing buffers from shard 7: {}
 5.516s  INFO test: outgoing buffers from shard 8: {}
new block #20 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash AefEKam8WJ98qvLyrSczMXTGWwJuZQpsnwczWE9g68mX epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 6.293s  INFO test: outgoing buffers from shard 5: {}
 6.293s  INFO test: outgoing buffers from shard 3: {6: [3.0 MB]}
 6.293s  INFO test: outgoing buffers from shard 7: {}
 6.293s  INFO test: outgoing buffers from shard 8: {}
 new block #21 shards: [5, 3, 7, 8] chunk mask [true, true, true, true] block hash 2Zyb5eG975zvjSMeNofEZbYvLpSfucfAkJfy7Ar4DPHB epoch id 4N2CxMwS3G7uyYjXRPPNGgHzgxp1HZkJ3r8RZ1zYQAiU
 6.669s  INFO test: outgoing buffers from shard 5: {}
 6.669s  INFO test: outgoing buffers from shard 3: {}
 6.669s  INFO test: outgoing buffers from shard 7: {}
 6.669s  INFO test: outgoing buffers from shard 8: {}

The test actually found some bugs in the initial implementation. The main bug was that bandwidth requests were only generated for shards which had an outgoing buffer, meaning that there had to be at least one receipt aimed at the shard in the past. New shards created after resharding didn't have an outgoing buffer, and because of that requests were not generated for them. The other bug was that the code short-circuited when there were no receipts in the outgoing buffer. It made sense before, but now we also have to check the receipts in the parent buffer before we can decide that there's no need to make a request.

I rewrote generate_bandwidth_request to better reflect the new logic.

Copy link
Contributor

@wacban wacban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Comment on lines +1031 to +1035
/// This test sends large (3MB) receipts from a stable shard to shard that will be split into two.
/// These large receipts are buffered and at the resharding boundary the stable shard's outgoing
/// buffer contains receipts to the shard that was split. Bandwidth requests to the child where the
/// receipts will be sent must include the receipts stored in outgoing buffer to the parent shard,
/// otherwise there will be no bandwidth grants to send them.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to further my understanding, can you confirm the following?

The stable shard will contain a mix of receipts for both children. The parent buffer will be included in the request calculation for both child shard and so the requests will be overestimated. That will continue until the parent shard is emptied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's correct 👍

Comment on lines +375 to +376
let memtrie =
get_memtrie_for_shard(&client_actor.client, &shard_uid, &tip.prev_block_hash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why get the memtrie for the prev block? Or is this the API?

If the latter IMO those methods should include something like "from_prev_block" in the name. It's all too easy to make an off by one error as is. Not related to your PR, just ranting ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument is called prev_block_hash:

pub fn get_memtrie_for_shard(
    client: &Client,
    shard_uid: &ShardUId,
    prev_block_hash: &CryptoHash,
) -> Trie {
    let state_root =
        *client.chain.get_chunk_extra(prev_block_hash, shard_uid).unwrap().state_root();

    // Here memtries will be used as long as client has memtries enabled.
    let memtrie = client
        .runtime_adapter
        .get_trie_for_shard(shard_uid.shard_id(), prev_block_hash, state_root, false)
        .unwrap();
    assert!(memtrie.has_memtries());
    memtrie
}

AFAIU we get post-state root of the chunk included in the previous block, which means that it's the pre-state root of the chunk at the current height.
But tbh I just copied it from check_buffered_receipts_exist_in_memtrie, in this test being off by one doesn't really matter, what matters is that there are only receipts in the outgoing buffer to the parent shard.

Comment on lines +381 to +386
let receipt_size = match receipt {
Ok(ReceiptOrStateStoredReceipt::StateStoredReceipt(
state_stored_receipt,
)) => state_stored_receipt.metadata().congestion_size,
_ => panic!("receipt is {:?}", receipt),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mini nit: This could also be written as a let <pattern> = receipt else { panic!(..) }; }; Not sure if it's any better but it may be a bit shorter.

Copy link
Contributor Author

@jancionear jancionear Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like let else works well with short patterns, but for longer ones I find it harder to read than a standard match.

And in here it would even be longer:

let receipt_size = if let Ok(
    ReceiptOrStateStoredReceipt::StateStoredReceipt(state_stored_receipt),
) = receipt
{
    state_stored_receipt.metadata().congestion_size
} else {
    panic!("receipt is {:?}", receipt)
};

let cur_epoch_estimated_end = cur_epoch_start + cur_epoch_length;
cur_epoch_estimated_end
}
_ => tip.height + 99999999999999, // Not in the next epoch, set to infinity into the future
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mini nit: maybe BlockHeight::MAX?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially did that, but then I had to add something to estimated_resharding_height and that'd overflow, so I changed it to something smaller, but still very far into the future. I think there is no more adding now, but I'd prefer to keep it the way it is, less likely to cause trouble in the future.

let cur_epoch_length =
epoch_manager.get_epoch_config(&tip.epoch_id).unwrap().epoch_length;
let cur_epoch_estimated_end = cur_epoch_start + cur_epoch_length;
cur_epoch_estimated_end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you following the convention of resharding_block being the last block of the old shard layout?

Copy link
Contributor Author

@jancionear jancionear Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm you're right, resharding_height is the last block of the old epoch, but cur_epoch_start + cur_epoch_length is actually the first block of the new one. I'll change it to match resharding_height.

{
for signer_id in &signer_ids {
for receiver_id in &receiver_ids {
// Send a 3MB cross-shard receipt from signer_id's shard to receiver_id's shard.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe move the contents to a helper method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to be a function with 8 arguments :/
call_burn_gas_contract also has the tx sending logic inlined, I think it's good enough.


/// Checks status of the provided transactions. Panics if transaction result is an error.
/// Removes transactions that finished successfully from the list.
pub fn check_txs_remove_successful(txs: &Cell<Vec<(CryptoHash, u64)>>, client: &Client) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: u64->BlockHeight

@jancionear jancionear added this pull request to the merge queue Jan 20, 2025
Merged via the queue into master with commit b12af57 Jan 20, 2025
28 checks passed
@jancionear jancionear deleted the jan_bandwidth_request_resharding branch January 20, 2025 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants