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 (
"container/heap"
"fmt"
"sort"
"sync"
"time"
@ -127,413 +126,402 @@ type ParallelExecutionResult struct {
}
const numGoProcs = 2
const numSpeculativeProcs = 16
const numSpeculativeProcs = 8
// 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
}
type ParallelExecutor struct {
tasks []ExecTask
// Stores the execution statistics for each task
stats := make([][]uint64, 0, len(tasks))
statsMutex := sync.Mutex{}
stats [][]uint64
statsMutex sync.Mutex
// Channel for tasks that should be prioritized
chTasks := make(chan ExecVersionView, len(tasks))
chTasks chan ExecVersionView
// Channel for speculative tasks
chSpeculativeTasks := make(chan struct{}, len(tasks))
// A priority queue that stores speculative tasks
specTaskQueue := NewSafePriorityQueue(len(tasks))
chSpeculativeTasks chan struct{}
// 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
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
resultQueue := NewSafePriorityQueue(len(tasks))
resultQueue *SafePriorityQueue
// 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
lastSettled := -1
lastSettled int
// For a task that runs only after all of its preceding tasks have finished and passed validation,
// its result will be absolutely valid and therefore its validation could be skipped.
// This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid).
skipCheck := make(map[int]bool)
for i := 0; i < len(tasks); i++ {
skipCheck[i] = false
}
skipCheck map[int]bool
// Execution tasks stores the state of each execution task
execTasks := makeStatusManager(len(tasks))
execTasks taskStatusManager
// Validate tasks stores the state of each validation task
validateTasks := makeStatusManager(0)
validateTasks taskStatusManager
// Stats for debugging purposes
var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
diagExecSuccess := make([]int, len(tasks))
diagExecAbort := make([]int, len(tasks))
diagExecSuccess, diagExecAbort []int
// Initialize MVHashMap
mvh := MakeMVHashMap()
// Multi-version hash map
mvh *MVHashMap
// 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
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
estimateDeps := make(map[int][]int, len(tasks))
for i := 0; i < len(tasks); i++ {
estimateDeps[i] = make([]int, 0)
}
estimateDeps map[int][]int
// 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{}
workerWg.Add(numSpeculativeProcs + numGoProcs)
// Enable profiling
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
for i := 0; i < numSpeculativeProcs+numGoProcs; i++ {
go func(procNum int) {
defer workerWg.Done()
defer pe.workerWg.Done()
doWork := func(task ExecVersionView) {
start := time.Duration(0)
if profile {
start = time.Since(begin)
if pe.profile {
start = time.Since(pe.begin)
}
res := task.Execute()
if res.err == nil {
mvh.FlushMVWriteSet(res.txAllOut)
pe.mvh.FlushMVWriteSet(res.txAllOut)
}
resultQueue.Push(res.ver.TxnIndex, res)
chResults <- struct{}{}
pe.resultQueue.Push(res.ver.TxnIndex, res)
pe.chResults <- struct{}{}
if profile {
end := time.Since(begin)
if pe.profile {
end := time.Since(pe.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()
pe.statsMutex.Lock()
pe.stats = append(pe.stats, stat)
pe.statsMutex.Unlock()
}
}
if procNum < numSpeculativeProcs {
for range chSpeculativeTasks {
doWork(specTaskQueue.Pop().(ExecVersionView))
for range pe.chSpeculativeTasks {
doWork(pe.specTaskQueue.Pop().(ExecVersionView))
}
} else {
for task := range chTasks {
for task := range pe.chTasks {
doWork(task)
}
}
}(i)
}
// Launch a worker that settles valid transactions
settleWg.Add(len(tasks))
pe.settleWg.Add(len(pe.tasks))
go func() {
for t := range chSettle {
tasks[t].Settle()
settleWg.Done()
for t := range pe.chSettle {
pe.tasks[t].Settle()
pe.settleWg.Done()
}
}()
// bootstrap first execution
tx := execTasks.takeNextPending()
tx := pe.execTasks.takeNextPending()
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)
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
}
// nolint: gocognit
func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult, err error) {
tx := res.ver.TxnIndex
if _, ok := res.err.(ErrExecAbortError); res.err != nil && !ok {
err = res.err
return
}
// nolint: nestif
if execErr, ok := res.err.(ErrExecAbortError); ok {
addedDependencies := false
if execErr.Dependency >= 0 {
l := len(pe.estimateDeps[tx])
for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency {
pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1])
pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1]
l--
}
addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx)
} else {
estimate := 0
if len(pe.estimateDeps[tx]) > 0 {
estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1]
}
addedDependencies = pe.execTasks.addDependencies(estimate, tx)
newEstimate := estimate + (estimate+tx)/2
if newEstimate >= tx {
newEstimate = tx - 1
}
pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate)
}
pe.execTasks.clearInProgress(tx)
if !addedDependencies {
pe.execTasks.pushPending(tx)
}
pe.txIncarnations[tx]++
pe.diagExecAbort[tx]++
pe.cntAbort++
} else {
pe.lastTxIO.recordRead(tx, res.txIn)
if res.ver.Incarnation == 0 {
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
} else {
if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) {
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
}
prevWrite := pe.lastTxIO.AllWriteSet(tx)
// Remove entries that were previously written but are no longer written
cmpMap := make(map[Key]bool)
for _, w := range res.txAllOut {
cmpMap[w.Path] = true
}
for _, v := range prevWrite {
if _, ok := cmpMap[v.Path]; !ok {
pe.mvh.Delete(v.Path, tx)
}
}
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
}
pe.validateTasks.pushPending(tx)
pe.execTasks.markComplete(tx)
pe.diagExecSuccess[tx]++
pe.cntSuccess++
pe.execTasks.removeDependency(tx)
}
// do validations ...
maxComplete := pe.execTasks.maxAllComplete()
toValidate := make([]int, 0, 2)
for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 {
toValidate = append(toValidate, pe.validateTasks.takeNextPending())
}
for i := 0; i < len(toValidate); i++ {
pe.cntTotalValidations++
tx := toValidate[i]
if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) {
pe.validateTasks.markComplete(tx)
} else {
pe.cntValidationFail++
pe.diagExecAbort[tx]++
for _, v := range pe.lastTxIO.AllWriteSet(tx) {
pe.mvh.MarkEstimate(v.Path, tx)
}
// 'create validation tasks for all transactions > tx ...'
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
pe.execTasks.clearComplete(tx)
pe.execTasks.pushPending(tx)
pe.preValidated[tx] = false
pe.txIncarnations[tx]++
}
}
// Settle transactions that have been validated to be correct and that won't be re-executed again
maxValidated := pe.validateTasks.maxAllComplete()
for pe.lastSettled < maxValidated {
pe.lastSettled++
if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) {
pe.lastSettled--
break
}
pe.chSettle <- pe.lastSettled
}
if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) {
log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100))
close(pe.chTasks)
close(pe.chSpeculativeTasks)
pe.workerWg.Wait()
close(pe.chResults)
pe.settleWg.Wait()
close(pe.chSettle)
var dag DAG
if pe.profile {
dag = BuildDAG(*pe.lastTxIO)
}
return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &dag}, err
}
// Send the next immediate pending transaction to be executed
if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 {
nextTx := pe.execTasks.takeNextPending()
if nextTx != -1 {
pe.cntExec++
pe.skipCheck[nextTx] = true
pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
}
}
// Send speculative tasks
for pe.execTasks.minPending() != -1 || len(pe.execTasks.inProgress) == 0 {
nextTx := pe.execTasks.takeNextPending()
if nextTx == -1 {
nextTx = pe.execTasks.takeNextPending()
}
if nextTx != -1 {
pe.cntExec++
task := ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
pe.specTaskQueue.Push(nextTx, task)
pe.chSpeculativeTasks <- struct{}{}
}
}
return
}
type PropertyCheck func(*ParallelExecutor) error
func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck) (result ParallelExecutionResult, err error) {
if len(tasks) == 0 {
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil
}
pe := NewParallelExecutor(tasks, profile)
pe.Prepare()
for range pe.chResults {
res := pe.resultQueue.Pop().(ExecResult)
result, err = pe.Step(res)
if err != nil {
return result, err
}
if check != nil {
err = check(pe)
}
if result.TxIO != nil || err != nil {
return result, err
}
}
return
}
func ExecuteParallel(tasks []ExecTask, profile bool) (result ParallelExecutionResult, err error) {
return executeParallelWithCheck(tasks, profile, func(pe *ParallelExecutor) error {
return nil
})
}

