Refactor blockstm executor

This commit is contained in:
Jerry 2022-09-21 18:21:50 -07:00
parent d107c183b8
commit 471afc8da2
5 changed files with 456 additions and 417 deletions

View file

@ -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
} }

View file

@ -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)

View file

@ -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)
}
} }
} }

View file

@ -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)

View file

@ -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: