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

Commit 5a102f7

Browse files
atheibkchr
andauthored
Implement nested storage transactions (#6269)
* Add transactional storage functionality to OverlayChanges A collection already has a natural None state. No need to wrap it with an option. * Add storage transactions runtime interface * Add frame support for transactions * Fix committed typo * Rename 'changes' variable to 'overlay' * Fix renaming change * Fixed strange line break * Rename clear to clear_where * Add comment regarding delete value on mutation * Add comment which changes are covered by a transaction * Do force the arg to with_transaction return a Result * Use rust doc comments on every documentable place * Fix wording of insert_diry doc * Improve doc on start_transaction * Rename value to overlayed in close_transaction * Inline negation * Improve wording of close_transaction comments * Get rid of an expect by using get_or_insert_with * Remove trailing whitespace * Rename should to expected in tests * Rolling back a transaction must mark the overlay as dirty * Protect client initiated storage tx from being droped by runtime * Review nits * Return Err when entering or exiting runtime fails * Documentation fixup * Remove close type * Move enter/exit runtime to excute_aux in the state-machine * Rename Discard -> Rollback * Move child changeset creation to constructor * Move child spawning into the closure * Apply suggestions from code review Co-authored-by: Bastian Köcher <[email protected]> * Fixup for code suggestion * Unify re-exports * Rename overlay_changes to mod.rs and move into subdir * Change proof wording * Adapt a new test from master to storage-tx * Suggestions from the latest round of review * Fix warning message Co-authored-by: Bastian Köcher <[email protected]>
1 parent 19826b9 commit 5a102f7

File tree

16 files changed

+1388
-537
lines changed

16 files changed

+1388
-537
lines changed

Cargo.lock

Lines changed: 20 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frame/support/src/storage/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ pub mod child;
2929
pub mod generator;
3030
pub mod migration;
3131

32+
/// Describes whether a storage transaction should be committed or rolled back.
33+
pub enum TransactionOutcome<T> {
34+
/// Transaction should be committed.
35+
Commit(T),
36+
/// Transaction should be rolled back.
37+
Rollback(T),
38+
}
39+
40+
/// Execute the supplied function in a new storage transaction.
41+
///
42+
/// All changes to storage performed by the supplied function are discarded if the returned
43+
/// outcome is `TransactionOutcome::Rollback`.
44+
///
45+
/// Transactions can be nested to any depth. Commits happen to the parent transaction.
46+
pub fn with_transaction<R>(f: impl FnOnce() -> TransactionOutcome<R>) -> R {
47+
use sp_io::storage::{
48+
start_transaction, commit_transaction, rollback_transaction,
49+
};
50+
use TransactionOutcome::*;
51+
52+
start_transaction();
53+
match f() {
54+
Commit(res) => { commit_transaction(); res },
55+
Rollback(res) => { rollback_transaction(); res },
56+
}
57+
}
58+
3259
/// A trait for working with macro-generated storage values under the substrate storage API.
3360
///
3461
/// Details on implementation can be found at
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// This file is part of Substrate.
2+
3+
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
use codec::{Encode, Decode, EncodeLike};
19+
use frame_support::{
20+
StorageMap, StorageValue, storage::{with_transaction, TransactionOutcome::*},
21+
};
22+
use sp_io::TestExternalities;
23+
24+
pub trait Trait {
25+
type Origin;
26+
type BlockNumber: Encode + Decode + EncodeLike + Default + Clone;
27+
}
28+
29+
frame_support::decl_module! {
30+
pub struct Module<T: Trait> for enum Call where origin: T::Origin {}
31+
}
32+
33+
frame_support::decl_storage!{
34+
trait Store for Module<T: Trait> as StorageTransactions {
35+
pub Value: u32;
36+
pub Map: map hasher(twox_64_concat) String => u32;
37+
}
38+
}
39+
40+
41+
#[test]
42+
fn storage_transaction_basic_commit() {
43+
TestExternalities::default().execute_with(|| {
44+
45+
assert_eq!(Value::get(), 0);
46+
assert!(!Map::contains_key("val0"));
47+
48+
with_transaction(|| {
49+
Value::set(99);
50+
Map::insert("val0", 99);
51+
assert_eq!(Value::get(), 99);
52+
assert_eq!(Map::get("val0"), 99);
53+
Commit(())
54+
});
55+
56+
assert_eq!(Value::get(), 99);
57+
assert_eq!(Map::get("val0"), 99);
58+
});
59+
}
60+
61+
#[test]
62+
fn storage_transaction_basic_rollback() {
63+
TestExternalities::default().execute_with(|| {
64+
65+
assert_eq!(Value::get(), 0);
66+
assert_eq!(Map::get("val0"), 0);
67+
68+
with_transaction(|| {
69+
Value::set(99);
70+
Map::insert("val0", 99);
71+
assert_eq!(Value::get(), 99);
72+
assert_eq!(Map::get("val0"), 99);
73+
Rollback(())
74+
});
75+
76+
assert_eq!(Value::get(), 0);
77+
assert_eq!(Map::get("val0"), 0);
78+
});
79+
}
80+
81+
#[test]
82+
fn storage_transaction_rollback_then_commit() {
83+
TestExternalities::default().execute_with(|| {
84+
Value::set(1);
85+
Map::insert("val1", 1);
86+
87+
with_transaction(|| {
88+
Value::set(2);
89+
Map::insert("val1", 2);
90+
Map::insert("val2", 2);
91+
92+
with_transaction(|| {
93+
Value::set(3);
94+
Map::insert("val1", 3);
95+
Map::insert("val2", 3);
96+
Map::insert("val3", 3);
97+
98+
assert_eq!(Value::get(), 3);
99+
assert_eq!(Map::get("val1"), 3);
100+
assert_eq!(Map::get("val2"), 3);
101+
assert_eq!(Map::get("val3"), 3);
102+
103+
Rollback(())
104+
});
105+
106+
assert_eq!(Value::get(), 2);
107+
assert_eq!(Map::get("val1"), 2);
108+
assert_eq!(Map::get("val2"), 2);
109+
assert_eq!(Map::get("val3"), 0);
110+
111+
Commit(())
112+
});
113+
114+
assert_eq!(Value::get(), 2);
115+
assert_eq!(Map::get("val1"), 2);
116+
assert_eq!(Map::get("val2"), 2);
117+
assert_eq!(Map::get("val3"), 0);
118+
});
119+
}
120+
121+
#[test]
122+
fn storage_transaction_commit_then_rollback() {
123+
TestExternalities::default().execute_with(|| {
124+
Value::set(1);
125+
Map::insert("val1", 1);
126+
127+
with_transaction(|| {
128+
Value::set(2);
129+
Map::insert("val1", 2);
130+
Map::insert("val2", 2);
131+
132+
with_transaction(|| {
133+
Value::set(3);
134+
Map::insert("val1", 3);
135+
Map::insert("val2", 3);
136+
Map::insert("val3", 3);
137+
138+
assert_eq!(Value::get(), 3);
139+
assert_eq!(Map::get("val1"), 3);
140+
assert_eq!(Map::get("val2"), 3);
141+
assert_eq!(Map::get("val3"), 3);
142+
143+
Commit(())
144+
});
145+
146+
assert_eq!(Value::get(), 3);
147+
assert_eq!(Map::get("val1"), 3);
148+
assert_eq!(Map::get("val2"), 3);
149+
assert_eq!(Map::get("val3"), 3);
150+
151+
Rollback(())
152+
});
153+
154+
assert_eq!(Value::get(), 1);
155+
assert_eq!(Map::get("val1"), 1);
156+
assert_eq!(Map::get("val2"), 0);
157+
assert_eq!(Map::get("val3"), 0);
158+
});
159+
}

primitives/api/proc-macro/src/impl_runtime_apis.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ fn generate_runtime_api_base_structures() -> Result<TokenStream> {
260260
&self,
261261
map_call: F,
262262
) -> std::result::Result<R, E> where Self: Sized {
263+
self.changes.borrow_mut().start_transaction();
263264
*self.commit_on_success.borrow_mut() = false;
264265
let res = map_call(self);
265266
*self.commit_on_success.borrow_mut() = true;
@@ -369,6 +370,9 @@ fn generate_runtime_api_base_structures() -> Result<TokenStream> {
369370
&self,
370371
call_api_at: F,
371372
) -> std::result::Result<#crate_::NativeOrEncoded<R>, E> {
373+
if *self.commit_on_success.borrow() {
374+
self.changes.borrow_mut().start_transaction();
375+
}
372376
let res = call_api_at(
373377
&self.call,
374378
self,
@@ -384,11 +388,16 @@ fn generate_runtime_api_base_structures() -> Result<TokenStream> {
384388
}
385389

386390
fn commit_on_ok<R, E>(&self, res: &std::result::Result<R, E>) {
391+
let proof = "\
392+
We only close a transaction when we opened one ourself.
393+
Other parts of the runtime that make use of transactions (state-machine)
394+
also balance their transactions. The runtime cannot close client initiated
395+
transactions. qed";
387396
if *self.commit_on_success.borrow() {
388397
if res.is_err() {
389-
self.changes.borrow_mut().discard_prospective();
398+
self.changes.borrow_mut().rollback_transaction().expect(proof);
390399
} else {
391-
self.changes.borrow_mut().commit_prospective();
400+
self.changes.borrow_mut().commit_transaction().expect(proof);
392401
}
393402
}
394403
}

primitives/externalities/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,29 @@ pub trait Externalities: ExtensionStore {
195195
/// The returned hash is defined by the `Block` and is SCALE encoded.
196196
fn storage_changes_root(&mut self, parent: &[u8]) -> Result<Option<Vec<u8>>, ()>;
197197

198+
/// Start a new nested transaction.
199+
///
200+
/// This allows to either commit or roll back all changes made after this call to the
201+
/// top changes or the default child changes. For every transaction there cam be a
202+
/// matching call to either `storage_rollback_transaction` or `storage_commit_transaction`.
203+
/// Any transactions that are still open after returning from runtime are committed
204+
/// automatically.
205+
///
206+
/// Changes made without any open transaction are committed immediately.
207+
fn storage_start_transaction(&mut self);
208+
209+
/// Rollback the last transaction started by `storage_start_transaction`.
210+
///
211+
/// Any changes made during that storage transaction are discarded. Returns an error when
212+
/// no transaction is open that can be closed.
213+
fn storage_rollback_transaction(&mut self) -> Result<(), ()>;
214+
215+
/// Commit the last transaction started by `storage_start_transaction`.
216+
///
217+
/// Any changes made during that storage transaction are committed. Returns an error when
218+
/// no transaction is open that can be closed.
219+
fn storage_commit_transaction(&mut self) -> Result<(), ()>;
220+
198221
/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
199222
/// Benchmarking related functionality and shouldn't be used anywhere else!
200223
/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

0 commit comments

Comments
 (0)