-
Notifications
You must be signed in to change notification settings - Fork 194
wip - initial draft results forest #7398
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
Changes from all commits
ec740f7
2471de6
5a8c9c4
05311e3
ae9e9c7
e641373
c0bf905
391632a
e90b4de
681859a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,186 @@ | ||||||||||||||||||||||||
package ingestion2 | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
"github.com/rs/zerolog" | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
"github.com/onflow/flow-go/consensus/hotstuff" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/consensus/hotstuff/model" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/engine" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/model/flow" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/module" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/module/component" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/module/counters" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/module/irrecoverable" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/module/jobqueue" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/state/protocol" | ||||||||||||||||||||||||
"github.com/onflow/flow-go/storage" | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
type Engine struct { | ||||||||||||||||||||||||
component.Component | ||||||||||||||||||||||||
cm *component.ComponentManager | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
log zerolog.Logger | ||||||||||||||||||||||||
state protocol.State // used to access the protocol state | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
blocks storage.Blocks | ||||||||||||||||||||||||
headers storage.Headers | ||||||||||||||||||||||||
executionReceipts storage.ExecutionReceipts | ||||||||||||||||||||||||
executionResults storage.ExecutionResults | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
lastFullBlockHeight *counters.PersistentStrictMonotonicCounter | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Job queue | ||||||||||||||||||||||||
finalizedBlockConsumer *jobqueue.ComponentConsumer | ||||||||||||||||||||||||
// Notifier for queue consumer | ||||||||||||||||||||||||
finalizedBlockNotifier engine.Notifier | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
resultsForest *ResultsForest | ||||||||||||||||||||||||
maxForestSize uint | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
collectionExecutedMetric module.CollectionExecutedMetric | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
latestPersistedSealedResult *LatestPersistedSealedResult | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
var _ hotstuff.FinalizationConsumer = (*Engine)(nil) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func New( | ||||||||||||||||||||||||
log zerolog.Logger, | ||||||||||||||||||||||||
latestPersistedSealedResult *LatestPersistedSealedResult, | ||||||||||||||||||||||||
) *Engine { | ||||||||||||||||||||||||
resultsForest := NewResultsForest(log, latestPersistedSealedResult) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
e := &Engine{ | ||||||||||||||||||||||||
log: log.With().Str("component", "ingestion").Logger(), | ||||||||||||||||||||||||
resultsForest: resultsForest, | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
cm := component.NewComponentManagerBuilder(). | ||||||||||||||||||||||||
AddWorker(e.runForest). | ||||||||||||||||||||||||
AddWorker(e.runForestLoader). | ||||||||||||||||||||||||
Build() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
e.cm = cm | ||||||||||||||||||||||||
e.Component = cm | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return e | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (e *Engine) runForest(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { | ||||||||||||||||||||||||
e.resultsForest.Start(ctx) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
select { | ||||||||||||||||||||||||
case <-ctx.Done(): | ||||||||||||||||||||||||
case <-e.resultsForest.Ready(): | ||||||||||||||||||||||||
ready() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
<-e.resultsForest.Done() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (e *Engine) runForestLoader(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { | ||||||||||||||||||||||||
loader := NewForestLoader(e.resultsForest, e.latestPersistedSealedResult.ResultID(), e.maxForestSize) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
select { | ||||||||||||||||||||||||
case <-ctx.Done(): | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
case <-e.resultsForest.Ready(): | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
ready() | ||||||||||||||||||||||||
if err := loader.Run(ctx); err != nil { | ||||||||||||||||||||||||
ctx.Throw(err) | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+94
to
+97
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. don't we need to run this repeatedly? 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. no. the approach I took was to run the loader once on startup with these steps:
Then it exits and results are added by the ingestion engine directly in real time. We don't really need to continuously run the loader since we can just add all results into the forest and only start a subset to manage resources. 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. 👍 happy with the ForestLoader not needing to run regularly in the background. A couple follow-up comments regarding the scenario you are describing above:
Overall, the following suggested approach is starting to manifest in my brain, which would address this larger class of problems in its entirety:
This is a lot of text explaining what the algorithm does. Sorry about this wall of text. Though, the algorithm is very simple to implement. 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. What remains is to prove that this algorithm will always make progress (liveness proof), I'll provide a sketch: Liveness Proof 🚧 under construction 🚧The current implementation of the consensus follower guarantees the following happens-before relation (in more intuitive terms causality):
Prerequisite:
🚧 continue this later 🚧 |
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// OnFinalizedBlock is called by the follower engine after a block has been finalized and | ||||||||||||||||||||||||
// the state has been updated. Receives events from the finalization distributor. | ||||||||||||||||||||||||
func (e *Engine) OnFinalizedBlock(*model.Block) { | ||||||||||||||||||||||||
// Per specification of the `hotstuff.FinalizationConsumer` consumers of the `OnBlockIncorporated` notification must | ||||||||||||||||||||||||
// be non-blocking. This code is run on the hotpath of consensus and should induce as little overhead as possible. | ||||||||||||||||||||||||
// | ||||||||||||||||||||||||
// The input is coming from the node-internal consensus follower, which is a trusted component. Hence, we don't | ||||||||||||||||||||||||
// need to verify the inputs and queue them directly for processing by one of the engine's workers. | ||||||||||||||||||||||||
e.finalizedBlockNotifier.Notify() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// OnBlockIncorporated is called by the follower engine after a block has been certified and the state has been updated. | ||||||||||||||||||||||||
// Receives block incorporated events from the finalization distributor. | ||||||||||||||||||||||||
func (e *Engine) OnBlockIncorporated(hotstuffBlock *model.Block) { | ||||||||||||||||||||||||
// Per specification of the `hotstuff.FinalizationConsumer` consumers of the `OnBlockIncorporated` notification must | ||||||||||||||||||||||||
// be non-blocking. This code is run on the hotpath of consensus and should induce as little overhead as possible. | ||||||||||||||||||||||||
// | ||||||||||||||||||||||||
// The input is coming from the node-internal consensus follower, which is a trusted component. Hence, we don't | ||||||||||||||||||||||||
// need to verify the inputs and queue them directly for processing by one of the engine's workers. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// ToDO: queue incoming incorporated hotstuffBlock for processing in a dedicated pipeline | ||||||||||||||||||||||||
// The thread picking up the hotstuffBlock would then convert it to `flow.block` and process it further | ||||||||||||||||||||||||
// | ||||||||||||||||||||||||
// block, err := e.blocks.ByID(hotstuffBlock.BlockID) | ||||||||||||||||||||||||
// if err != nil { | ||||||||||||||||||||||||
// return irrecoverable.NewExceptionf("received incorporated block %s from consensus follower, but failed to retrieve full block: %w", err) | ||||||||||||||||||||||||
// } | ||||||||||||||||||||||||
// err = e.processCertifiedBlock(block) | ||||||||||||||||||||||||
// ... | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// processCertifiedBlock adds results from the certified block to the results forest. | ||||||||||||||||||||||||
// No errors are expected during normal operation. | ||||||||||||||||||||||||
func (e *Engine) processCertifiedBlock(block *flow.Block) error { | ||||||||||||||||||||||||
for _, result := range block.Payload.Results { | ||||||||||||||||||||||||
if err := e.resultsForest.AddResult(result, block.Header, false); err != nil { | ||||||||||||||||||||||||
return fmt.Errorf("could not add result %s to forest: %w", result.ID(), err) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
peterargue marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
// processFinalizedBlock handles an incoming finalized block. | ||||||||||||||||||||||||
// It processes the block, indexes it for further processing, and requests missing collections if necessary. | ||||||||||||||||||||||||
// | ||||||||||||||||||||||||
// Expected errors during normal operation: | ||||||||||||||||||||||||
// - storage.ErrNotFound - if last full block height does not exist in the database. | ||||||||||||||||||||||||
// - storage.ErrAlreadyExists - if the collection within block or an execution result ID already exists in the database. | ||||||||||||||||||||||||
// - generic error in case of unexpected failure from the database layer, or failure | ||||||||||||||||||||||||
// to decode an existing database value. | ||||||||||||||||||||||||
func (e *Engine) processFinalizedBlock(block *flow.Block) error { | ||||||||||||||||||||||||
// index the block storage with each of the collection guarantee | ||||||||||||||||||||||||
err := e.blocks.IndexBlockForCollections(block.Header.ID(), flow.GetIDs(block.Payload.Guarantees)) | ||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||
return fmt.Errorf("could not index block for collections: %w", err) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// index sealed results and notify the results forest | ||||||||||||||||||||||||
for _, seal := range block.Payload.Seals { | ||||||||||||||||||||||||
if err := e.executionResults.Index(seal.BlockID, seal.ResultID); err != nil { | ||||||||||||||||||||||||
return fmt.Errorf("could not index block for execution result (id: %s): %w", seal.ResultID, err) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if err := e.resultsForest.OnResultSealed(seal.ResultID); err != nil { | ||||||||||||||||||||||||
return fmt.Errorf("could not notify results forest of newly sealed result (id: %s): %w", seal.ResultID, err) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+163
to
+166
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 like that you have worker threads in the Engine, I think it would be good to have them serve the |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
e.collectionExecutedMetric.BlockFinalized(block) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// handleExecutionReceipt persists the execution receipt locally. | ||||||||||||||||||||||||
// Storing the execution receipt and updates the collection executed metric. | ||||||||||||||||||||||||
// | ||||||||||||||||||||||||
// No errors are expected during normal operation. | ||||||||||||||||||||||||
func (e *Engine) handleExecutionReceipt(receipt *flow.ExecutionReceipt) error { | ||||||||||||||||||||||||
// persist the execution receipt locally, storing will also index the receipt | ||||||||||||||||||||||||
err := e.executionReceipts.Store(receipt) | ||||||||||||||||||||||||
Comment on lines
+178
to
+179
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 think execution receipts are already persisted and indexed by the consensus follower. Is the AN doing extra stuff when storing the receipt? 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. this is copied from the existing ingestion engine and needs to be updated. currently, we just store all receipts received from ENs immediately. This will be skipped for now and eventually we could use the size limited cache. |
||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||
return fmt.Errorf("failed to store execution receipt: %w", err) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
e.collectionExecutedMetric.ExecutionReceiptReceived(receipt) | ||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package ingestion2 | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/onflow/flow-go/model/flow" | ||
pipeline "github.com/onflow/flow-go/module/executiondatasync/optimistic_syncing" | ||
"github.com/onflow/flow-go/module/forest" | ||
) | ||
|
||
var _ forest.Vertex = (*ExecutionResultContainer)(nil) | ||
|
||
// ExecutionResultContainer represents an ExecutionResult within the LevelledForest. | ||
// Implements LevelledForest's Vertex interface. | ||
type ExecutionResultContainer struct { | ||
result *flow.ExecutionResult | ||
resultID flow.Identifier // precomputed ID of result to avoid expensive hashing on each call | ||
blockHeader *flow.Header // header of the block which the result is for | ||
pipeline pipeline.Pipeline | ||
} | ||
|
||
// NewExecutionResultContainer instantiates an empty Equivalence Class (without any receipts) | ||
// No errors are expected during normal operation. | ||
func NewExecutionResultContainer( | ||
result *flow.ExecutionResult, | ||
header *flow.Header, | ||
pipeline pipeline.Pipeline, | ||
) (*ExecutionResultContainer, error) { | ||
// sanity check: initial result should be for block | ||
if header.ID() != result.BlockID { | ||
return nil, fmt.Errorf("initial result is for different block") | ||
} | ||
|
||
// construct ExecutionResultContainer only containing initialReceipt | ||
return &ExecutionResultContainer{ | ||
result: result, | ||
resultID: result.ID(), | ||
blockHeader: header, | ||
pipeline: pipeline, | ||
}, nil | ||
} | ||
|
||
// VertexID returns the ExecutionResult ID of the ExecutionResultContainer. | ||
func (c *ExecutionResultContainer) VertexID() flow.Identifier { return c.resultID } | ||
|
||
// Level returns the View of the block the ExecutionResult is associated with. | ||
func (c *ExecutionResultContainer) Level() uint64 { return c.blockHeader.View } | ||
|
||
// Parent returns the ID and view of the parent result. | ||
func (c *ExecutionResultContainer) Parent() (flow.Identifier, uint64) { | ||
return c.result.PreviousResultID, c.blockHeader.ParentView | ||
} |
Uh oh!
There was an error while loading. Please reload this page.