-
Notifications
You must be signed in to change notification settings - Fork 27
feat: bidirectional references (clean two-commit history) #843
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
3a3f2e3
feat: bidirectional references — elements whose reference chains stay…
fominok 23ee336
feat: redesign backward references onto elements with two-layer hashi…
QuantumExplorer c7f65d6
Merge branch 'develop' into feat/bidirectional-references-v2
QuantumExplorer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # Addressing Atomicity | ||
|
|
||
| ## Level 1: RocksDB Transactions | ||
|
|
||
| In GroveDB, almost no operation -- if any at all -- can be executed as a single atomic | ||
| operation in RocksDB, the underlying storage used by GroveDB. As long as parallel access | ||
| to GroveDB is allowed, there is no guarantee that data will remain consistent across the | ||
| multiple operations required at the RocksDB level. Partially, we address this issue using | ||
| RocksDB batches, which will be discussed in more detail in the next section. However, | ||
| these batches do not address data fetches that may occur while the final RocksDB batch is | ||
| being constructed. The data fetched at one step of the operation may be inconsistent with | ||
| the data fetched later, as background updates may have occurred in the meantime. | ||
|
|
||
| To demonstrate the problem, let’s consider a scenario where there is an only key `c` | ||
| under the subtree `[a,b]`. One actor updates this key with a new value while another actor | ||
| performs an insertion at a different location: | ||
|
|
||
| ```text | ||
| Actor 1: Actor 2: | ||
| - load subtree [a,b] with root -- c | ||
| - under subtree [a,b] key c insert value x, | ||
| we're not going into much detail there as - insert empty subtree into [a,b] under key d | ||
| as it was done in one batch and we care - under subtree [a,b,d] key e insert value y | ||
| only about what happened to c - under subtree [a,b] key d insert new root | ||
| ... hash and root key of subtree [a,b,d] | ||
| - compute root [a,b] hash as hash of joined | ||
| hashes of c and d *WE HAVE OLD C* | ||
| - under subtree [a] key b insert new root | ||
| hash and root key of subtree [a,b] | ||
| - under subtree [] key a insert new root | ||
| hash and root key of subtree [a] | ||
| ``` | ||
|
|
||
| ... and not to mention what will happen with the ancestors' hashes. | ||
|
|
||
| __Solution__: all operations shall be performed via RocksDB transactions. | ||
|
|
||
| While this is straightforward for modifications, queries and `get` operations also require | ||
| transactions. In general, they cannot be represented by a single RocksDB operation too. | ||
| Although `get` may be an exception when no references are involved, data still needs to be | ||
| loaded first, and isolation might be required. Therefore, transactions should be provided | ||
| from the start. | ||
|
|
||
| Since the first release transaction arguments are optional, now we internally start a | ||
| transaction if none is provided. To facilitate this, `crate::utils::TxRef` was introduced. | ||
|
|
||
| `TxRef` wraps a transactions provided from user if any, otherwise starts a new one. The | ||
| rest of the GroveDB internals are unaware of the transaction source and uses what `TxRef` | ||
| provided to them with `TxRef::as_ref` method. | ||
|
|
||
| In case the transaction was started internally it shall be committed internally as well, | ||
| for that purpose `TxRef::commit_local` is used, that will commit the transaction if it is | ||
| indeed "local" or is no-operation if the transaction is passed by user, leaving it to the | ||
| user to decide what to do with it. | ||
|
|
||
| ## Level 2: RocksDB Batches | ||
|
|
||
| _Not to be confused with GroveDB batches!_ | ||
|
|
||
| In general, if an operation fails, it doesn't necessarily mean that the entire transaction | ||
| should be aborted, unless it came into an inconsistent state. At least, this is not the | ||
| desired behavior in GroveDB, as it is used in Dash Platform: a transaction should live | ||
| for the duration of a block, with operations happening seamlessly -- even those that | ||
| may fail. | ||
|
|
||
| As stated before, an operation that changes the state of GroveDB consists of many RocksDB | ||
| operations. However, we do not apply them directly to the provided transaction. Instead, | ||
| we aggregate them into a RocksDB batch, which is applied to the transaction all at once | ||
| at the end of the GroveDB operation. This approach allows for failure without aborting | ||
| the entire transaction, as it will only abort the batch, leaving the transaction state | ||
| untouched. | ||
|
|
||
| To apply the `StorageBatch` with these deferred operations onto a running transaction, | ||
| `Storage::commit_multi_context_batch` is used, where the main implementation of `Storage` | ||
| in our case is `RocksDbStorage`. | ||
|
|
||
| ## Level 3: GroveDB Batches | ||
|
|
||
| While RocksDB batches are an implementation detail, GroveDB batches are part of the public | ||
| API, on par with regular operations provided by GroveDB. When several updates to GroveDB | ||
| need to be performed atomically from a user perspective, without sacrificing a transaction | ||
| in case of failure, GroveDB batches are used. | ||
|
|
||
| The main takeaways are: | ||
|
|
||
| - Always a transaction, whether provided externally or not. | ||
| - Always one RocksDB batch applied for modifications. | ||
| - Calling `insert*/delete*` results in one RocksDB batch being applied. | ||
| - Applying a GroveDB batch full of `insert*/delete*` results in one RocksDB batch, likely | ||
| just larger. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.