go-ethereum/core/blockstm/executor.go

624 lines
15 KiB
Go

package blockstm
import (
"container/heap"
"context"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type ExecResult struct {
err error
ver Version
txIn TxnInput
txOut TxnOutput
txAllOut TxnOutput
}
type ExecTask interface {
Execute(mvh *MVHashMap, incarnation int) error
MVReadList() []ReadDescriptor
MVWriteList() []WriteDescriptor
MVFullWriteList() []WriteDescriptor
Hash() common.Hash
Sender() common.Address
Settle()
Dependencies() []int
}
type ExecVersionView struct {
ver Version
et ExecTask
mvh *MVHashMap
sender common.Address
}
var NumSpeculativeProcs int = 8
func SetProcs(specProcs int) {
NumSpeculativeProcs = specProcs
}
func (ev *ExecVersionView) Execute() (er ExecResult) {
er.ver = ev.ver
if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil {
return
}
er.txIn = ev.et.MVReadList()
er.txOut = ev.et.MVWriteList()
er.txAllOut = ev.et.MVFullWriteList()
return
}
type ErrExecAbortError struct {
Dependency int
OriginError error
}
func (e ErrExecAbortError) Error() string {
if e.Dependency >= 0 {
return fmt.Sprintf("Execution aborted due to dependency %d", e.Dependency)
} else {
return "Execution aborted"
}
}
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
type SafeQueue interface {
Push(v int, d interface{})
Pop() interface{}
Len() int
}
type SafeFIFOQueue struct {
c chan interface{}
}
func NewSafeFIFOQueue(capacity int) *SafeFIFOQueue {
return &SafeFIFOQueue{
c: make(chan interface{}, capacity),
}
}
func (q *SafeFIFOQueue) Push(v int, d interface{}) {
q.c <- d
}
func (q *SafeFIFOQueue) Pop() interface{} {
return <-q.c
}
func (q *SafeFIFOQueue) Len() int {
return len(q.c)
}
// A thread safe priority queue
type SafePriorityQueue struct {
m sync.Mutex
queue *IntHeap
data map[int]interface{}
}
func NewSafePriorityQueue(capacity int) *SafePriorityQueue {
q := make(IntHeap, 0, capacity)
return &SafePriorityQueue{
m: sync.Mutex{},
queue: &q,
data: make(map[int]interface{}, capacity),
}
}
func (pq *SafePriorityQueue) Push(v int, d interface{}) {
pq.m.Lock()
heap.Push(pq.queue, v)
pq.data[v] = d
pq.m.Unlock()
}
func (pq *SafePriorityQueue) Pop() interface{} {
pq.m.Lock()
defer pq.m.Unlock()
v := heap.Pop(pq.queue).(int)
return pq.data[v]
}
func (pq *SafePriorityQueue) Len() int {
return pq.queue.Len()
}
type ParallelExecutionResult struct {
TxIO *TxnInputOutput
Stats *map[int]ExecutionStat
Deps *DAG
AllDeps map[int]map[int]bool
}
const numGoProcs = 1
type ParallelExecutor struct {
tasks []ExecTask
// Stores the execution statistics for the last incarnation of each task
stats map[int]ExecutionStat
statsMutex sync.Mutex
// Channel for tasks that should be prioritized
chTasks chan ExecVersionView
// Channel for speculative tasks
chSpeculativeTasks chan struct{}
// Channel to signal that the result of a transaction could be written to storage
specTaskQueue SafeQueue
// A priority queue that stores speculative tasks
chSettle chan int
// Channel to signal that a transaction has finished executing
chResults chan struct{}
// A priority queue that stores the transaction index of results, so we can validate the results in order
resultQueue SafeQueue
// A wait group to wait for all settling tasks to finish
settleWg sync.WaitGroup
// An integer that tracks the index of last settled transaction
lastSettled int
// For a task that runs only after all of its preceding tasks have finished and passed validation,
// its result will be absolutely valid and therefore its validation could be skipped.
// This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid).
skipCheck map[int]bool
// Execution tasks stores the state of each execution task
execTasks taskStatusManager
// Validate tasks stores the state of each validation task
validateTasks taskStatusManager
// Stats for debugging purposes
cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
diagExecSuccess, diagExecAbort []int
// Multi-version hash map
mvh *MVHashMap
// Stores the inputs and outputs of the last incardanotion of all transactions
lastTxIO *TxnInputOutput
// Tracks the incarnation number of each transaction
txIncarnations []int
// A map that stores the estimated dependency of a transaction if it is aborted without any known dependency
estimateDeps map[int][]int
// A map that records whether a transaction result has been speculatively validated
preValidated map[int]bool
// Time records when the parallel execution starts
begin time.Time
// Enable profiling
profile bool
// Worker wait group
workerWg sync.WaitGroup
}
type ExecutionStat struct {
TxIdx int
Incarnation int
Start uint64
End uint64
Worker int
}
func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *ParallelExecutor {
numTasks := len(tasks)
var resultQueue SafeQueue
var specTaskQueue SafeQueue
if metadata {
resultQueue = NewSafeFIFOQueue(numTasks)
specTaskQueue = NewSafeFIFOQueue(numTasks)
} else {
resultQueue = NewSafePriorityQueue(numTasks)
specTaskQueue = NewSafePriorityQueue(numTasks)
}
pe := &ParallelExecutor{
tasks: tasks,
stats: make(map[int]ExecutionStat, numTasks),
chTasks: make(chan ExecVersionView, numTasks),
chSpeculativeTasks: make(chan struct{}, numTasks),
chSettle: make(chan int, numTasks),
chResults: make(chan struct{}, numTasks),
specTaskQueue: specTaskQueue,
resultQueue: resultQueue,
lastSettled: -1,
skipCheck: make(map[int]bool),
execTasks: makeStatusManager(numTasks),
validateTasks: makeStatusManager(0),
diagExecSuccess: make([]int, numTasks),
diagExecAbort: make([]int, numTasks),
mvh: MakeMVHashMap(),
lastTxIO: MakeTxnInputOutput(numTasks),
txIncarnations: make([]int, numTasks),
estimateDeps: make(map[int][]int),
preValidated: make(map[int]bool),
begin: time.Now(),
profile: profile,
}
return pe
}
// nolint: gocognit
func (pe *ParallelExecutor) Prepare() {
prevSenderTx := make(map[common.Address]int)
for i, t := range pe.tasks {
clearPendingFlag := false
pe.skipCheck[i] = false
pe.estimateDeps[i] = make([]int, 0)
if len(t.Dependencies()) > 0 {
for _, val := range t.Dependencies() {
clearPendingFlag = true
pe.execTasks.addDependencies(val, i)
}
if clearPendingFlag {
pe.execTasks.clearPending(i)
clearPendingFlag = false
}
} else {
if tx, ok := prevSenderTx[t.Sender()]; ok {
pe.execTasks.addDependencies(tx, i)
pe.execTasks.clearPending(i)
}
prevSenderTx[t.Sender()] = i
}
}
pe.workerWg.Add(NumSpeculativeProcs + numGoProcs)
// Launch workers that execute transactions
for i := 0; i < NumSpeculativeProcs+numGoProcs; i++ {
go func(procNum int) {
defer pe.workerWg.Done()
doWork := func(task ExecVersionView) {
start := time.Duration(0)
if pe.profile {
start = time.Since(pe.begin)
}
res := task.Execute()
if res.err == nil {
pe.mvh.FlushMVWriteSet(res.txAllOut)
}
pe.resultQueue.Push(res.ver.TxnIndex, res)
pe.chResults <- struct{}{}
if pe.profile {
end := time.Since(pe.begin)
pe.statsMutex.Lock()
pe.stats[res.ver.TxnIndex] = ExecutionStat{
TxIdx: res.ver.TxnIndex,
Incarnation: res.ver.Incarnation,
Start: uint64(start),
End: uint64(end),
Worker: procNum,
}
pe.statsMutex.Unlock()
}
}
if procNum < NumSpeculativeProcs {
for range pe.chSpeculativeTasks {
doWork(pe.specTaskQueue.Pop().(ExecVersionView))
}
} else {
for task := range pe.chTasks {
doWork(task)
}
}
}(i)
}
pe.settleWg.Add(1)
go func() {
for t := range pe.chSettle {
pe.tasks[t].Settle()
}
pe.settleWg.Done()
}()
// bootstrap first execution
tx := pe.execTasks.takeNextPending()
if tx != -1 {
pe.cntExec++
pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()}
}
}
func (pe *ParallelExecutor) Close(wait bool) {
close(pe.chTasks)
close(pe.chSpeculativeTasks)
close(pe.chSettle)
if wait {
pe.workerWg.Wait()
}
if wait {
pe.settleWg.Wait()
}
}
// nolint: gocognit
func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResult, err error) {
tx := res.ver.TxnIndex
if abortErr, ok := res.err.(ErrExecAbortError); ok && abortErr.OriginError != nil && pe.skipCheck[tx] {
// If the transaction failed when we know it should not fail, this means the transaction itself is
// bad (e.g. wrong nonce), and we should exit the execution immediately
err = fmt.Errorf("could not apply tx %d [%v]: %w", tx, pe.tasks[tx].Hash(), abortErr.OriginError)
pe.Close(true)
return
}
// nolint: nestif
if execErr, ok := res.err.(ErrExecAbortError); ok {
addedDependencies := false
if execErr.Dependency >= 0 {
l := len(pe.estimateDeps[tx])
for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency {
pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1])
pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1]
l--
}
addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx)
} else {
estimate := 0
if len(pe.estimateDeps[tx]) > 0 {
estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1]
}
addedDependencies = pe.execTasks.addDependencies(estimate, tx)
newEstimate := estimate + (estimate+tx)/2
if newEstimate >= tx {
newEstimate = tx - 1
}
pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate)
}
pe.execTasks.clearInProgress(tx)
if !addedDependencies {
pe.execTasks.pushPending(tx)
}
pe.txIncarnations[tx]++
pe.diagExecAbort[tx]++
pe.cntAbort++
} else {
pe.lastTxIO.recordRead(tx, res.txIn)
if res.ver.Incarnation == 0 {
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
} else {
if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) {
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
}
prevWrite := pe.lastTxIO.AllWriteSet(tx)
// Remove entries that were previously written but are no longer written
cmpMap := make(map[Key]bool)
for _, w := range res.txAllOut {
cmpMap[w.Path] = true
}
for _, v := range prevWrite {
if _, ok := cmpMap[v.Path]; !ok {
pe.mvh.Delete(v.Path, tx)
}
}
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
}
pe.validateTasks.pushPending(tx)
pe.execTasks.markComplete(tx)
pe.diagExecSuccess[tx]++
pe.cntSuccess++
pe.execTasks.removeDependency(tx)
}
// do validations ...
maxComplete := pe.execTasks.maxAllComplete()
toValidate := make([]int, 0, 2)
for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 {
toValidate = append(toValidate, pe.validateTasks.takeNextPending())
}
for i := 0; i < len(toValidate); i++ {
pe.cntTotalValidations++
tx := toValidate[i]
if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) {
pe.validateTasks.markComplete(tx)
} else {
pe.cntValidationFail++
pe.diagExecAbort[tx]++
for _, v := range pe.lastTxIO.AllWriteSet(tx) {
pe.mvh.MarkEstimate(v.Path, tx)
}
// 'create validation tasks for all transactions > tx ...'
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
pe.execTasks.clearComplete(tx)
pe.execTasks.pushPending(tx)
pe.preValidated[tx] = false
pe.txIncarnations[tx]++
}
}
// Settle transactions that have been validated to be correct and that won't be re-executed again
maxValidated := pe.validateTasks.maxAllComplete()
for pe.lastSettled < maxValidated {
pe.lastSettled++
if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) {
pe.lastSettled--
break
}
pe.chSettle <- pe.lastSettled
}
if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) {
log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100))
pe.Close(true)
var allDeps map[int]map[int]bool
if pe.profile {
allDeps = GetDep(*pe.lastTxIO)
}
return ParallelExecutionResult{pe.lastTxIO, &pe.stats, nil, allDeps}, err
}
// Send the next immediate pending transaction to be executed
if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 {
nextTx := pe.execTasks.takeNextPending()
if nextTx != -1 {
pe.cntExec++
pe.skipCheck[nextTx] = true
pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
}
}
// Send speculative tasks
for pe.execTasks.minPending() != -1 {
nextTx := pe.execTasks.takeNextPending()
if nextTx != -1 {
pe.cntExec++
task := ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
pe.specTaskQueue.Push(nextTx, task)
pe.chSpeculativeTasks <- struct{}{}
}
}
return
}
type PropertyCheck func(*ParallelExecutor) error
func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) {
if len(tasks) == 0 {
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil
}
pe := NewParallelExecutor(tasks, profile, metadata)
pe.Prepare()
for range pe.chResults {
if interruptCtx != nil {
select {
case <-interruptCtx.Done():
pe.Close(true)
return result, interruptCtx.Err()
default:
}
}
res := pe.resultQueue.Pop().(ExecResult)
result, err = pe.Step(&res)
if err != nil {
return result, err
}
if check != nil {
err = check(pe)
}
if result.TxIO != nil || err != nil {
return result, err
}
}
return
}
func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) {
return executeParallelWithCheck(tasks, profile, nil, metadata, interruptCtx)
}