Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Use Storage for Tracking Transactional Layers #10744

Closed
wants to merge 15 commits into from

Conversation

shawntabrizi
Copy link
Member

@shawntabrizi shawntabrizi commented Jan 27, 2022

In progress...

Specification: #10806

@github-actions github-actions bot added the A0-please_review Pull request needs code review. label Jan 27, 2022
@shawntabrizi
Copy link
Member Author

Probably we should rename this from "transactional" which is just super confusing.

Maybe something more explicit like #[storage_overlays(n)]

@athei
Copy link
Member

athei commented Jan 29, 2022

I still don't get why we want this. Shouldn't the transactional overhead incurred be part of the worst case benchmark. Meaning: Isn't the benchmark for an dispatchable already choosing the path that makes use of the most storage layers. So why do we need to track and cap it?

@shawntabrizi
Copy link
Member Author

Number of storage layers is not something that is deterministic at the moment for a particular extrinsic. For example batch_all.

This limit can be used as a hint for the benchmarking to know how many layers to emulate for calculating weight.

@athei
Copy link
Member

athei commented Jan 31, 2022

So this is to guard against a case where a storage heavy dispatchable is called with a lot of layers already created?

@shawntabrizi
Copy link
Member Author

Yes, basically to know ahead of time the worst case number of layers that we could encounter, and potentially take care of this in weight calculation. Otherwise, we would have no idea, and we would need to assume worst case scenario, which would be like infinite.

Also, by default, we will make the number of layers 1.

@athei
Copy link
Member

athei commented Feb 2, 2022

Yes, basically to know ahead of time the worst case number of layers that we could encounter, and potentially take care of this in weight calculation.

Maybe you can walk me through has this is supposed to work? Let's assume you are batch_all and limit the transaction depth to 3 on your transactional annotation. How would that help you with benchmarking batch_all? The weight does not depend on the number of layers but how many keys are written by the callee (called by batch_all) to said layers.

@kianenigma kianenigma added A3-in_progress Pull request is in progress. No review needed at this stage. and removed A0-please_review Pull request needs code review. labels Feb 2, 2022
Copy link
Member

@ggwpez ggwpez left a comment

Choose a reason for hiding this comment

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

I know it's WIP code but I think the comments still apply; was just curious.

#(#attrs)*
#vis #sig {
use #crate_::storage::{execute_with_storage_layer, has_storage_layer};
if has_storage_layer() {
Copy link
Member

Choose a reason for hiding this comment

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

So this prevents recursive storage layers?
Was that not the whole point of having a limit?

Copy link
Member Author

Choose a reason for hiding this comment

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

This would be an entirely different approach to storage layers, where rather than spawning new transactional layers per thing, we would just spawn at most one.

This would be the thing we would probably use by default across all pallet extrinsics.

thread_local! {
static TRANSACTION_LEVEL: RefCell<u32> = RefCell::new(0);
pub fn get_storage_layer() -> Layer {
crate::storage::unhashed::get_or_default::<Layer>(STORAGE_LAYER_KEY)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if there could be an optimization that we always return default for the top call in the call-stack, since the first call always has default we could get rid of the storage access there.

Copy link
Member Author

Choose a reason for hiding this comment

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

how do we know we are in a top call?

In any case, this is not a "real storage read" since it happens in memory always

return Err(())
}
// Cannot overflow because of check above.
set_storage_layer(&(existing_levels + 1));
Copy link
Member

Choose a reason for hiding this comment

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

If the get/set ends up wasting resources, we could add an Increment(key, limit) function to ParityDB.
Sql servers have this to reduce the number of queries.

Copy link
Member Author

Choose a reason for hiding this comment

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

this never gets written to DB, and thus is always in memory. I will let you see if you can rationalize why

fn dec_storage_layer() {
let existing_levels = get_storage_layer();
if existing_levels == 0 {
log::warn!(
Copy link
Member

Choose a reason for hiding this comment

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

Debug assert maybe?
When we extend the defensive_trait from @kianenigma we could have a proper solution here.

thread_local! {
static TRANSACTION_LEVEL: RefCell<u32> = RefCell::new(0);
pub fn get_storage_layer() -> Layer {
crate::storage::unhashed::get_or_default::<Layer>(STORAGE_LAYER_KEY)
Copy link
Member

Choose a reason for hiding this comment

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

You can debug assert that: if a value is read, it must be != 0.

}

pub fn has_storage_layer() -> bool {
get_storage_layer() > 0
Copy link
Member

Choose a reason for hiding this comment

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

This reduces to has_key(STORAGE_LAYER_KEY) as the storage layer value in the DB should never be 0.

Copy link
Member Author

Choose a reason for hiding this comment

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

has_key and get is the same complexity except has_key also needs to decode the storage into the right type. In this case, a u8 is trivial

require_transaction();
TransactionOutcome::Rollback(())
});
assert_ok!(execute_with_storage_layer(u8::MAX, || -> DispatchResult {
Copy link
Member

Choose a reason for hiding this comment

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

You can use assert_storage_noop! here and probably in others (eg require_transaction_should_return_error) as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

i am not asserting a storage noop here though. I could modify storage in this call and it would still be valid. the test here is that it should return ok, and that within the function, the storage layer should exist.

@stale
Copy link

stale bot commented Mar 7, 2022

Hey, is anyone still working on this? Due to the inactivity this issue has been automatically marked as stale. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the A5-stale Pull request did not receive any updates in a long time. No review needed at this stage. Close it. label Mar 7, 2022
@kianenigma kianenigma removed the A5-stale Pull request did not receive any updates in a long time. No review needed at this stage. Close it. label Mar 8, 2022
use syn::{parse::Parse, ItemFn, LitInt, Result};

struct StorageLayerLimit {
limit: u8,
Copy link
Contributor

@kianenigma kianenigma Mar 24, 2022

Choose a reason for hiding this comment

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

why is this not a tuple struct?

Copy link
Contributor

@kianenigma kianenigma left a comment

Choose a reason for hiding this comment

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

Is there any written note on what should change about this PR?

@shawntabrizi
Copy link
Member Author

I updated the comment to include the specification issue here: #10806

Sorry about that

Comment on lines +82 to +84
log::warn!(
"We are underflowing with calculating transactional levels. Not great, but let's not panic...",
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
log::warn!(
"We are underflowing with calculating transactional levels. Not great, but let's not panic...",
);
frame_support::defensive!(
"We are underflowing with calculating transactional levels. Not great, but let's not panic...",
);

@@ -691,7 +695,9 @@ impl PartialEq for DispatchError {
(CannotLookup, CannotLookup) |
(BadOrigin, BadOrigin) |
(ConsumerRemaining, ConsumerRemaining) |
(NoProviders, NoProviders) => true,
(NoProviders, NoProviders) |
(TooManyConsumers, TooManyConsumers) |
Copy link
Contributor

Choose a reason for hiding this comment

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

wish there would've been compiler warnings for not covering these..

@shawntabrizi
Copy link
Member Author

breaking this down into parts, starting with:

#10808

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A3-in_progress Pull request is in progress. No review needed at this stage.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants