mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-10 22:13:47 +00:00
Refactor blockstm executor
This commit is contained in:
parent
d107c183b8
commit
471afc8da2
5 changed files with 456 additions and 417 deletions
|
|
@ -3,7 +3,6 @@ package blockstm
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
"container/heap"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -127,413 +126,402 @@ type ParallelExecutionResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const numGoProcs = 2
|
const numGoProcs = 2
|
||||||
const numSpeculativeProcs = 16
|
const numSpeculativeProcs = 8
|
||||||
|
|
||||||
// Max number of pre-validation to run per loop
|
type ParallelExecutor struct {
|
||||||
const preValidateLimit = 5
|
tasks []ExecTask
|
||||||
|
|
||||||
// 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
|
// Stores the execution statistics for each task
|
||||||
stats := make([][]uint64, 0, len(tasks))
|
stats [][]uint64
|
||||||
statsMutex := sync.Mutex{}
|
statsMutex sync.Mutex
|
||||||
|
|
||||||
// Channel for tasks that should be prioritized
|
// Channel for tasks that should be prioritized
|
||||||
chTasks := make(chan ExecVersionView, len(tasks))
|
chTasks chan ExecVersionView
|
||||||
|
|
||||||
// Channel for speculative tasks
|
// Channel for speculative tasks
|
||||||
chSpeculativeTasks := make(chan struct{}, len(tasks))
|
chSpeculativeTasks chan struct{}
|
||||||
|
|
||||||
// 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
|
// Channel to signal that the result of a transaction could be written to storage
|
||||||
chSettle := make(chan int, len(tasks))
|
specTaskQueue *SafePriorityQueue
|
||||||
|
|
||||||
|
// A priority queue that stores speculative tasks
|
||||||
|
chSettle chan int
|
||||||
|
|
||||||
// Channel to signal that a transaction has finished executing
|
// Channel to signal that a transaction has finished executing
|
||||||
chResults := make(chan struct{}, len(tasks))
|
chResults chan struct{}
|
||||||
|
|
||||||
// A priority queue that stores the transaction index of results, so we can validate the results in order
|
// A priority queue that stores the transaction index of results, so we can validate the results in order
|
||||||
resultQueue := NewSafePriorityQueue(len(tasks))
|
resultQueue *SafePriorityQueue
|
||||||
|
|
||||||
// A wait group to wait for all settling tasks to finish
|
// A wait group to wait for all settling tasks to finish
|
||||||
var settleWg sync.WaitGroup
|
settleWg sync.WaitGroup
|
||||||
|
|
||||||
// An integer that tracks the index of last settled transaction
|
// An integer that tracks the index of last settled transaction
|
||||||
lastSettled := -1
|
lastSettled int
|
||||||
|
|
||||||
// For a task that runs only after all of its preceding tasks have finished and passed validation,
|
// 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.
|
// 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).
|
// This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid).
|
||||||
skipCheck := make(map[int]bool)
|
skipCheck map[int]bool
|
||||||
|
|
||||||
for i := 0; i < len(tasks); i++ {
|
|
||||||
skipCheck[i] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execution tasks stores the state of each execution task
|
// Execution tasks stores the state of each execution task
|
||||||
execTasks := makeStatusManager(len(tasks))
|
execTasks taskStatusManager
|
||||||
|
|
||||||
// Validate tasks stores the state of each validation task
|
// Validate tasks stores the state of each validation task
|
||||||
validateTasks := makeStatusManager(0)
|
validateTasks taskStatusManager
|
||||||
|
|
||||||
// Stats for debugging purposes
|
// Stats for debugging purposes
|
||||||
var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
|
cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
|
||||||
|
|
||||||
diagExecSuccess := make([]int, len(tasks))
|
diagExecSuccess, diagExecAbort []int
|
||||||
diagExecAbort := make([]int, len(tasks))
|
|
||||||
|
|
||||||
// Initialize MVHashMap
|
// Multi-version hash map
|
||||||
mvh := MakeMVHashMap()
|
mvh *MVHashMap
|
||||||
|
|
||||||
// Stores the inputs and outputs of the last incardanotion of all transactions
|
// Stores the inputs and outputs of the last incardanotion of all transactions
|
||||||
lastTxIO := MakeTxnInputOutput(len(tasks))
|
lastTxIO *TxnInputOutput
|
||||||
|
|
||||||
// Tracks the incarnation number of each transaction
|
// Tracks the incarnation number of each transaction
|
||||||
txIncarnations := make([]int, len(tasks))
|
txIncarnations []int
|
||||||
|
|
||||||
// A map that stores the estimated dependency of a transaction if it is aborted without any known dependency
|
// 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))
|
estimateDeps map[int][]int
|
||||||
|
|
||||||
for i := 0; i < len(tasks); i++ {
|
|
||||||
estimateDeps[i] = make([]int, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A map that records whether a transaction result has been speculatively validated
|
// A map that records whether a transaction result has been speculatively validated
|
||||||
preValidated := make(map[int]bool, len(tasks))
|
preValidated map[int]bool
|
||||||
|
|
||||||
begin := time.Now()
|
// Time records when the parallel execution starts
|
||||||
|
begin time.Time
|
||||||
|
|
||||||
workerWg := sync.WaitGroup{}
|
// Enable profiling
|
||||||
workerWg.Add(numSpeculativeProcs + numGoProcs)
|
profile bool
|
||||||
|
|
||||||
|
// Worker wait group
|
||||||
|
workerWg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor {
|
||||||
|
numTasks := len(tasks)
|
||||||
|
|
||||||
|
pe := &ParallelExecutor{
|
||||||
|
tasks: tasks,
|
||||||
|
stats: make([][]uint64, numTasks),
|
||||||
|
chTasks: make(chan ExecVersionView, numTasks),
|
||||||
|
chSpeculativeTasks: make(chan struct{}, numTasks),
|
||||||
|
chSettle: make(chan int, numTasks),
|
||||||
|
chResults: make(chan struct{}, numTasks),
|
||||||
|
specTaskQueue: NewSafePriorityQueue(numTasks),
|
||||||
|
resultQueue: NewSafePriorityQueue(numTasks),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe *ParallelExecutor) Prepare() {
|
||||||
|
prevSenderTx := make(map[common.Address]int)
|
||||||
|
|
||||||
|
for i, t := range pe.tasks {
|
||||||
|
pe.skipCheck[i] = false
|
||||||
|
pe.estimateDeps[i] = make([]int, 0)
|
||||||
|
|
||||||
|
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
|
// Launch workers that execute transactions
|
||||||
for i := 0; i < numSpeculativeProcs+numGoProcs; i++ {
|
for i := 0; i < numSpeculativeProcs+numGoProcs; i++ {
|
||||||
go func(procNum int) {
|
go func(procNum int) {
|
||||||
defer workerWg.Done()
|
defer pe.workerWg.Done()
|
||||||
|
|
||||||
doWork := func(task ExecVersionView) {
|
doWork := func(task ExecVersionView) {
|
||||||
start := time.Duration(0)
|
start := time.Duration(0)
|
||||||
if profile {
|
if pe.profile {
|
||||||
start = time.Since(begin)
|
start = time.Since(pe.begin)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := task.Execute()
|
res := task.Execute()
|
||||||
|
|
||||||
if res.err == nil {
|
if res.err == nil {
|
||||||
mvh.FlushMVWriteSet(res.txAllOut)
|
pe.mvh.FlushMVWriteSet(res.txAllOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultQueue.Push(res.ver.TxnIndex, res)
|
pe.resultQueue.Push(res.ver.TxnIndex, res)
|
||||||
chResults <- struct{}{}
|
pe.chResults <- struct{}{}
|
||||||
|
|
||||||
if profile {
|
if pe.profile {
|
||||||
end := time.Since(begin)
|
end := time.Since(pe.begin)
|
||||||
|
|
||||||
stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)}
|
stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)}
|
||||||
|
|
||||||
statsMutex.Lock()
|
pe.statsMutex.Lock()
|
||||||
stats = append(stats, stat)
|
pe.stats = append(pe.stats, stat)
|
||||||
statsMutex.Unlock()
|
pe.statsMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if procNum < numSpeculativeProcs {
|
if procNum < numSpeculativeProcs {
|
||||||
for range chSpeculativeTasks {
|
for range pe.chSpeculativeTasks {
|
||||||
doWork(specTaskQueue.Pop().(ExecVersionView))
|
doWork(pe.specTaskQueue.Pop().(ExecVersionView))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for task := range chTasks {
|
for task := range pe.chTasks {
|
||||||
doWork(task)
|
doWork(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch a worker that settles valid transactions
|
pe.settleWg.Add(len(pe.tasks))
|
||||||
settleWg.Add(len(tasks))
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for t := range chSettle {
|
for t := range pe.chSettle {
|
||||||
tasks[t].Settle()
|
pe.tasks[t].Settle()
|
||||||
settleWg.Done()
|
pe.settleWg.Done()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// bootstrap first execution
|
// bootstrap first execution
|
||||||
tx := execTasks.takeNextPending()
|
tx := pe.execTasks.takeNextPending()
|
||||||
if tx != -1 {
|
if tx != -1 {
|
||||||
cntExec++
|
pe.cntExec++
|
||||||
|
|
||||||
chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()}
|
pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.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)
|
// nolint: gocognit
|
||||||
|
func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult, err error) {
|
||||||
for i, t := range tasks {
|
tx := res.ver.TxnIndex
|
||||||
if tx, ok := prevSenderTx[t.Sender()]; ok {
|
|
||||||
execTasks.addDependencies(tx, i)
|
if _, ok := res.err.(ErrExecAbortError); res.err != nil && !ok {
|
||||||
execTasks.clearPending(i)
|
err = res.err
|
||||||
}
|
return
|
||||||
|
}
|
||||||
prevSenderTx[t.Sender()] = i
|
|
||||||
}
|
// nolint: nestif
|
||||||
|
if execErr, ok := res.err.(ErrExecAbortError); ok {
|
||||||
var res ExecResult
|
addedDependencies := false
|
||||||
|
|
||||||
var err error
|
if execErr.Dependency >= 0 {
|
||||||
|
l := len(pe.estimateDeps[tx])
|
||||||
// Start main validation loop
|
for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency {
|
||||||
// nolint:nestif
|
pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1])
|
||||||
for range chResults {
|
pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1]
|
||||||
res = resultQueue.Pop().(ExecResult)
|
l--
|
||||||
tx := res.ver.TxnIndex
|
}
|
||||||
|
|
||||||
if res.err == nil {
|
addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx)
|
||||||
lastTxIO.recordRead(tx, res.txIn)
|
} else {
|
||||||
|
estimate := 0
|
||||||
if res.ver.Incarnation == 0 {
|
|
||||||
lastTxIO.recordWrite(tx, res.txOut)
|
if len(pe.estimateDeps[tx]) > 0 {
|
||||||
lastTxIO.recordAllWrite(tx, res.txAllOut)
|
estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1]
|
||||||
} else {
|
}
|
||||||
if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(tx)) {
|
addedDependencies = pe.execTasks.addDependencies(estimate, tx)
|
||||||
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
newEstimate := estimate + (estimate+tx)/2
|
||||||
}
|
if newEstimate >= tx {
|
||||||
|
newEstimate = tx - 1
|
||||||
prevWrite := lastTxIO.AllWriteSet(tx)
|
}
|
||||||
|
pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate)
|
||||||
// Remove entries that were previously written but are no longer written
|
}
|
||||||
|
|
||||||
cmpMap := make(map[Key]bool)
|
pe.execTasks.clearInProgress(tx)
|
||||||
|
|
||||||
for _, w := range res.txAllOut {
|
if !addedDependencies {
|
||||||
cmpMap[w.Path] = true
|
pe.execTasks.pushPending(tx)
|
||||||
}
|
}
|
||||||
|
pe.txIncarnations[tx]++
|
||||||
for _, v := range prevWrite {
|
pe.diagExecAbort[tx]++
|
||||||
if _, ok := cmpMap[v.Path]; !ok {
|
pe.cntAbort++
|
||||||
mvh.Delete(v.Path, tx)
|
} else {
|
||||||
}
|
pe.lastTxIO.recordRead(tx, res.txIn)
|
||||||
}
|
|
||||||
|
if res.ver.Incarnation == 0 {
|
||||||
lastTxIO.recordWrite(tx, res.txOut)
|
pe.lastTxIO.recordWrite(tx, res.txOut)
|
||||||
lastTxIO.recordAllWrite(tx, res.txAllOut)
|
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
|
||||||
}
|
} else {
|
||||||
|
if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) {
|
||||||
validateTasks.pushPending(tx)
|
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
|
||||||
execTasks.markComplete(tx)
|
}
|
||||||
diagExecSuccess[tx]++
|
|
||||||
cntSuccess++
|
prevWrite := pe.lastTxIO.AllWriteSet(tx)
|
||||||
|
|
||||||
execTasks.removeDependency(tx)
|
// Remove entries that were previously written but are no longer written
|
||||||
} else if execErr, ok := res.err.(ErrExecAbortError); ok {
|
|
||||||
|
cmpMap := make(map[Key]bool)
|
||||||
addedDependencies := false
|
|
||||||
|
for _, w := range res.txAllOut {
|
||||||
if execErr.Dependency >= 0 {
|
cmpMap[w.Path] = true
|
||||||
l := len(estimateDeps[tx])
|
}
|
||||||
for l > 0 && estimateDeps[tx][l-1] > execErr.Dependency {
|
|
||||||
execTasks.removeDependency(estimateDeps[tx][l-1])
|
for _, v := range prevWrite {
|
||||||
estimateDeps[tx] = estimateDeps[tx][:l-1]
|
if _, ok := cmpMap[v.Path]; !ok {
|
||||||
l--
|
pe.mvh.Delete(v.Path, tx)
|
||||||
}
|
}
|
||||||
if txIncarnations[tx] < maxIncarnation {
|
}
|
||||||
addedDependencies = execTasks.addDependencies(execErr.Dependency, tx)
|
|
||||||
} else {
|
pe.lastTxIO.recordWrite(tx, res.txOut)
|
||||||
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
estimate := 0
|
pe.validateTasks.pushPending(tx)
|
||||||
|
pe.execTasks.markComplete(tx)
|
||||||
if len(estimateDeps[tx]) > 0 {
|
pe.diagExecSuccess[tx]++
|
||||||
estimate = estimateDeps[tx][len(estimateDeps[tx])-1]
|
pe.cntSuccess++
|
||||||
}
|
|
||||||
addedDependencies = execTasks.addDependencies(estimate, tx)
|
pe.execTasks.removeDependency(tx)
|
||||||
newEstimate := estimate + (estimate+tx)/2
|
}
|
||||||
if newEstimate >= tx {
|
|
||||||
newEstimate = tx - 1
|
// do validations ...
|
||||||
}
|
maxComplete := pe.execTasks.maxAllComplete()
|
||||||
estimateDeps[tx] = append(estimateDeps[tx], newEstimate)
|
|
||||||
}
|
toValidate := make([]int, 0, 2)
|
||||||
|
|
||||||
execTasks.clearInProgress(tx)
|
for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 {
|
||||||
if !addedDependencies {
|
toValidate = append(toValidate, pe.validateTasks.takeNextPending())
|
||||||
execTasks.pushPending(tx)
|
}
|
||||||
}
|
|
||||||
txIncarnations[tx]++
|
for i := 0; i < len(toValidate); i++ {
|
||||||
diagExecAbort[tx]++
|
pe.cntTotalValidations++
|
||||||
cntAbort++
|
|
||||||
} else {
|
tx := toValidate[i]
|
||||||
err = res.err
|
|
||||||
break
|
if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) {
|
||||||
}
|
pe.validateTasks.markComplete(tx)
|
||||||
|
} else {
|
||||||
// do validations ...
|
pe.cntValidationFail++
|
||||||
maxComplete := execTasks.maxAllComplete()
|
pe.diagExecAbort[tx]++
|
||||||
|
for _, v := range pe.lastTxIO.AllWriteSet(tx) {
|
||||||
var toValidate []int
|
pe.mvh.MarkEstimate(v.Path, tx)
|
||||||
|
}
|
||||||
for validateTasks.minPending() <= maxComplete && validateTasks.minPending() >= 0 {
|
// 'create validation tasks for all transactions > tx ...'
|
||||||
toValidate = append(toValidate, validateTasks.takeNextPending())
|
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
|
||||||
}
|
pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
|
||||||
|
|
||||||
for i := 0; i < len(toValidate); i++ {
|
pe.execTasks.clearComplete(tx)
|
||||||
cntTotalValidations++
|
pe.execTasks.pushPending(tx)
|
||||||
|
|
||||||
tx := toValidate[i]
|
pe.preValidated[tx] = false
|
||||||
|
pe.txIncarnations[tx]++
|
||||||
if skipCheck[tx] || ValidateVersion(tx, lastTxIO, mvh) {
|
}
|
||||||
validateTasks.markComplete(tx)
|
}
|
||||||
} else {
|
|
||||||
cntValidationFail++
|
// Settle transactions that have been validated to be correct and that won't be re-executed again
|
||||||
diagExecAbort[tx]++
|
maxValidated := pe.validateTasks.maxAllComplete()
|
||||||
for _, v := range lastTxIO.AllWriteSet(tx) {
|
|
||||||
mvh.MarkEstimate(v.Path, tx)
|
for pe.lastSettled < maxValidated {
|
||||||
}
|
pe.lastSettled++
|
||||||
// 'create validation tasks for all transactions > tx ...'
|
if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) {
|
||||||
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
pe.lastSettled--
|
||||||
validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
|
break
|
||||||
|
}
|
||||||
addedDependencies := false
|
pe.chSettle <- pe.lastSettled
|
||||||
if txIncarnations[tx] >= maxIncarnation {
|
}
|
||||||
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
|
||||||
}
|
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))
|
||||||
execTasks.clearComplete(tx)
|
|
||||||
if !addedDependencies {
|
close(pe.chTasks)
|
||||||
execTasks.pushPending(tx)
|
close(pe.chSpeculativeTasks)
|
||||||
}
|
pe.workerWg.Wait()
|
||||||
|
close(pe.chResults)
|
||||||
preValidated[tx] = false
|
pe.settleWg.Wait()
|
||||||
txIncarnations[tx]++
|
close(pe.chSettle)
|
||||||
}
|
|
||||||
}
|
var dag DAG
|
||||||
|
|
||||||
preValidateCount := 0
|
if pe.profile {
|
||||||
invalidated := []int{}
|
dag = BuildDAG(*pe.lastTxIO)
|
||||||
|
}
|
||||||
i := sort.SearchInts(validateTasks.pending, maxComplete+1)
|
|
||||||
|
return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &dag}, err
|
||||||
for i < len(validateTasks.pending) && preValidateCount < preValidateLimit {
|
}
|
||||||
tx := validateTasks.pending[i]
|
|
||||||
|
// Send the next immediate pending transaction to be executed
|
||||||
if !preValidated[tx] {
|
if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 {
|
||||||
cntTotalValidations++
|
nextTx := pe.execTasks.takeNextPending()
|
||||||
|
if nextTx != -1 {
|
||||||
if !ValidateVersion(tx, lastTxIO, mvh) {
|
pe.cntExec++
|
||||||
cntValidationFail++
|
|
||||||
diagExecAbort[tx]++
|
pe.skipCheck[nextTx] = true
|
||||||
|
|
||||||
invalidated = append(invalidated, tx)
|
pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
|
||||||
|
}
|
||||||
if execTasks.checkComplete(tx) {
|
}
|
||||||
execTasks.clearComplete(tx)
|
|
||||||
}
|
// Send speculative tasks
|
||||||
|
for pe.execTasks.minPending() != -1 || len(pe.execTasks.inProgress) == 0 {
|
||||||
if !execTasks.checkInProgress(tx) {
|
nextTx := pe.execTasks.takeNextPending()
|
||||||
for _, v := range lastTxIO.AllWriteSet(tx) {
|
|
||||||
mvh.MarkEstimate(v.Path, tx)
|
if nextTx == -1 {
|
||||||
}
|
nextTx = pe.execTasks.takeNextPending()
|
||||||
|
}
|
||||||
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
|
|
||||||
|
if nextTx != -1 {
|
||||||
addedDependencies := false
|
pe.cntExec++
|
||||||
if txIncarnations[tx] >= maxIncarnation {
|
|
||||||
addedDependencies = execTasks.addDependencies(tx-1, tx)
|
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)
|
||||||
if !addedDependencies {
|
pe.chSpeculativeTasks <- struct{}{}
|
||||||
execTasks.pushPending(tx)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return
|
||||||
txIncarnations[tx]++
|
}
|
||||||
|
|
||||||
preValidated[tx] = false
|
type PropertyCheck func(*ParallelExecutor) error
|
||||||
} else {
|
|
||||||
preValidated[tx] = true
|
func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck) (result ParallelExecutionResult, err error) {
|
||||||
}
|
if len(tasks) == 0 {
|
||||||
preValidateCount++
|
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i++
|
pe := NewParallelExecutor(tasks, profile)
|
||||||
}
|
pe.Prepare()
|
||||||
|
|
||||||
for _, tx := range invalidated {
|
for range pe.chResults {
|
||||||
validateTasks.clearPending(tx)
|
res := pe.resultQueue.Pop().(ExecResult)
|
||||||
}
|
|
||||||
|
result, err = pe.Step(res)
|
||||||
// Settle transactions that have been validated to be correct and that won't be re-executed again
|
|
||||||
maxValidated := validateTasks.maxAllComplete()
|
if err != nil {
|
||||||
|
return result, err
|
||||||
for lastSettled < maxValidated {
|
}
|
||||||
lastSettled++
|
|
||||||
if execTasks.checkInProgress(lastSettled) || execTasks.checkPending(lastSettled) || execTasks.blockCount[lastSettled] >= 0 {
|
if check != nil {
|
||||||
lastSettled--
|
err = check(pe)
|
||||||
break
|
}
|
||||||
}
|
|
||||||
chSettle <- lastSettled
|
if result.TxIO != nil || err != nil {
|
||||||
}
|
return result, err
|
||||||
|
}
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the next immediate pending transaction to be executed
|
func ExecuteParallel(tasks []ExecTask, profile bool) (result ParallelExecutionResult, err error) {
|
||||||
if execTasks.minPending() != -1 && execTasks.minPending() == maxValidated+1 {
|
return executeParallelWithCheck(tasks, profile, func(pe *ParallelExecutor) error {
|
||||||
nextTx := execTasks.takeNextPending()
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ type testExecTask struct {
|
||||||
nonce int
|
nonce int
|
||||||
}
|
}
|
||||||
|
|
||||||
type PathGenerator func(addr common.Address, j int, total int) Key
|
type PathGenerator func(addr common.Address, i int, j int, total int) Key
|
||||||
|
|
||||||
type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration)
|
type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration)
|
||||||
|
|
||||||
|
|
@ -169,11 +169,11 @@ func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var randomPathGenerator = func(sender common.Address, j int, total int) Key {
|
var randomPathGenerator = func(sender common.Address, i int, j int, total int) Key {
|
||||||
return NewStateKey(sender, common.BigToHash((big.NewInt(int64(total)))))
|
return NewStateKey(common.BigToAddress((big.NewInt(int64(i % 10)))), common.BigToHash((big.NewInt(int64(total)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
var dexPathGenerator = func(sender common.Address, j int, total int) Key {
|
var dexPathGenerator = func(sender common.Address, i int, j int, total int) Key {
|
||||||
if j == total-1 || j == 2 {
|
if j == total-1 || j == 2 {
|
||||||
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1)
|
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -226,10 +226,10 @@ func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonI
|
||||||
// Generate time and key path for each op except first two that are always read and write nonce
|
// Generate time and key path for each op except first two that are always read and write nonce
|
||||||
for j := 2; j < len(ops); j++ {
|
for j := 2; j < len(ops); j++ {
|
||||||
if ops[j].opType == readType {
|
if ops[j].opType == readType {
|
||||||
ops[j].key = pathGenerator(s, j, len(ops))
|
ops[j].key = pathGenerator(s, i, j, len(ops))
|
||||||
ops[j].duration = readTime(i, j)
|
ops[j].duration = readTime(i, j)
|
||||||
} else if ops[j].opType == writeType {
|
} else if ops[j].opType == writeType {
|
||||||
ops[j].key = pathGenerator(s, j, len(ops))
|
ops[j].key = pathGenerator(s, i, j, len(ops))
|
||||||
ops[j].duration = writeTime(i, j)
|
ops[j].duration = writeTime(i, j)
|
||||||
} else {
|
} else {
|
||||||
ops[j].duration = nonIOTime(i, j)
|
ops[j].duration = nonIOTime(i, j)
|
||||||
|
|
@ -290,13 +290,64 @@ func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites []
|
||||||
fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100)
|
fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput) bool) time.Duration {
|
func composeValidations(checks []PropertyCheck) PropertyCheck {
|
||||||
|
return func(pe *ParallelExecutor) error {
|
||||||
|
for _, check := range checks {
|
||||||
|
err := check(pe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNoStatusOverlap(pe *ParallelExecutor) error {
|
||||||
|
seen := make(map[int]string)
|
||||||
|
|
||||||
|
for _, tx := range pe.execTasks.complete {
|
||||||
|
seen[tx] = "complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tx := range pe.execTasks.inProgress {
|
||||||
|
if v, ok := seen[tx]; ok {
|
||||||
|
return fmt.Errorf("tx %v is in both %v and inProgress", v, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[tx] = "inProgress"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tx := range pe.execTasks.pending {
|
||||||
|
if v, ok := seen[tx]; ok {
|
||||||
|
return fmt.Errorf("tx %v is in both %v complete and pending", v, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[tx] = "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNoDroppedTx(pe *ParallelExecutor) error {
|
||||||
|
for i := 0; i < len(pe.tasks); i++ {
|
||||||
|
if !pe.execTasks.checkComplete(i) && !pe.execTasks.checkInProgress(i) && !pe.execTasks.checkPending(i) {
|
||||||
|
if !pe.execTasks.isBlocked(i) {
|
||||||
|
return fmt.Errorf("tx %v is not in any status and is not blocked by any other tx", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck) time.Duration {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
results, _ := ExecuteParallel(tasks, false)
|
_, err := executeParallelWithCheck(tasks, false, validation)
|
||||||
|
|
||||||
txio := results.TxIO
|
assert.NoError(t, err, "error occur during parallel execution")
|
||||||
|
|
||||||
// Need to apply the final write set to storage
|
// Need to apply the final write set to storage
|
||||||
|
|
||||||
|
|
@ -317,10 +368,6 @@ func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput)
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if validation != nil {
|
|
||||||
assert.True(t, validation(*txio))
|
|
||||||
}
|
|
||||||
|
|
||||||
return duration
|
return duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,6 +380,8 @@ func TestLessConflicts(t *testing.T) {
|
||||||
numWrites := []int{20, 100, 200}
|
numWrites := []int{20, 100, 200}
|
||||||
numNonIO := []int{100, 500}
|
numNonIO := []int{100, 500}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
sender := func(i int) common.Address {
|
sender := func(i int) common.Address {
|
||||||
randomness := rand.Intn(10) + 10
|
randomness := rand.Intn(10) + 10
|
||||||
|
|
@ -340,7 +389,28 @@ func TestLessConflicts(t *testing.T) {
|
||||||
}
|
}
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, nil), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZeroTx(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rand.Seed(0)
|
||||||
|
|
||||||
|
totalTxs := []int{0}
|
||||||
|
numReads := []int{20}
|
||||||
|
numWrites := []int{20}
|
||||||
|
numNonIO := []int{100}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(1))) }
|
||||||
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
@ -355,11 +425,13 @@ func TestAlternatingTx(t *testing.T) {
|
||||||
numWrites := []int{20}
|
numWrites := []int{20}
|
||||||
numNonIO := []int{100}
|
numNonIO := []int{100}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) }
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) }
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, nil), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
@ -374,6 +446,8 @@ func TestMoreConflicts(t *testing.T) {
|
||||||
numWrites := []int{20, 100, 200}
|
numWrites := []int{20, 100, 200}
|
||||||
numNonIO := []int{100, 500}
|
numNonIO := []int{100, 500}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
sender := func(i int) common.Address {
|
sender := func(i int) common.Address {
|
||||||
randomness := rand.Intn(10) + 10
|
randomness := rand.Intn(10) + 10
|
||||||
|
|
@ -381,7 +455,7 @@ func TestMoreConflicts(t *testing.T) {
|
||||||
}
|
}
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, nil), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
@ -396,12 +470,14 @@ func TestRandomTx(t *testing.T) {
|
||||||
numWrites := []int{20, 100, 200}
|
numWrites := []int{20, 100, 200}
|
||||||
numNonIO := []int{100, 500}
|
numNonIO := []int{100, 500}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
// Randomly assign this tx to one of 10 senders
|
// Randomly assign this tx to one of 10 senders
|
||||||
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) }
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) }
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, nil), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
@ -416,6 +492,8 @@ func TestTxWithLongTailRead(t *testing.T) {
|
||||||
numWrites := []int{20, 100, 200}
|
numWrites := []int{20, 100, 200}
|
||||||
numNonIO := []int{100, 500}
|
numNonIO := []int{100, 500}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
sender := func(i int) common.Address {
|
sender := func(i int) common.Address {
|
||||||
randomness := rand.Intn(10) + 10
|
randomness := rand.Intn(10) + 10
|
||||||
|
|
@ -426,7 +504,7 @@ func TestTxWithLongTailRead(t *testing.T) {
|
||||||
|
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, nil), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
@ -441,29 +519,27 @@ func TestDexScenario(t *testing.T) {
|
||||||
numWrites := []int{20, 100, 200}
|
numWrites := []int{20, 100, 200}
|
||||||
numNonIO := []int{100, 500}
|
numNonIO := []int{100, 500}
|
||||||
|
|
||||||
validation := func(txio TxnInputOutput) bool {
|
postValidation := func(pe *ParallelExecutor) error {
|
||||||
for i, inputs := range txio.inputs {
|
if pe.lastSettled == len(pe.tasks) {
|
||||||
foundDep := false
|
for i, inputs := range pe.lastTxIO.inputs {
|
||||||
|
for _, input := range inputs {
|
||||||
for _, input := range inputs {
|
if input.V.TxnIndex != i-1 {
|
||||||
if input.V.TxnIndex == i-1 {
|
return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex)
|
||||||
foundDep = true
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foundDep {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx})
|
||||||
|
|
||||||
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
||||||
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) }
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) }
|
||||||
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime)
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime)
|
||||||
|
|
||||||
return runParallel(t, tasks, validation), serialDuration
|
return runParallel(t, tasks, checks), serialDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ func makeStatusManager(numTasks int) (t taskStatusManager) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.dependency = make(map[int]map[int]bool, numTasks)
|
t.dependency = make(map[int]map[int]bool, numTasks)
|
||||||
t.blockCount = make(map[int]int, numTasks)
|
t.blocker = make(map[int]map[int]bool, numTasks)
|
||||||
|
|
||||||
for i := 0; i < numTasks; i++ {
|
for i := 0; i < numTasks; i++ {
|
||||||
t.blockCount[i] = -1
|
t.blocker[i] = make(map[int]bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -26,7 +26,7 @@ type taskStatusManager struct {
|
||||||
inProgress []int
|
inProgress []int
|
||||||
complete []int
|
complete []int
|
||||||
dependency map[int]map[int]bool
|
dependency map[int]map[int]bool
|
||||||
blockCount map[int]int
|
blocker map[int]map[int]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertInList(l []int, v int) []int {
|
func insertInList(l []int, v int) []int {
|
||||||
|
|
@ -56,35 +56,6 @@ func (m *taskStatusManager) takeNextPending() int {
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *taskStatusManager) peekPendingGE(n int) int {
|
|
||||||
x := sort.SearchInts(m.pending, n)
|
|
||||||
if x >= len(m.pending) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.pending[x]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take a pending task whose transaction index is greater than or equal to the given tx index
|
|
||||||
func (m *taskStatusManager) takePendingGE(n int) int {
|
|
||||||
x := sort.SearchInts(m.pending, n)
|
|
||||||
if x >= len(m.pending) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
v := m.pending[x]
|
|
||||||
|
|
||||||
if x < len(m.pending)-1 {
|
|
||||||
m.pending = append(m.pending[:x], m.pending[x+1:]...)
|
|
||||||
} else {
|
|
||||||
m.pending = m.pending[:x]
|
|
||||||
}
|
|
||||||
|
|
||||||
m.inProgress = insertInList(m.inProgress, v)
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasNoGap(l []int) bool {
|
func hasNoGap(l []int) bool {
|
||||||
return l[0]+len(l) == l[len(l)-1]+1
|
return l[0]+len(l) == l[len(l)-1]+1
|
||||||
}
|
}
|
||||||
|
|
@ -106,11 +77,7 @@ func (m taskStatusManager) maxAllComplete() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *taskStatusManager) pushPending(tx int) {
|
func (m *taskStatusManager) pushPending(tx int) {
|
||||||
if !m.checkComplete(tx) && !m.checkInProgress(tx) {
|
m.pending = insertInList(m.pending, tx)
|
||||||
m.pending = insertInList(m.pending, tx)
|
|
||||||
} else {
|
|
||||||
panic(fmt.Errorf("should not happen - clear complete or inProgress before pushing pending"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFromList(l []int, v int, expect bool) []int {
|
func removeFromList(l []int, v int, expect bool) []int {
|
||||||
|
|
@ -155,15 +122,12 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
curBlocker := m.blockCount[dependent]
|
curblockers := m.blocker[dependent]
|
||||||
|
|
||||||
if curBlocker > blocker {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.checkComplete(blocker) {
|
if m.checkComplete(blocker) {
|
||||||
// Blocking blocker has already completed
|
// Blocker has already completed
|
||||||
m.blockCount[dependent] = -1
|
delete(curblockers, blocker)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,16 +136,21 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.dependency[blocker][dependent] = true
|
m.dependency[blocker][dependent] = true
|
||||||
m.blockCount[dependent] = blocker
|
curblockers[blocker] = true
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *taskStatusManager) isBlocked(tx int) bool {
|
||||||
|
return len(m.blocker[tx]) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (m *taskStatusManager) removeDependency(tx int) {
|
func (m *taskStatusManager) removeDependency(tx int) {
|
||||||
if deps, ok := m.dependency[tx]; ok && len(deps) > 0 {
|
if deps, ok := m.dependency[tx]; ok && len(deps) > 0 {
|
||||||
for k := range deps {
|
for k := range deps {
|
||||||
if m.blockCount[k] == tx {
|
delete(m.blocker[k], tx)
|
||||||
m.blockCount[k] = -1
|
|
||||||
|
if len(m.blocker[k]) == 0 {
|
||||||
if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) {
|
if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) {
|
||||||
m.pushPending(k)
|
m.pushPending(k)
|
||||||
}
|
}
|
||||||
|
|
@ -243,9 +212,7 @@ func (m *taskStatusManager) pushPendingSet(set []int) {
|
||||||
m.clearComplete(v)
|
m.clearComplete(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.checkInProgress(v) {
|
m.pushPending(v)
|
||||||
m.pushPending(v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,17 @@ func (task *ExecutionTask) Sender() common.Address {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (task *ExecutionTask) Settle() {
|
func (task *ExecutionTask) Settle() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// In some rare cases, ApplyMVWriteSet will panic due to an index out of range error when calculating the
|
||||||
|
// address hash in sha3 module. Recover from panic and continue the execution.
|
||||||
|
// After recovery, block receipts or merckle root will be incorrect, but this is fine, because the block
|
||||||
|
// will be rejected and re-synced.
|
||||||
|
log.Info("Recovered from error", "Error:", r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
task.finalStateDB.Prepare(task.tx.Hash(), task.index)
|
task.finalStateDB.Prepare(task.tx.Hash(), task.index)
|
||||||
|
|
||||||
coinbase, _ := task.blockChain.Engine().Author(task.header)
|
coinbase, _ := task.blockChain.Engine().Author(task.header)
|
||||||
|
|
|
||||||
|
|
@ -275,11 +275,8 @@ func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *S
|
||||||
}
|
}
|
||||||
case blockstm.MVReadResultDependency:
|
case blockstm.MVReadResultDependency:
|
||||||
{
|
{
|
||||||
if res.DepIdx() > s.dep {
|
s.dep = res.DepIdx()
|
||||||
s.dep = res.DepIdx()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return immediate to executor when we found a dependency
|
|
||||||
panic("Found dependency")
|
panic("Found dependency")
|
||||||
}
|
}
|
||||||
case blockstm.MVReadResultNone:
|
case blockstm.MVReadResultNone:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue