mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-30 00:23:46 +00:00
Added tests for executor and some improvements: 1. Add a dependency map during execution. This will prevent aborted tasks from being sent for execution immedaitely after failure. 2. Change the key of MVHashMap from string to a byte array. This will reduce time to convert byte slices to strings. 3. Use sync.Map to reduce the time spent in global mutex. 4. Skip applying intermediate states. 5. Estimate dependency when an execution fails without dependency information. 6. Divide execution task queue into two separate queues. One for relatively certain transactions, and the other for speculative future transactions. 7. Setting dependencies of Txs coming from the same sender before starting parallel execution. 8. Process results in their semantic order (transaction index) instead of the order when they arrive. Replace result channel with a priority queue.
539 lines
13 KiB
Go
539 lines
13 KiB
Go
package blockstm
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"sort"
|
|
"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
|
|
Sender() common.Address
|
|
Settle()
|
|
}
|
|
|
|
type ExecVersionView struct {
|
|
ver Version
|
|
et ExecTask
|
|
mvh *MVHashMap
|
|
sender common.Address
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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 *[][]uint64
|
|
Deps *DAG
|
|
}
|
|
|
|
const numGoProcs = 2
|
|
const numSpeculativeProcs = 16
|
|
|
|
// Max number of pre-validation to run per loop
|
|
const preValidateLimit = 5
|
|
|
|
// Max number of times a transaction (t) can be executed before its dependency is resolved to its previous tx (t-1)
|
|
const maxIncarnation = 2
|
|
|
|
// nolint: gocognit
|
|
// A stateless executor that executes transactions in parallel
|
|
func ExecuteParallel(tasks []ExecTask, profile bool) (ParallelExecutionResult, error) {
|
|
if len(tasks) == 0 {
|
|
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil
|
|
}
|
|
|
|
// Stores the execution statistics for each task
|
|
stats := make([][]uint64, 0, len(tasks))
|
|
statsMutex := sync.Mutex{}
|
|
|
|
// Channel for tasks that should be prioritized
|
|
chTasks := make(chan ExecVersionView, len(tasks))
|
|
|
|
// Channel for speculative tasks
|
|
chSpeculativeTasks := make(chan struct{}, len(tasks))
|
|
|
|
// A priority queue that stores speculative tasks
|
|
specTaskQueue := NewSafePriorityQueue(len(tasks))
|
|
|
|
// Channel to signal that the result of a transaction could be written to storage
|
|
chSettle := make(chan int, len(tasks))
|
|
|
|
// Channel to signal that a transaction has finished executing
|
|
chResults := make(chan struct{}, len(tasks))
|
|
|
|
// A priority queue that stores the transaction index of results, so we can validate the results in order
|
|
resultQueue := NewSafePriorityQueue(len(tasks))
|
|
|
|
// A wait group to wait for all settling tasks to finish
|
|
var settleWg sync.WaitGroup
|
|
|
|
// An integer that tracks the index of last settled transaction
|
|
lastSettled := -1
|
|
|
|
// 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 := make(map[int]bool)
|
|
|
|
for i := 0; i < len(tasks); i++ {
|
|
skipCheck[i] = false
|
|
}
|
|
|
|
// Execution tasks stores the state of each execution task
|
|
execTasks := makeStatusManager(len(tasks))
|
|
|
|
// Validate tasks stores the state of each validation task
|
|
validateTasks := makeStatusManager(0)
|
|
|
|
// Stats for debugging purposes
|
|
var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
|
|
|
|
diagExecSuccess := make([]int, len(tasks))
|
|
diagExecAbort := make([]int, len(tasks))
|
|
|
|
// Initialize MVHashMap
|
|
mvh := MakeMVHashMap()
|
|
|
|
// Stores the inputs and outputs of the last incardanotion of all transactions
|
|
lastTxIO := MakeTxnInputOutput(len(tasks))
|
|
|
|
// Tracks the incarnation number of each transaction
|
|
txIncarnations := make([]int, len(tasks))
|
|
|
|
// A map that stores the estimated dependency of a transaction if it is aborted without any known dependency
|
|
estimateDeps := make(map[int][]int, len(tasks))
|
|
|
|
for i := 0; i < len(tasks); i++ {
|
|
estimateDeps[i] = make([]int, 0)
|
|
}
|
|
|
|
// A map that records whether a transaction result has been speculatively validated
|
|
preValidated := make(map[int]bool, len(tasks))
|
|
|
|
begin := time.Now()
|
|
|
|
workerWg := sync.WaitGroup{}
|
|
workerWg.Add(numSpeculativeProcs + numGoProcs)
|
|
|
|
// Launch workers that execute transactions
|
|
for i := 0; i < numSpeculativeProcs+numGoProcs; i++ {
|
|
go func(procNum int) {
|
|
defer workerWg.Done()
|
|
|
|
doWork := func(task ExecVersionView) {
|
|
start := time.Duration(0)
|
|
if profile {
|
|
start = time.Since(begin)
|
|
}
|
|
|
|
res := task.Execute()
|
|
|
|
if res.err == nil {
|
|
mvh.FlushMVWriteSet(res.txAllOut)
|
|
}
|
|
|
|
resultQueue.Push(res.ver.TxnIndex, res)
|
|
chResults <- struct{}{}
|
|
|
|
if profile {
|
|
end := time.Since(begin)
|
|
|
|
stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)}
|
|
|
|
statsMutex.Lock()
|
|
stats = append(stats, stat)
|
|
statsMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
if procNum < numSpeculativeProcs {
|
|
for range chSpeculativeTasks {
|
|
doWork(specTaskQueue.Pop().(ExecVersionView))
|
|
}
|
|
} else {
|
|
for task := range chTasks {
|
|
doWork(task)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Launch a worker that settles valid transactions
|
|
settleWg.Add(len(tasks))
|
|
|
|
go func() {
|
|
for t := range chSettle {
|
|
tasks[t].Settle()
|
|
settleWg.Done()
|
|
}
|
|
}()
|
|
|
|
// bootstrap first execution
|
|
tx := execTasks.takeNextPending()
|
|
if tx != -1 {
|
|
cntExec++
|
|
|
|
chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()}
|
|
}
|
|
|
|
// Before starting execution, going through each task to check their explicit dependencies (whether they are coming from the same account)
|
|
prevSenderTx := make(map[common.Address]int)
|
|
|
|
for i, t := range tasks {
|
|
if tx, ok := prevSenderTx[t.Sender()]; ok {
|
|
execTasks.addDependencies(tx, i)
|
|
execTasks.clearPending(i)
|
|
}
|
|
|
|
prevSenderTx[t.Sender()] = i
|
|
}
|
|
|
|
var res ExecResult
|
|
|
|
var err error
|
|
|
|
// Start main validation loop
|
|
// nolint:nestif
|
|
for range chResults {
|
|
res = resultQueue.Pop().(ExecResult)
|
|
tx := res.ver.TxnIndex
|
|
|
|
if res.err == nil {
|
|
lastTxIO.recordRead(tx, res.txIn)
|
|
|
|
if res.ver.Incarnation == 0 {
|
|
lastTxIO.recordWrite(tx, res.txOut)
|
|
lastTxIO.recordAllWrite(tx, res.txAllOut)
|
|
} else {
|
|
if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(tx)) {
|
|
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
|
}
|
|
|
|
prevWrite := 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 {
|
|
mvh.Delete(v.Path, tx)
|
|
}
|
|
}
|
|
|
|
lastTxIO.recordWrite(tx, res.txOut)
|
|
lastTxIO.recordAllWrite(tx, res.txAllOut)
|
|
}
|
|
|
|
validateTasks.pushPending(tx)
|
|
execTasks.markComplete(tx)
|
|
diagExecSuccess[tx]++
|
|
cntSuccess++
|
|
|
|
execTasks.removeDependency(tx)
|
|
} else if execErr, ok := res.err.(ErrExecAbortError); ok {
|
|
|
|
addedDependencies := false
|
|
|
|
if execErr.Dependency >= 0 {
|
|
l := len(estimateDeps[tx])
|
|
for l > 0 && estimateDeps[tx][l-1] > execErr.Dependency {
|
|
execTasks.removeDependency(estimateDeps[tx][l-1])
|
|
estimateDeps[tx] = estimateDeps[tx][:l-1]
|
|
l--
|
|
}
|
|
if txIncarnations[tx] < maxIncarnation {
|
|
addedDependencies = execTasks.addDependencies(execErr.Dependency, tx)
|
|
} else {
|
|
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
|
}
|
|
} else {
|
|
estimate := 0
|
|
|
|
if len(estimateDeps[tx]) > 0 {
|
|
estimate = estimateDeps[tx][len(estimateDeps[tx])-1]
|
|
}
|
|
addedDependencies = execTasks.addDependencies(estimate, tx)
|
|
newEstimate := estimate + (estimate+tx)/2
|
|
if newEstimate >= tx {
|
|
newEstimate = tx - 1
|
|
}
|
|
estimateDeps[tx] = append(estimateDeps[tx], newEstimate)
|
|
}
|
|
|
|
execTasks.clearInProgress(tx)
|
|
if !addedDependencies {
|
|
execTasks.pushPending(tx)
|
|
}
|
|
txIncarnations[tx]++
|
|
diagExecAbort[tx]++
|
|
cntAbort++
|
|
} else {
|
|
err = res.err
|
|
break
|
|
}
|
|
|
|
// do validations ...
|
|
maxComplete := execTasks.maxAllComplete()
|
|
|
|
var toValidate []int
|
|
|
|
for validateTasks.minPending() <= maxComplete && validateTasks.minPending() >= 0 {
|
|
toValidate = append(toValidate, validateTasks.takeNextPending())
|
|
}
|
|
|
|
for i := 0; i < len(toValidate); i++ {
|
|
cntTotalValidations++
|
|
|
|
tx := toValidate[i]
|
|
|
|
if skipCheck[tx] || ValidateVersion(tx, lastTxIO, mvh) {
|
|
validateTasks.markComplete(tx)
|
|
} else {
|
|
cntValidationFail++
|
|
diagExecAbort[tx]++
|
|
for _, v := range lastTxIO.AllWriteSet(tx) {
|
|
mvh.MarkEstimate(v.Path, tx)
|
|
}
|
|
// 'create validation tasks for all transactions > tx ...'
|
|
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
|
validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
|
|
|
|
addedDependencies := false
|
|
if txIncarnations[tx] >= maxIncarnation {
|
|
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
|
}
|
|
|
|
execTasks.clearComplete(tx)
|
|
if !addedDependencies {
|
|
execTasks.pushPending(tx)
|
|
}
|
|
|
|
preValidated[tx] = false
|
|
txIncarnations[tx]++
|
|
}
|
|
}
|
|
|
|
preValidateCount := 0
|
|
invalidated := []int{}
|
|
|
|
i := sort.SearchInts(validateTasks.pending, maxComplete+1)
|
|
|
|
for i < len(validateTasks.pending) && preValidateCount < preValidateLimit {
|
|
tx := validateTasks.pending[i]
|
|
|
|
if !preValidated[tx] {
|
|
cntTotalValidations++
|
|
|
|
if !ValidateVersion(tx, lastTxIO, mvh) {
|
|
cntValidationFail++
|
|
diagExecAbort[tx]++
|
|
|
|
invalidated = append(invalidated, tx)
|
|
|
|
if execTasks.checkComplete(tx) {
|
|
execTasks.clearComplete(tx)
|
|
}
|
|
|
|
if !execTasks.checkInProgress(tx) {
|
|
for _, v := range lastTxIO.AllWriteSet(tx) {
|
|
mvh.MarkEstimate(v.Path, tx)
|
|
}
|
|
|
|
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
|
|
|
addedDependencies := false
|
|
if txIncarnations[tx] >= maxIncarnation {
|
|
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
|
}
|
|
|
|
if !addedDependencies {
|
|
execTasks.pushPending(tx)
|
|
}
|
|
}
|
|
|
|
txIncarnations[tx]++
|
|
|
|
preValidated[tx] = false
|
|
} else {
|
|
preValidated[tx] = true
|
|
}
|
|
preValidateCount++
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
for _, tx := range invalidated {
|
|
validateTasks.clearPending(tx)
|
|
}
|
|
|
|
// Settle transactions that have been validated to be correct and that won't be re-executed again
|
|
maxValidated := validateTasks.maxAllComplete()
|
|
|
|
for lastSettled < maxValidated {
|
|
lastSettled++
|
|
if execTasks.checkInProgress(lastSettled) || execTasks.checkPending(lastSettled) || execTasks.blockCount[lastSettled] >= 0 {
|
|
lastSettled--
|
|
break
|
|
}
|
|
chSettle <- lastSettled
|
|
}
|
|
|
|
if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) {
|
|
log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(tasks))/float64(cntExec)*100))
|
|
break
|
|
}
|
|
|
|
// Send the next immediate pending transaction to be executed
|
|
if execTasks.minPending() != -1 && execTasks.minPending() == maxValidated+1 {
|
|
nextTx := execTasks.takeNextPending()
|
|
if nextTx != -1 {
|
|
cntExec++
|
|
|
|
skipCheck[nextTx] = true
|
|
|
|
chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()}
|
|
}
|
|
}
|
|
|
|
// Send speculative tasks
|
|
for execTasks.peekPendingGE(maxValidated+3) != -1 || len(execTasks.inProgress) == 0 {
|
|
// We skip the next transaction to avoid the case where they all have conflicts and could not be
|
|
// scheduled for re-execution immediately even when it's their time to run, because they are already in
|
|
// speculative queue.
|
|
nextTx := execTasks.takePendingGE(maxValidated + 3)
|
|
|
|
if nextTx == -1 {
|
|
nextTx = execTasks.takeNextPending()
|
|
}
|
|
|
|
if nextTx != -1 {
|
|
cntExec++
|
|
|
|
task := ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()}
|
|
|
|
specTaskQueue.Push(nextTx, task)
|
|
chSpeculativeTasks <- struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
close(chTasks)
|
|
close(chSpeculativeTasks)
|
|
workerWg.Wait()
|
|
close(chResults)
|
|
settleWg.Wait()
|
|
close(chSettle)
|
|
|
|
var dag DAG
|
|
if profile {
|
|
dag = BuildDAG(*lastTxIO)
|
|
}
|
|
|
|
return ParallelExecutionResult{lastTxIO, &stats, &dag}, err
|
|
}
|