-
Notifications
You must be signed in to change notification settings - Fork 116
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
go/roothash: Batch history reindex writes #6050
base: master
Are you sure you want to change the base?
go/roothash: Batch history reindex writes #6050
Conversation
✅ Deploy Preview for oasisprotocol-oasis-core canceled.
|
75602c3
to
aff6166
Compare
go/roothash/api/history.go
Outdated
// | ||
// Watcher will not be notified about new blocks since this method is only | ||
// meant to be used during reindex. | ||
CommitBatch(blks []*AnnotatedBlock, roundResults []*RoundResults) error |
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.
Do we want to group *AnnotatedBlock
and *RoundResults
in dedicated struct? Also use it during Commit
and in other places. No strong opinion.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #6050 +/- ##
==========================================
- Coverage 65.15% 64.89% -0.27%
==========================================
Files 635 635
Lines 64634 64706 +72
==========================================
- Hits 42113 41989 -124
- Misses 17597 17815 +218
+ Partials 4924 4902 -22 ☔ View full report in Codecov by Sentry. |
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.
I thought that in the end we will have N workers, everyone with a range of heights to process, so that they can e.g read consensus blocks, process blocks, and encode data in parallel . Of course, they can also write to history in batches, if the history supports. Furthermore, they should fetch block results once and not multiple times if we have more runtimes. Will this solution be able to upgrade to this?
.changelog/5738.feature.md
Outdated
@@ -0,0 +1,4 @@ | |||
go/roothahs: Optimize runtime history reindex |
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.
go/roothahs: Optimize runtime history reindex | |
go/roothash: Optimize runtime history reindex |
Also, the following git messages are not in correct format:
go/consensus/cometbft/roothash: return 0 value on error
go/roothash: Batch writting runtime blocks during reindex
.changelog/5738.feature.md
Outdated
@@ -0,0 +1,4 @@ | |||
go/roothahs: Optimize runtime history reindex | |||
|
|||
During runtime history reindex, we batch 1000 writes at the same time, |
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.
I would be more general and remove values 1000 and 2x from the description. I would only say that blocks and results are written in batches.
) (uint64, error) { | ||
sc.logger.Debug("reindexing batch", | ||
"runtime_id", runtimeID, | ||
"batch_start", start, |
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.
"batch_start", start, | |
"start", start, |
var results *cmtrpctypes.ResultBlockResults | ||
results, err = sc.backend.GetBlockResults(sc.ctx, height) | ||
results, err := sc.backend.GetBlockResults(sc.ctx, height) |
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.
Which context is the right one, ctx
or sc.ctx
?
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.
My understanding is they are the same. I have improved this in 14e21df to always pass context explicitly. Technically WatchBlocks
and WatchEvents
methods still use sc.Ctx
and simply ignore the context they receive (so kinda misleading for the caller :/).
This is existing code so would prefer to tackle this problem elsewhere unless trivial. Any suggestions what to do in such case? Probably we want to combine two contexts and propagate correct error, or stop passing it if find it unnecessarily.
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.
Any suggestions what to do in such case?
Never store context in a struct, and if possible, when making changes to the existing code fix functions so that context is the first argument.
go/roothash/api/history.go
Outdated
@@ -25,6 +25,13 @@ type BlockHistory interface { | |||
// Must be called in order, sorted by round. | |||
Commit(blk *AnnotatedBlock, roundResults *RoundResults, notify bool) error | |||
|
|||
// CommitBatch commits annotated blocks and corresponding round results, |
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.
// CommitBatch commits annotated blocks and corresponding round results, | |
// CommitBatch commits annotated blocks and corresponding round results |
go/roothash/api/history.go
Outdated
// into runtime history. | ||
// | ||
// Watcher will not be notified about new blocks since this method is only | ||
// meant to be used during reindex. |
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.
This method is public, so I can use it wherever I want, not only during reindex. Fix the comment.
go/runtime/history/db.go
Outdated
@@ -214,6 +214,59 @@ func (d *DB) commit(blk *roothash.AnnotatedBlock, roundResults *roothash.RoundRe | |||
}) | |||
} | |||
|
|||
func (d *DB) commitBatch(blks []*roothash.AnnotatedBlock, roundResults []*roothash.RoundResults) error { | |||
if len(blks) != len(roundResults) { | |||
return fmt.Errorf("runtime/history: got %d annotated blocks and %d roundResults, but should be equal", len(blks), len(roundResults)) |
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.
Too long.
go/roothash/api/history.go
Outdated
// | ||
// Watcher will not be notified about new blocks since this method is only | ||
// meant to be used during reindex. | ||
CommitBatch(blks []*AnnotatedBlock, roundResults []*RoundResults) error |
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 we planning to call CommitBatch
also in parallel? Will this work? I think that currently we cannot call Commit
in parallel, or can we?
Must blocks be in order, sorted by round?
Must this be called in order, sorted by round?
What happens if I call Commit
, CommitBatch
, Commit
, ... Will things still work?
Why don't we notify if needed?
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.
You can call it in parallel see benchmarks. Notice that parallelization of consensus blocks without batching (see benchmarks), only improved performance by 2x. This is the performance gain we get with batching only (still running things sequentially). It is for this reason that I would prefer to get in batching as a separate/independent feature.
Must blocks be in order, sorted by round?
Must this be called in order, sorted by round?
I should elaborate these requirements in the docstring. Thanks for pointing.
What happens if I call Commit, CommitBatch, Commit, ... Will things still work?
It should work, I am writting a test for this rn :).
Why don't we notify if needed?
Because this will be only used during reindex and we don't notify during it. If someone will later need this functionality I suggest it is added at that point (should be trivial). Agree that comment should probably changed to something simpler/with less assumptions:
// This method does not notify watchers.
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.
You can call it in parallel
No you can't, because the meta
will be incorrect.
It should work, I am writting a test for this rn :).
I would say not, because Commit
has extra code, which CommitBatch
doesn't.
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.
No you can't, because the meta will be incorrect.
You are right. I think we should extend the batching to support that when/if we start calling it in parallel.
I would say not, because Commit has extra code, which CommitBatch doesn't.
You are right, we are forgetting to notify pruner. Will fix this. As for the notify
I still think we don't have to expose this functionality. See existing Commit
docstring/promise:
// Commit commits an annotated block into history. If notify is set to true,
// the watchers will be notified about the new block. Disable notify when
// doing reindexing.
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.
I think we should extend the batching to support that when/if we start calling it in parallel.
As desribed in Possible_solutions/blockers/2, this should be fixed, by refactoring block history to operate in two modes; reindexing
and reindexed
. During reindexing
, metadata
will be ingnored, and we won't notify watchers expect for the latest round of reindex (as we do rn).
Technically, you can already safely call Commit
and CommitBatch
in parallel, however you will receive an error (which you should handle) in most cases, due to metadata
mismatch.
Alternative, as discussed on the slack is to try to parallelize reading batch part (update) only (as suggested by @ptrus), and skip this complex refactor altogether, provided we will be happy with performance.
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.
I would say not, because Commit has extra code, which CommitBatch doesn't.
I have fixed this following your suggestion in private: where Commit
becomes a helper for CommitBatch
of single entry. I have added separate commit for that.
Prior to that I have also extended TestWatchBlocks
corner cases to capture existing behaviour: see commit.
Docstrings have also been reworked for both Commit
and CommitBatch
.
Finally, as discussed in private we will only fetch consensus state in parallel. The writing part will be written sequentially (in batches).
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.
I would say not, because Commit has extra code, which CommitBatch doesn't.
I have fixed this following your suggestion in private: where
Commit
becomes a helper forCommitBatch
of single entry. I have added separate commit for that.Prior to that I have also extended
TestWatchBlocks
corner cases to capture existing behaviour: see commit.Docstrings have also been reworked for both
Commit
andCommitBatch
.
You don't need to describe these changes in detail. I will see that in the code. Just extra work for you and for reviewers.
Finally, as discussed in private we will only fetch consensus state in parallel. The writing part will be written sequentially (in batches).
This comment is fine, to let other reviewers know what we discussed.
return nil | ||
} | ||
|
||
return d.db.Update(func(tx *badger.Txn) error { |
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.
Transactions should always be fast and small. Therefore, you should improve this by moving outside of the tx
data encoding, data verification, and calculation of max/min.
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.
Fixed here eae4b93, please let me know if you see simpler way of doing it?
Not sure it would have much impact though since badger uses (serializable) snapshot isolation, so reads are actually never blocked by writes (?). We will not support writing in parallel, due to metadata mismatch (and writing part not being a bottleneck).
We should probably still pre-process (pre-validate) it to follow best practices and not to make assumptions how code will be used in the future?
sc.ctx is the same as ctx received by DeliverEvent or DeliverCommand, that triggers reindex or normal event processing. See go/consensus/cometbft/full package (full.go and services.go file) to confirm.
This method was only used for testing, and is redundant since Commit correctly updates LastConsensusHeight.
Additional defensive check, that prevents committing blocks at the same height is added. This ensures we receive at most one runtime block per consensus height. We want to use same defensive check withing a batch (when we implement it). This should be safe since the only method that modifies last consensus height is Commit, which is only called from processFinalizedEvent (reindex or normal event processing).
reindexBlocks is only called from processFinalizedEvents. In case of an error, lastRound is ignored. To avoid confusion, we always return 0 value in case of an error.
aff6166
to
306869a
Compare
We add two more scenarios: 1. Local storage with notify set to false. 2. No local storage with notify set to false.
Commit is now only a special case of CommitBatch.
306869a
to
783354e
Compare
results, err := sc.backend.GetBlockResults(ctx, height) | ||
if err != nil { | ||
// XXX: could soft-fail first few heights in case more heights were | ||
// pruned right after the GetLastRetainedVersion query. | ||
logger.Error("failed to get cometbft block results", | ||
sc.logger.Error("failed to get cometbft block results", | ||
"err", err, | ||
"height", height, | ||
) | ||
return lastRound, fmt.Errorf("failed to get cometbft block results: %w", err) | ||
return 0, fmt.Errorf("failed to get cometbft block results: %w", err) |
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.
I was thinking, if we have prunning set to only retain few blocks (possibly one), or the time interval is very small, could batching always receive an error? Let's say on my machine to process a batch it takes 200-300ms
. Would it be better to defensively check what is the last retained round again if we receive an error, and simply update the latest height if it has changed?
cc @ptrus
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.
I think the minimum interval is 1 second currently, and in practice it's likely much more (e.g. in minutes).
Likely not problematic in practice, so I don't really have strong opinions on how to handle it.
@@ -387,7 +390,7 @@ func (sc *serviceClient) getRuntimeNotifiers(id common.Namespace) *runtimeBroker | |||
return notifiers | |||
} | |||
|
|||
func (sc *serviceClient) reindexBlocks(currentHeight int64, bh api.BlockHistory) (uint64, error) { | |||
func (sc *serviceClient) reindexBlocks(ctx context.Context, currentHeight int64, bh api.BlockHistory) (uint64, error) { |
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.
As discussed with @ptrus it might be a good idea to harden existing history reindex e2e test, by possibly adding a query for every round that we expect it should be reindexed.
Finally also in long term e2e tests we are also querying old random blocks && restarting nodes in between possibly triggering reindex?
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.
Maybe create a follow-up issue for this. I think the simplest way would be running a TestClientScenario which queries (runs GetKeyValueTx
) queries all the old blocks, at the end of the existing history reindex e2e test.
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.
Agree, #6070 created to track this.
const crashPointBlockBeforeIndex = "roothash.before_index" | ||
const ( | ||
crashPointBlockBeforeIndex = "roothash.before_index" | ||
batchSize = 1000 |
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.
Maybe
batchSize = 1000 | |
reindexWriteBatchSize = 1000 |
to make it a bit more descriptive.
if err != nil { | ||
return 0, fmt.Errorf("failed to commit batch to history keeper: %w", err) | ||
} | ||
if last != api.RoundInvalid { // non-empty batch |
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.
I believe reindex returns api.RoundInvalid
and no error, only when there was no runtime rounds finalized in the contained batch of heights, lets update the comment to better reflect this:
Something like
if last != api.RoundInvalid { // non-empty batch | |
if last != api.RoundInvalid { | |
// New rounds indexed. |
default: | ||
return lastRound, fmt.Errorf("failed to get latest block: %w", err) | ||
} | ||
// Do not notify watchers when during history reindex. |
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.
// Do not notify watchers when during history reindex. | |
// Do not notify watchers during history reindex. |
go/roothash/api/history.go
Outdated
// CommitBatch calls is valid as long as order is respected. | ||
// | ||
// Returns an error if we have already committed a block at higher or equal | ||
// round (and or height). | ||
Commit(blk *AnnotatedBlock, roundResults *RoundResults, notify bool) error |
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.
Commit
is now only used when not doing history reindex right? I believe in that case we always want to notify? Is there a case where notify
would be false
with the new code?
If not, we should maybe just remove the argument and always notify.
go/roothash/api/history.go
Outdated
ConsensusCheckpoint(height int64) error | ||
// Returns an error if we have already committed block at higher or equal | ||
// round (and or height) than the first item in a batch. | ||
CommitBatch(blks []*AnnotatedBlock, roundResults []*RoundResults, notify bool) error |
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.
Is CommitBatch
ever needed with notify
being true? I would expect not, so maybe lets remove the argument and never notify from CommitBatch
?
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.
Makes sense also for the Commit
part.
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.
Done here 089fd7d. If we like I will squash it.
cc @peternose ?
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.
I guess so, since we need to review from the begging anyway.
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.
I think I have found a minor issue inside master
related to this.
Context:
We always notify blocks when processFinalizedEvent
succeeds. Either normal event processing or successful history reindex. The first time this happens is after first reindex, which also marks common committee node as initialized (see).
Rn, after first reindex, there is always a second reindex. Finally in case of any error during normal event processing, this simply marks a block history for reindex.
Problem:
Client committee node waits for initialization to be finished (see). Afterwards, it subscribes to runtime history WatchBlocks
(see), thinking it will receive all blocks from this point on. Since we don't notify during reindex, in extremely unlikely scenario, SubmitTx may be called during subsequent reindex, meaning client worker may never receive blocks for the corresponding transaction, thus never handling SubmitTx
properly.
** Observe we have two different WatchBlocks
methods: 1. and 2. Common committee node is subscribed to first, common client committee node to second.
Solution:
- Do only one initial reindex, repeat until
n
blocks from latest height.- If later normal events processing fails, all subsequent reindexes should notify about the blocks that happen in between...
- Alternatively handle this in client committee worker, by e.g. fetching all missing blocks manually (not best).
If you agree this is problematic I would prefer to tackle this as a follow-up given this is existing (separate issue) that we have. We may want to preserve notify
in the CommitBatch
if you think first bullet is the right solution?
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.
Actually this is not a problem, since if you submit transaction in the middle of n-th reindex (unlikely), this is already past and transaction can only be processed when you catch-up, so you are guaranteed to receive this block after-all...?
go/runtime/history/db.go
Outdated
resVals := make([][]byte, batchSize) | ||
|
||
// Preprocess first block. | ||
if batchSize == 0 { |
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.
It makes more sense to do this check above, right after the existing length checks, before doing any array initializations.
go/runtime/history/db.go
Outdated
} | ||
|
||
first := blks[0] | ||
if first.Block == nil { |
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.
It's reasonable to expect that roothash.AnnotatedBlock
always has Block
field present (unless the API docs explicitly mention it could be null), and panicking in case it does not is probably desired (since something major must be wrong, likely a developer error), so no need for this check.
Same for below in the loop.
We don't check this in other cases, e.g.:
oasis-core/go/worker/client/committee/node.go
Line 223 in f94b700
return hrt.Query(ctx, annBlk.Block, lb, epoch, maxMessages, method, args) |
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.
Makes sense. It should be resolved now.
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.
It's reasonable to expect that
roothash.AnnotatedBlock
always hasBlock
field present (unless the API docs explicitly mention it could be null)
Yes, this is the best practice 👍 A field cannot be nil
, unless explicitly stated. And if it can be nil
, try to change the code (if possible) so that it can't be.
results, err := sc.backend.GetBlockResults(ctx, height) | ||
if err != nil { | ||
// XXX: could soft-fail first few heights in case more heights were | ||
// pruned right after the GetLastRetainedVersion query. | ||
logger.Error("failed to get cometbft block results", | ||
sc.logger.Error("failed to get cometbft block results", | ||
"err", err, | ||
"height", height, | ||
) | ||
return lastRound, fmt.Errorf("failed to get cometbft block results: %w", err) | ||
return 0, fmt.Errorf("failed to get cometbft block results: %w", err) |
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.
I think the minimum interval is 1 second currently, and in practice it's likely much more (e.g. in minutes).
Likely not problematic in practice, so I don't really have strong opinions on how to handle it.
783354e
to
c4808e9
Compare
go/runtime/history/db.go
Outdated
@@ -164,15 +164,15 @@ func (d *DB) commit(blk *roothash.AnnotatedBlock, roundResults *roothash.RoundRe | |||
) | |||
} | |||
|
|||
if blk.Height < meta.LastConsensusHeight { | |||
return fmt.Errorf("runtime/history: commit at lower consensus height (current: %d wanted: %d)", | |||
if blk.Height <= meta.LastConsensusHeight && meta.LastConsensusHeight != 0 { |
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.
This won't work if we have multiple runtime blocks per one consensus block in the future. So why adding this limitation?
I would put these two if
s under another if meta.LastConsensusHeight != 0 {
.
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.
I was trying to prevent receiving multiple rounds for the same height in batch. I see your point that this limits future extension, albeit I should probably still validate for this rn? cc @ptrus
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.
No, I think that previous implementation took into account that multiple blocks can be published on the same height. And now you are throwing that away. In general, I would stay that we shouldn't worry about consensus block heights at all, but didn't think this through. In theory, there could be a runtime which publishes runtime block 2 in height 1 and block 1 in height 2.
if err != nil { | ||
return 0, fmt.Errorf("failed to fetch roothash finalized round: %w", err) | ||
} | ||
err = bh.Commit(annBlk, roundResults, false) |
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.
Now, we don't log sc.logger.Debug("commit block",
for every commit.
func (d *DB) commit(blk *roothash.AnnotatedBlock, roundResults *roothash.RoundResults) error { | ||
func (d *DB) commitBatch(blks []*roothash.AnnotatedBlock, results []*roothash.RoundResults) error { | ||
if len(blks) != len(results) { | ||
return fmt.Errorf("%d blocks != %d round results", len(blks), len(results)) |
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.
This message could be improved, so that error reads like meaningful sentence and not an equation.
// the watchers will be notified about the new block. Disable notify when | ||
// doing reindexing. | ||
// Commit commits an annotated block with corresponding round results | ||
// into block history. |
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.
// into block history. |
go/roothash/api/history.go
Outdated
// | ||
// Must be called in order, sorted by round. | ||
// If notify is set to true, the watchers will be notified about the new block, | ||
// provided node has no local storage. |
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.
What is node, what is local storage from API point of view? Those two things should be unknwon.
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.
We have existing references to this:
oasis-core/go/roothash/api/history.go
Line 43 in f94b700
// If node has local storage this includes waiting for the round to be synced into storage. |
I see your point though.
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.
If I checked correctly, there is no function in the API of the history which uses or mentions nodes or local storage. So giving a comment for a special use case which we use, is not the best.
go/runtime/history/history_test.go
Outdated
}, | ||
} | ||
|
||
err = history.CommitBatch(nil, nil, true) |
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.
This could be wrapped in a subtest so that the name tells you what you are testing.
[]*roothash.RoundResults{results1}, | ||
true, | ||
) | ||
require.Error(err, "CommitBatch should fail when slices don't have equal size") |
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.
It is nice to test the happy path first, unless that complicates things a lot.
return nil | ||
} | ||
|
||
// Transaction should be fast, therefore we pre-process encoding and validation. |
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.
You can create a new commit for this, so that we can revert if needed. Can you test speed with and without these changes, just to be sure we don't slower things.
Would this be also ok?
func (d *DB) commitBatch(blks []*roothash.AnnotatedBlock, results []*roothash.RoundResults) error {
if len(blks) != len(results) {
return fmt.Errorf("%d blocks != %d round results", len(blks), len(results))
}
batchSize := len(blks)
if batchSize == 0 {
return nil
}
// Transaction should be fast, therefore we pre-process encoding and validation.
blkKeys := make([][]byte, batchSize)
blkVals := make([][]byte, batchSize)
resKeys := make([][]byte, batchSize)
resVals := make([][]byte, batchSize)
var lastHeight int64
var lastRound uint64
for i := 0; i < batchSize; i++ {
if rtID := blks[i].Block.Header.Namespace; !rtID.Equal(&d.runtimeID) {
return fmt.Errorf("runtime mismatch, want %s, got %s)", d.runtimeID, rtID)
}
height := blks[i].Height
if height <= lastHeight {
return fmt.Errorf("out-of-order block (height: %d came after %d)", height, lastHeight)
}
round := blks[i].Block.Header.Round
if round <= lastRound {
return fmt.Errorf("out-of-order block (round: %d came after %d)", round, lastRound)
}
lastHeight = height
lastRound = round
blkKeys[i], blkVals[i] = blockKeyFmt.Encode(round), cbor.Marshal(blks[i])
resKeys[i], resVals[i] = roundResultsKeyFmt.Encode(round), cbor.Marshal(results[i])
}
// Commit batch transaction.
return d.db.Update(func(tx *badger.Txn) error {
meta, err := d.queryGetMetadata(tx)
if err != nil {
return err
}
if meta.LastConsensusHeight != 0 {
if firstHeight := blks[0].Height; firstHeight <= meta.LastConsensusHeight {
return fmt.Errorf("commit at lower or equal consensus height (current: %d wanted: %d)",
meta.LastConsensusHeight,
firstHeight,
)
}
if firstRound := blks[0].Block.Header.Round; firstRound <= meta.LastRound {
return fmt.Errorf("commit at lower or equal round (current: %d wanted: %d)",
meta.LastRound,
firstRound,
)
}
}
for i := 0; i < batchSize; i++ {
if err = tx.Set(blkKeys[i], blkVals[i]); err != nil {
return err
}
if err = tx.Set(resKeys[i], resVals[i]); err != nil {
return err
}
}
meta.LastRound = lastRound
meta.LastConsensusHeight = lastHeight
return tx.Set(metadataKeyFmt.Encode(), cbor.Marshal(meta))
})
}
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.
Will bench this tmr, albeit I don't expect major slowdowns here.
I have already benchmarked sorting every batch, before committing, since later when we will fetch data in parallel we will have to sort items first. Luckily it was negligible.
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.
I tested this locally with artificial data and it looked like optimization (similar to the one above) was slower. Have no idea why. So I would test this before committing. But in general, it should still hold that locks should be released as fast as possible.
@@ -505,7 +505,8 @@ func (sc *serviceClient) reindexBlocks(ctx context.Context, currentHeight int64, | |||
if err != nil { | |||
return 0, fmt.Errorf("failed to fetch roothash finalized round: %w", err) | |||
} | |||
err = bh.Commit(annBlk, roundResults, false) | |||
// Do not notify watchers during history reindex. |
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.
This comment is not the best.
// Within a batch, blocks should be sorted by round. Any sequence of Commit | ||
// and CommitBatch is valid as long as order is respected. | ||
// | ||
// Watchers will not be automatically notified about blocks in batch. |
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.
This is very strange. How will watcher react if someone calls Commit
, CommitBatch
, and Commit
. Where are the missing blocks? We should find a neater solution as once database is available for use/watching, all blocks should be reported.
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.
Agree. I have outlined this problem here with possible solutions: #6050 (comment).
Given this is existing issue we have in master
** I suggest to handle this in a follow-up or agree that existing code should be first refactored (possibly to first bullet point under solution section in this comment)?
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.
Yes, this was not handled well before. I don't have a solution for this, but if you have one, it would be nice to implement it here. Thinking out loud, we could implement init phase where we could commit without notifications, we could enable/disable notifications, notify heights instead of blocks, ... not sure what is the best. But if I call this API and get height 1 and afterwards height 1001, I have no idea how my code should react.
Btw, do we ever check that rounds in the batch are sequential, we only check that the next round is higher, right?
Closes #6069
What
During history reindex, we now batch
1000
writes at the same time.Why
As from the benchmarks, this already gives 2-3x performance gain for the single threaded program.
Given its simplicity and the fact, that it will be required by all possible solutions, this is a natural start.