View file

@ -36,7 +36,7 @@ type testExecTask struct {
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)
@ -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 {
return NewStateKey(sender, common.BigToHash((big.NewInt(int64(total)))))
var randomPathGenerator = func(sender common.Address, i int, j int, total int) Key {
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 {
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1)
} 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
for j := 2; j < len(ops); j++ {
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)
} 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)
} else {
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)
}
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()
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
@ -317,10 +368,6 @@ func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput)
duration := time.Since(start)
if validation != nil {
assert.True(t, validation(*txio))
}
return duration
}
@ -333,6 +380,8 @@ func TestLessConflicts(t *testing.T) {
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
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 {
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)
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)
@ -355,11 +425,13 @@ func TestAlternatingTx(t *testing.T) {
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(i % 2))) }
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)
@ -374,6 +446,8 @@ func TestMoreConflicts(t *testing.T) {
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
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 {
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)
return runParallel(t, tasks, nil), serialDuration
return runParallel(t, tasks, checks), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
@ -396,12 +470,14 @@ func TestRandomTx(t *testing.T) {
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
// Randomly assign this tx to one of 10 senders
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)
return runParallel(t, tasks, nil), serialDuration
return runParallel(t, tasks, checks), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
@ -416,6 +492,8 @@ func TestTxWithLongTailRead(t *testing.T) {
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
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 {
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)
return runParallel(t, tasks, nil), serialDuration
return runParallel(t, tasks, checks), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
@ -441,29 +519,27 @@ func TestDexScenario(t *testing.T) {
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
validation := func(txio TxnInputOutput) bool {
for i, inputs := range txio.inputs {
foundDep := false
for _, input := range inputs {
if input.V.TxnIndex == i-1 {
foundDep = true
postValidation := func(pe *ParallelExecutor) error {
if pe.lastSettled == len(pe.tasks) {
for i, inputs := range pe.lastTxIO.inputs {
for _, input := range inputs {
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)
}
}
}
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) {
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)
return runParallel(t, tasks, validation), serialDuration
return runParallel(t, tasks, checks), serialDuration
}
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.blockCount = make(map[int]int, numTasks)
t.blocker = make(map[int]map[int]bool, numTasks)
for i := 0; i < numTasks; i++ {
t.blockCount[i] = -1
t.blocker[i] = make(map[int]bool)
}
return
@ -26,7 +26,7 @@ type taskStatusManager struct {
inProgress []int
complete []int
dependency map[int]map[int]bool
blockCount map[int]int
blocker map[int]map[int]bool
}
func insertInList(l []int, v int) []int {
@ -56,35 +56,6 @@ func (m *taskStatusManager) takeNextPending() int {
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 {
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) {
if !m.checkComplete(tx) && !m.checkInProgress(tx) {
m.pending = insertInList(m.pending, tx)
} else {
panic(fmt.Errorf("should not happen - clear complete or inProgress before pushing pending"))
}
m.pending = insertInList(m.pending, tx)
}
func removeFromList(l []int, v int, expect bool) []int {
@ -155,15 +122,12 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool {
return false
}
curBlocker := m.blockCount[dependent]
if curBlocker > blocker {
return true
}
curblockers := m.blocker[dependent]
if m.checkComplete(blocker) {
// Blocking blocker has already completed
m.blockCount[dependent] = -1
// Blocker has already completed
delete(curblockers, blocker)
return false
}
@ -172,16 +136,21 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool {
}
m.dependency[blocker][dependent] = true
m.blockCount[dependent] = blocker
curblockers[blocker] = true
return true
}
func (m *taskStatusManager) isBlocked(tx int) bool {
return len(m.blocker[tx]) > 0
}
func (m *taskStatusManager) removeDependency(tx int) {
if deps, ok := m.dependency[tx]; ok && len(deps) > 0 {
for k := range deps {
if m.blockCount[k] == tx {
m.blockCount[k] = -1
delete(m.blocker[k], tx)
if len(m.blocker[k]) == 0 {
if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) {
m.pushPending(k)
}
@ -243,9 +212,7 @@ func (m *taskStatusManager) pushPendingSet(set []int) {
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() {
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)
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:
{
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")
}
case blockstm.MVReadResultNone: