-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Safe and sane multi-item storage removal #11490
Changes from 31 commits
0e48dc9
ae9b285
7f80f12
3e580e5
4a5ff62
8d56c13
91362cd
4767538
08b898c
aee7993
a1982d1
ed26985
72f8fdd
d97aa89
f4bbf8d
e82b1ca
73294b0
8a8efb1
a626d54
6867534
94e885e
2566a4d
a71f9a0
f435107
bc181a0
9317f6a
0e51dbe
ee53604
1fb2ef5
0338fce
73d1fc6
ef7f475
7db73b1
6aeb3a3
4692d06
7e8a8c2
c22b924
edf7ce0
33224f2
a5f349b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ | |
// NOTE: could replace unhashed by having only one kind of storage (top trie being the child info | ||
// of null length parent storage key). | ||
|
||
pub use crate::sp_io::KillStorageResult; | ||
pub use crate::sp_io::{KillStorageResult, MultiRemovalResults}; | ||
use crate::sp_std::prelude::*; | ||
use codec::{Codec, Decode, Encode}; | ||
pub use sp_core::storage::{ChildInfo, ChildType, StateVersion}; | ||
|
@@ -136,13 +136,66 @@ pub fn exists(child_info: &ChildInfo, key: &[u8]) -> bool { | |
/// not make much sense because it is not cumulative when called inside the same block. | ||
/// Use this function to distribute the deletion of a single child trie across multiple | ||
/// blocks. | ||
#[deprecated = "Use `clear_storage` instead"] | ||
pub fn kill_storage(child_info: &ChildInfo, limit: Option<u32>) -> KillStorageResult { | ||
match child_info.child_type() { | ||
ChildType::ParentKeyId => | ||
sp_io::default_child_storage::storage_kill(child_info.storage_key(), limit), | ||
} | ||
} | ||
|
||
/// Partially clear the child storage of each key-value pair. | ||
/// | ||
/// # Limit | ||
/// | ||
/// A *limit* should always be provided through `maybe_limit`. This is one fewer than the | ||
/// maximum number of backend iterations which may be done by this operation and as such | ||
/// represents the maximum number of backend deletions which may happen. A *limit* of zero | ||
/// implies that no keys will be deleted, though there may be a single iteration done. | ||
/// | ||
/// The limit can be used to partially delete storage items in case it is too large or costly | ||
/// to delete all in a single operation. | ||
/// | ||
/// # Cursor | ||
/// | ||
/// A *cursor* may be passed in to this operation with `maybe_cursor`. `None` should only be | ||
/// passed once (in the initial call) for any attempt to clear storage. In general, subsequent calls | ||
/// operating on the same prefix should pass `Some` and this value should be equal to the | ||
/// previous call result's `maybe_cursor` field. The only exception to this is when you can | ||
/// guarantee that the subsequent call is in a new block; in this case the previous call's result | ||
/// cursor need not be passed in an a `None` may be passed instead. This exception may be useful | ||
/// then making this call solely from a block-hook such as `on_initialize`. | ||
/// | ||
/// Returns [`MultiRemovalResults`](sp_io::MultiRemovalResults) to inform about the result. Once the | ||
/// resultant `maybe_cursor` field is `None`, then no further items remain to be deleted. | ||
/// | ||
/// NOTE: After the initial call for any given child storage, it is important that no keys further | ||
/// keys are inserted. If so, then they may or may not be deleted by subsequent calls. | ||
/// | ||
/// # Note | ||
/// | ||
/// Please note that keys which are residing in the overlay for the child are deleted without | ||
/// counting towards the `limit`. | ||
pub fn clear_storage( | ||
child_info: &ChildInfo, | ||
maybe_limit: Option<u32>, | ||
_maybe_cursor: Option<&[u8]>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WTF? Now our code using the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're writing this comment just to blow off some steam, this is not the right place. What exactly is the code in your repo that is undergoing an infinite loop after this PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code like this: let mut res = <MyMap<T>>::clear(16, None);
while let Some(cursor) = res.maybe_cursor {
res = <MyMap<T>>::clear(16, Some(&cursor));
} Where
It is quite a serious issue with how one would use a cursor. Here, the issue is obvious, as the cursor is simply ignored, leading to a never-ending loop, since the code goes over the same data over and over again. The workaround is to use a deprecated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that is right! Sorry that was an oversight by us. This currently happens because we don't have the new host function enabled. With this we wouldn't get this infinite loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's my understanding as well, and it is also understandable. But it shouldn't probably have been integrated at this stage. It also doesn't help that the only working option We're guilty of this too, but somehow this issue has passed through all the QA layers here and at our end - and we ended up with a bug. I propose that, as a hotfix measure, and so that people don't start jumping to the new (and broken so far) API, we undeprecate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Emphasis added on purpose. The API avoided giving guarantees so that downstream projects can write code which works regardless of the status of host functions. A simpler (and non-broken) version of the code above is just to use In general if you see an API designed to force a limit on the number of operations, then it might not be such a great idea to "workaround" the API designer's intention by placing it within an unbounded loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First of all thanks for the workaround of Secondly, this is definitely going even more sideways. When I use the code that documents how the cursor behaves, I expect it to do what is documented.
The thing is, I am not trying to work around anything. I am using the API as documented. It is not my fault that it is broken. Look, I've tried to wrap my mind around this phrase to try to agree with it, like in a general case, but I don't think in the general case it makes any sense at all. And in the context - well, the cursor APIs are precisely designed to be used in a loop of some shape or form. How else would you use them? What I'd like to see improved here is the documentation on the correctness. The documentation does not mention that this function can only work once per block (but it should've been). In fact, I don't think it intended to have this limitation - with a proper overlay API it can just work (but that's where you need new host functions, right?). And that's what I expected from this API. Cause why would it be worth working on otherwise? We already have So, one question about the intent: the cursor, will it work if we pass it from one block to another? Obviously, it doesn't matter in the current implementation, but what's the intent here? It would be nice if it's also explained more thoroughly in the documentation. This is pretty serious, and it seems like this issue has the potential to be shoved under the rug without a proper resolution, but without any real reason to do this. So, currently, the documentation doesn't mention anything about the requirement to run the code over multiple blocks (that would work properly since the removals would span across many instances of the storage overlays). The thing is, the cursor is unused in this implementation, so the documentation on it will not be true still. So, really, I don't know to make this API right without the proper support from the host end, so, I'd stick with the suggestion I gave in the last message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how this is broken. If I were to write the code that you provided above, I would have checked to see whether or not the returned cursor is at the same position as it was before the When I see the word attempt, that is immediately what I think of, because it signals to me that the cursor may not advance after every operation. Regardless of whether or not we actually consume the cursor provided, checking that the cursor has in fact advanced is a sensible operation to perform after every |
||
) -> MultiRemovalResults { | ||
// TODO: Once the network has upgraded to include the new host functions, this code can be | ||
// enabled. | ||
// sp_io::default_child_storage::storage_kill(prefix, maybe_limit, maybe_cursor) | ||
let r = match child_info.child_type() { | ||
ChildType::ParentKeyId => | ||
sp_io::default_child_storage::storage_kill(child_info.storage_key(), maybe_limit), | ||
}; | ||
use sp_io::KillStorageResult::*; | ||
let (maybe_cursor, backend) = match r { | ||
AllRemoved(db) => (None, db), | ||
SomeRemaining(db) => (Some(child_info.storage_key().to_vec()), db), | ||
}; | ||
MultiRemovalResults { maybe_cursor, backend, unique: backend, loops: backend } | ||
} | ||
|
||
/// Ensure `key` has no explicit entry in storage. | ||
pub fn kill(child_info: &ChildInfo, key: &[u8]) { | ||
match child_info.child_type() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any use cases where it makes sense to pass
None
tomaybe_limit
? If not, it sounds like we should just not make it anOption
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth keeping the host functions general for now. Can't predict future uses quite yet.