├── go.mod ├── .gitignore ├── .github └── workflows │ └── go.yml ├── LICENSE ├── rollback.go ├── go.sum ├── message.go ├── executor.go ├── error.go ├── dependency_test.go ├── pool.go ├── example_test.go ├── dependency.go ├── tcc.go ├── coverage.out ├── README.md ├── benchmark_test.go └── tcc_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/piaodazhu/gotcc 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/panjf2000/ants/v2 v2.7.3 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: gotcc CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.14 23 | - name: Test 24 | run: go test ./... -v 25 | - name: PrintMsg 26 | run: echo "--GOTCC--" 27 | codereport: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Install cloc 32 | run: sudo apt install cloc 33 | - name: Generate report 34 | run: cloc . --include-lang="Go" 35 | codecov: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Upload coverage reports to Codecov 40 | uses: codecov/codecov-action@v3 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zhanghuixian Luo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollback.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import "sync" 4 | 5 | type undoStack struct { 6 | lock sync.Mutex 7 | items []*undoFunc 8 | } 9 | 10 | type undoFunc struct { 11 | name string 12 | 13 | skipError bool 14 | 15 | args map[string]interface{} 16 | f func(map[string]interface{}) error 17 | } 18 | 19 | func newUndoFunc(name string, skipError bool, undo func(args map[string]interface{}) error, args map[string]interface{}) *undoFunc { 20 | return &undoFunc{ 21 | name: name, 22 | skipError: skipError, 23 | args: args, 24 | f: undo, 25 | } 26 | } 27 | 28 | func (u *undoStack) push(uf *undoFunc) { 29 | u.lock.Lock() 30 | u.items = append(u.items, uf) 31 | u.lock.Unlock() 32 | } 33 | 34 | func (u *undoStack) reset() { 35 | u.lock.Lock() 36 | u.items = []*undoFunc{} 37 | u.lock.Unlock() 38 | } 39 | 40 | func (u *undoStack) undoAll(taskErrors *errorLisk, cancelled *cancelList) *errorLisk { 41 | undoErrors := &errorLisk{} 42 | for i := len(u.items) - 1; i >= 0; i-- { 43 | u.items[i].args["TASKERR"] = taskErrors.items 44 | u.items[i].args["UNDOERR"] = undoErrors.items 45 | u.items[i].args["CANCELLED"] = cancelled.items 46 | 47 | err := u.items[i].f(u.items[i].args) 48 | if err != nil { 49 | undoErrors.append(newErrorMessage(u.items[i].name, err)) 50 | if !u.items[i].skipError { 51 | return undoErrors 52 | } 53 | } 54 | } 55 | return undoErrors 56 | } 57 | 58 | // Default undo function 59 | var EmptyUndoFunc = func(args map[string]interface{}) error { 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/panjf2000/ants/v2 v2.7.3 h1:rHQ0hH0DQvuNUqqlWIMJtkMcDuL1uQAfpX2mIhQ5/s0= 7 | github.com/panjf2000/ants/v2 v2.7.3/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 16 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 17 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 18 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | type message struct { 9 | senderId uint32 10 | senderName string 11 | value interface{} 12 | } 13 | 14 | type ErrorMessage struct { 15 | TaskName string 16 | Error error 17 | } 18 | 19 | func newErrorMessage(taskName string, err error) *ErrorMessage { 20 | return &ErrorMessage{ 21 | TaskName: taskName, 22 | Error: err, 23 | } 24 | } 25 | 26 | type errorLisk struct { 27 | lock sync.Mutex 28 | items []*ErrorMessage 29 | } 30 | 31 | func (el *errorLisk) append(em *ErrorMessage) { 32 | el.lock.Lock() 33 | el.items = append(el.items, em) 34 | el.lock.Unlock() 35 | } 36 | 37 | func (el *errorLisk) reset() { 38 | el.lock.Lock() 39 | el.items = []*ErrorMessage{} 40 | el.lock.Unlock() 41 | } 42 | 43 | func (el *errorLisk) String() string { 44 | var sb strings.Builder 45 | el.lock.Lock() 46 | for i := range el.items { 47 | sb.WriteString(el.items[i].TaskName) 48 | sb.WriteString(": ") 49 | sb.WriteString(el.items[i].Error.Error()) 50 | sb.WriteString("\n") 51 | } 52 | el.lock.Unlock() 53 | return sb.String() 54 | } 55 | 56 | type StateMessage struct { 57 | TaskName string 58 | State State 59 | } 60 | 61 | type State interface { 62 | String() string 63 | } 64 | 65 | func newStateMessage(taskName string, state State) *StateMessage { 66 | return &StateMessage{ 67 | TaskName: taskName, 68 | State: state, 69 | } 70 | } 71 | 72 | type cancelList struct { 73 | lock sync.Mutex 74 | items []*StateMessage 75 | } 76 | 77 | func (cl *cancelList) append(sm *StateMessage) { 78 | cl.lock.Lock() 79 | cl.items = append(cl.items, sm) 80 | cl.lock.Unlock() 81 | } 82 | 83 | func (cl *cancelList) reset() { 84 | cl.lock.Lock() 85 | cl.items = []*StateMessage{} 86 | cl.lock.Unlock() 87 | } 88 | 89 | func (cl *cancelList) String() string { 90 | var sb strings.Builder 91 | cl.lock.Lock() 92 | for i := range cl.items { 93 | sb.WriteString(cl.items[i].TaskName) 94 | sb.WriteString(": ") 95 | sb.WriteString(cl.items[i].State.String()) 96 | sb.WriteString("\n") 97 | } 98 | cl.lock.Unlock() 99 | return sb.String() 100 | } 101 | -------------------------------------------------------------------------------- /executor.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type Executor struct { 8 | id uint32 9 | name string 10 | 11 | bindArgs interface{} 12 | task func(args map[string]interface{}) (interface{}, error) 13 | undo func(args map[string]interface{}) error 14 | undoSkipError bool 15 | 16 | dependency map[uint32]bool 17 | dependencyExpr DependencyExpression 18 | 19 | messageBuffer chan message 20 | subscribers []*chan message 21 | } 22 | 23 | func newExecutor(name string, f func(args map[string]interface{}) (interface{}, error), args interface{}) *Executor { 24 | return &Executor{ 25 | id: uuid.New().ID(), 26 | name: name, 27 | 28 | dependency: map[uint32]bool{}, 29 | dependencyExpr: DefaultTrueExpr, 30 | 31 | messageBuffer: make(chan message), 32 | subscribers: []*chan message{}, 33 | 34 | bindArgs: args, 35 | task: f, 36 | undo: EmptyUndoFunc, 37 | } 38 | } 39 | 40 | // Create a dependency expression for the executor. 41 | // It means the task launching may depend on executor `d`. 42 | func (e *Executor) NewDependencyExpr(d *Executor) DependencyExpression { 43 | if _, exists := e.dependency[d.id]; !exists { 44 | e.dependency[d.id] = false 45 | e.messageBuffer = make(chan message, cap(e.messageBuffer)+1) 46 | d.subscribers = append(d.subscribers, &e.messageBuffer) 47 | } 48 | return newDependencyExpr(e.dependency, d.id) 49 | } 50 | 51 | // Get dependency expression of the executor. 52 | func (e *Executor) DependencyExpr() DependencyExpression { 53 | return e.dependencyExpr 54 | } 55 | 56 | // Set dependency expression for the executor. `Expr` is a dependency expression. 57 | func (e *Executor) SetDependency(Expr DependencyExpression) *Executor { 58 | e.dependencyExpr = Expr 59 | return e 60 | } 61 | 62 | func (e *Executor) calcDependency() bool { 63 | return e.dependencyExpr.f() 64 | } 65 | 66 | func (e *Executor) markDependency(id uint32, finished bool) { 67 | e.dependency[id] = finished 68 | } 69 | 70 | // Set undo function the task executor. The undo function will get all arguments of the task function. 71 | func (e *Executor) SetUndoFunc(undo func(args map[string]interface{}) error, skipError bool) *Executor { 72 | e.undo = undo 73 | e.undoSkipError = skipError 74 | return e 75 | } 76 | 77 | // Get task name of the executor. 78 | func (e *Executor) Name() string { 79 | return e.name 80 | } 81 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import "strings" 4 | 5 | // ---------- Executor-Level Errors ----------- 6 | 7 | // It means the task is cancelled by the controller because the some other task return a fatal error. 8 | // If a task is aborted with args["CANCEL"].(context.Context).done(), it should return ErrCancelled. 9 | // And its running state can be set into the error. 10 | // The task and its state will be put into Cancelled list. 11 | type ErrCancelled struct { 12 | State State 13 | } 14 | 15 | func (e ErrCancelled) Error() string { 16 | return "Error: Task is canncelled due to other errors." 17 | } 18 | 19 | // It means the task failed, but you don't want the tasks execution aborted. 20 | // The failed task will be put into TaskErrors if the tasks execution finally failed. 21 | type ErrSilentFail struct{} 22 | 23 | func (e ErrSilentFail) Error() string { 24 | return "Error: Task failed in silence." 25 | } 26 | 27 | // ---------- Controller-Level Errors ----------- 28 | 29 | // It means the controller's termination condition haven't been set. 30 | type ErrNoTermination struct{} 31 | 32 | func (ErrNoTermination) Error() string { 33 | return "Error: No termination condition has been set!" 34 | } 35 | 36 | // It means there is loop dependency among the tasks. 37 | type ErrLoopDependency struct { 38 | State State 39 | } 40 | 41 | func (e ErrLoopDependency) Error() string { 42 | return "Error: Tasks has loop dependency." 43 | } 44 | 45 | // It means the controller doesn't support PoolRun() because not all dependency expressions are `AND`. 46 | type ErrPoolUnsupport struct{} 47 | 48 | func (ErrPoolUnsupport) Error() string { 49 | return "Error: PoolRun is not support when any dependency is not AND!" 50 | } 51 | 52 | // It means some fatal errors occur so the execution failed. 53 | // It consists of multiple errors. TaskErrors: errors from task running. 54 | // UndoErrors: errors from undo function running. Cancelled: running but cancelled tasks. 55 | type ErrAborted struct { 56 | TaskErrors []*ErrorMessage 57 | UndoErrors []*ErrorMessage 58 | Cancelled []*StateMessage 59 | } 60 | 61 | func (e ErrAborted) Error() string { 62 | var sb strings.Builder 63 | sb.WriteString("\n[x] TaskErrors:\n") 64 | sb.WriteString((&errorLisk{items: e.TaskErrors}).String()) 65 | sb.WriteString("[-] UndoErrors:\n") 66 | sb.WriteString((&errorLisk{items: e.UndoErrors}).String()) 67 | sb.WriteString("[/] Cancelled:\n") 68 | sb.WriteString((&cancelList{items: e.Cancelled}).String()) 69 | return sb.String() 70 | } 71 | 72 | -------------------------------------------------------------------------------- /dependency_test.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import "testing" 4 | 5 | func TestDependency(t *testing.T) { 6 | A := newExecutor("A", nil, "A") 7 | B := newExecutor("B", nil, "B") 8 | C := newExecutor("C", nil, "C") 9 | D := newExecutor("D", nil, "D") 10 | E := newExecutor("E", nil, "E") 11 | F := newExecutor("F", nil, "F") 12 | 13 | // C <- A && B 14 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) 15 | 16 | // D <- A || B 17 | D.SetDependency(MakeOrExpr(D.NewDependencyExpr(A), D.NewDependencyExpr(B))) 18 | 19 | // E <- !D 20 | E.SetDependency(MakeNotExpr(E.NewDependencyExpr(D))) 21 | 22 | // F <- B ^ D 23 | F.SetDependency(MakeXorExpr(F.NewDependencyExpr(B), F.NewDependencyExpr(D))) 24 | 25 | // value table: 26 | // A B C D E F 27 | // 0 0 0 0 1 0 28 | // 1 0 0 1 0 1 29 | // 0 1 0 1 0 0 30 | // 1 1 1 1 0 0 31 | checkResult := func(valA bool, valB bool, valC bool, valD bool, valE bool, valF bool) bool { 32 | if valA { 33 | A.SetDependency(DefaultTrueExpr) 34 | } else { 35 | A.SetDependency(DefaultFalseExpr) 36 | } 37 | if valB { 38 | B.SetDependency(DefaultTrueExpr) 39 | } else { 40 | B.SetDependency(DefaultFalseExpr) 41 | } 42 | 43 | C.markDependency(A.id, A.calcDependency()) 44 | C.markDependency(B.id, B.calcDependency()) 45 | 46 | D.markDependency(A.id, A.calcDependency()) 47 | D.markDependency(B.id, B.calcDependency()) 48 | 49 | E.markDependency(D.id, D.calcDependency()) 50 | 51 | F.markDependency(B.id, B.calcDependency()) 52 | F.markDependency(D.id, D.calcDependency()) 53 | 54 | if valC != C.calcDependency() { 55 | return false 56 | } 57 | if valD != D.calcDependency() { 58 | return false 59 | } 60 | if valE != E.calcDependency() { 61 | return false 62 | } 63 | if valF != F.calcDependency() { 64 | return false 65 | } 66 | return true 67 | } 68 | 69 | // 0 0 0 0 1 0 70 | if !checkResult(false, false, false, false, true, false) { 71 | t.Errorf("Error: A=%v, B=%v, C=%v, D=%v, E=%v, F=%v\n", A.calcDependency(), B.calcDependency(), C.calcDependency(), D.calcDependency(), E.calcDependency(), F.calcDependency()) 72 | } 73 | 74 | // 1 0 0 1 0 1 75 | if !checkResult(true, false, false, true, false, true) { 76 | t.Errorf("Error: A=%v, B=%v, C=%v, D=%v, E=%v, F=%v\n", A.calcDependency(), B.calcDependency(), C.calcDependency(), D.calcDependency(), E.calcDependency(), F.calcDependency()) 77 | } 78 | 79 | // 0 1 0 1 0 0 80 | if !checkResult(false, true, false, true, false, false) { 81 | t.Errorf("Error: A=%v, B=%v, C=%v, D=%v, E=%v, F=%v\n", A.calcDependency(), B.calcDependency(), C.calcDependency(), D.calcDependency(), E.calcDependency(), F.calcDependency()) 82 | } 83 | 84 | // 1 1 1 1 0 0 85 | if !checkResult(true, true, true, true, false, false) { 86 | t.Errorf("Error: A=%v, B=%v, C=%v, D=%v, E=%v, F=%v\n", A.calcDependency(), B.calcDependency(), C.calcDependency(), D.calcDependency(), E.calcDependency(), F.calcDependency()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/panjf2000/ants/v2" 7 | ) 8 | 9 | // Goroutine pool interface. It should be blocked until worker available. 10 | type GoroutinePool interface { 11 | Go(task func()) error 12 | } 13 | 14 | // Run the execution with a Coroutine Pool. If success, return a map[name]value, where names are task 15 | // of termination dependent tasks and values are their return value. 16 | // If failed, return ErrNoTermination, ErrLoopDependency or ErrAborted 17 | func (m *TCController) PoolRun(pool GoroutinePool) (map[string]interface{}, error) { 18 | if len(m.termination.dependency) == 0 { 19 | return nil, ErrNoTermination{} 20 | } 21 | taskorder, noloop := m.analyzeDependency() 22 | if !noloop { 23 | return nil, ErrLoopDependency{} 24 | } 25 | sortedId, canSort := m.sortExecutor(taskorder) 26 | if !canSort { 27 | return nil, ErrPoolUnsupport{} 28 | } 29 | 30 | defer m.reset() 31 | 32 | wg := sync.WaitGroup{} 33 | lauchLoop: 34 | for _, taskid := range sortedId { 35 | e := m.executors[taskid] 36 | wg.Add(1) 37 | err := pool.Go(func() { 38 | m.launch(e, &wg) 39 | }) 40 | if err != nil { 41 | return nil, err 42 | } 43 | select { 44 | case <-m.cancelCtx.Done(): 45 | break lauchLoop 46 | default: 47 | } 48 | } 49 | 50 | // wait termination 51 | t := m.termination 52 | Results := map[string]interface{}{} 53 | Aborted := false 54 | 55 | waitLoop: 56 | for !t.dependencyExpr.f() { 57 | select { 58 | case <-m.cancelCtx.Done(): 59 | // aborted 60 | Aborted = true 61 | break waitLoop 62 | case msg := <-t.messageBuffer: 63 | t.markDependency(msg.senderId, true) 64 | Results[msg.senderName] = msg.value 65 | } 66 | } 67 | if !Aborted { 68 | // all done! 69 | m.cancelFunc() 70 | wg.Wait() 71 | return Results, nil 72 | } else { 73 | // aborted because of some error 74 | wg.Wait() 75 | returnErr := ErrAborted{ 76 | TaskErrors: m.errorMsgs.items, 77 | Cancelled: m.cancelled.items, 78 | } 79 | 80 | // do the rollback 81 | returnErr.UndoErrors = m.undoStack.undoAll(&m.errorMsgs, &m.cancelled).items 82 | // fmt.Println(returnErr.Error()) 83 | return nil, returnErr 84 | } 85 | } 86 | 87 | // Default coroutine pool: actually not a coroutine pool but only launch new goroutines. 88 | type DefaultNoPool struct{} 89 | 90 | func (DefaultNoPool) Go(task func()) error { 91 | go task() 92 | return nil 93 | } 94 | 95 | // Default coroutine pool: base on ants pool. 96 | type DefaultPool struct { 97 | pool *ants.Pool 98 | } 99 | 100 | // Create a default coroutine pool with `size`. 101 | func NewDefaultPool(size int) DefaultPool { 102 | if size <= 0 { 103 | pool, _ := ants.NewPool(size) 104 | return DefaultPool{pool} 105 | } else { 106 | pool, _ := ants.NewPool(size, ants.WithPreAlloc(true)) 107 | return DefaultPool{pool} 108 | } 109 | } 110 | 111 | func (p DefaultPool) Go(task func()) error { 112 | return p.pool.Submit(task) 113 | } 114 | 115 | func (p DefaultPool) Close() { 116 | p.pool.Release() 117 | } 118 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func hello(args map[string]interface{}) (interface{}, error) { 9 | time.Sleep(10 * time.Millisecond) 10 | fmt.Println("hello") 11 | return 1, nil 12 | } 13 | 14 | func world(args map[string]interface{}) (interface{}, error) { 15 | time.Sleep(20 * time.Millisecond) 16 | fmt.Println("world") 17 | return 2, nil 18 | } 19 | 20 | func helloworld(args map[string]interface{}) (interface{}, error) { 21 | fmt.Println("helloworld") 22 | return 3, nil 23 | } 24 | 25 | func foo(args map[string]interface{}) (interface{}, error) { 26 | time.Sleep(30 * time.Millisecond) 27 | fmt.Println("foo") 28 | return 4, nil 29 | } 30 | 31 | func bar(args map[string]interface{}) (interface{}, error) { 32 | time.Sleep(40 * time.Millisecond) 33 | fmt.Println("bar") 34 | return 5, nil 35 | } 36 | 37 | func foobar(args map[string]interface{}) (interface{}, error) { 38 | fmt.Println("foobar") 39 | return 5, nil 40 | } 41 | 42 | func ExampleTCController_BatchRun() { 43 | // in this example: 44 | // hello -+ 45 | // +-(&&)-> helloworld + 46 | // world -+ + 47 | // foo -+ +-(&&)-> [termination] 48 | // +-(||)-> foobar + 49 | // bar -+ 50 | 51 | controller := NewTCController() 52 | 53 | hello := controller.AddTask("hello", hello, 0) 54 | world := controller.AddTask("world", world, 1) 55 | helloworld := controller.AddTask("helloworld", helloworld, 2) 56 | foo := controller.AddTask("foo", foo, 3) 57 | bar := controller.AddTask("bar", bar, 4) 58 | foobar := controller.AddTask("foobar", foobar, 5) 59 | 60 | helloworld.SetDependency(MakeAndExpr(helloworld.NewDependencyExpr(hello), helloworld.NewDependencyExpr(world))) 61 | 62 | foobar.SetDependency(MakeOrExpr(foobar.NewDependencyExpr(foo), foobar.NewDependencyExpr(bar))) 63 | 64 | controller.SetTermination(MakeAndExpr(controller.NewTerminationExpr(foobar), controller.NewTerminationExpr(helloworld))) 65 | 66 | _, err := controller.BatchRun() 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | // Output: 72 | // hello 73 | // world 74 | // helloworld 75 | // foo 76 | // foobar 77 | // bar 78 | } 79 | 80 | func sleeptask(args map[string]interface{}) (interface{}, error) { 81 | time.Sleep(10 * time.Millisecond * time.Duration(args["BIND"].(int))) 82 | fmt.Println(args["NAME"].(string)) 83 | return nil, nil 84 | } 85 | 86 | func ExampleTCController_PoolRun() { 87 | // in this example: 88 | // we use ants pool (panjf2000/ants) with cap=2 89 | // A(1) -+ 90 | // +-(&&)-> E(2) + 91 | // B(3) -+ | 92 | // C(1) -+ +-(&&)-> H(1) --> [termination] 93 | // +-(&&)-> F(2) + 94 | // D(2) -+ | 95 | // G(2) ---------------+ 96 | controller := NewTCController() 97 | 98 | A := controller.AddTask("A", sleeptask, 1) 99 | B := controller.AddTask("B", sleeptask, 3) 100 | C := controller.AddTask("C", sleeptask, 1) 101 | D := controller.AddTask("D", sleeptask, 2) 102 | E := controller.AddTask("E", sleeptask, 2) 103 | F := controller.AddTask("F", sleeptask, 2) 104 | G := controller.AddTask("G", sleeptask, 2) 105 | H := controller.AddTask("H", sleeptask, 1) 106 | 107 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(A), E.NewDependencyExpr(B))) 108 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(C), F.NewDependencyExpr(D))) 109 | H.SetDependency(MakeAndExpr( 110 | MakeAndExpr(H.NewDependencyExpr(E), H.NewDependencyExpr(F)), 111 | H.NewDependencyExpr(G), 112 | )) 113 | 114 | controller.SetTermination(controller.NewTerminationExpr(H)) 115 | 116 | pool := NewDefaultPool(2) 117 | defer pool.Close() 118 | 119 | _, err := controller.PoolRun(pool) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | // Output: 125 | // A 126 | // C 127 | // B 128 | // D 129 | // G 130 | // E 131 | // F 132 | // H 133 | } 134 | -------------------------------------------------------------------------------- /dependency.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // A dependency expression is a filter to describe the tasks' dependency 8 | // A task will be launched only if the expression is true. 9 | type DependencyExpression struct { 10 | f func() bool 11 | allAnd bool 12 | } 13 | 14 | func MakeNotExpr(Expr DependencyExpression) DependencyExpression { 15 | return DependencyExpression{ 16 | f: func() bool { 17 | return !Expr.f() 18 | }, 19 | allAnd: false, 20 | } 21 | } 22 | 23 | func MakeAndExpr(Expr1 DependencyExpression, Expr2 DependencyExpression) DependencyExpression { 24 | return DependencyExpression{ 25 | f: func() bool { 26 | return Expr1.f() && Expr2.f() 27 | }, 28 | allAnd: Expr1.allAnd && Expr2.allAnd, 29 | } 30 | } 31 | 32 | func MakeOrExpr(Expr1 DependencyExpression, Expr2 DependencyExpression) DependencyExpression { 33 | return DependencyExpression{ 34 | f: func() bool { 35 | return Expr1.f() || Expr2.f() 36 | }, 37 | allAnd: false, 38 | } 39 | } 40 | 41 | func MakeXorExpr(Expr1 DependencyExpression, Expr2 DependencyExpression) DependencyExpression { 42 | return DependencyExpression{ 43 | f: func() bool { 44 | return (Expr1.f() && !Expr2.f()) || (!Expr1.f() && Expr2.f()) 45 | }, 46 | allAnd: false, 47 | } 48 | } 49 | 50 | func newDependencyExpr(valMap map[uint32]bool, key uint32) DependencyExpression { 51 | return DependencyExpression{ 52 | f: func() bool { 53 | return valMap[key] 54 | }, 55 | allAnd: true, 56 | } 57 | } 58 | 59 | func (m *TCController) analyzeDependency() (map[uint32]int, bool) { 60 | const ( 61 | white = 0 62 | gray = 1 63 | black = 2 64 | ) 65 | color := map[uint32]int{} 66 | order := map[uint32]int{} 67 | var dfs func(curr uint32) bool 68 | dfs = func(curr uint32) bool { 69 | if len(m.executors[curr].dependency) == 0 { 70 | order[curr] = 0 71 | color[curr] = black 72 | return true 73 | } 74 | 75 | color[curr] = gray 76 | maxorder := 0 77 | for neighbor := range m.executors[curr].dependency { 78 | switch color[neighbor] { 79 | case white: 80 | if !dfs(neighbor) { 81 | return false 82 | } 83 | maxorder = max(maxorder, order[neighbor]) 84 | case gray: 85 | return false 86 | case black: 87 | maxorder = max(maxorder, order[neighbor]) 88 | } 89 | } 90 | color[curr] = black 91 | order[curr] = maxorder + 1 92 | return true 93 | } 94 | 95 | for taskid := range m.executors { 96 | if !dfs(taskid) { 97 | return nil, false 98 | } 99 | } 100 | 101 | return order, true 102 | } 103 | 104 | func max(x, y int) int { 105 | if x > y { 106 | return x 107 | } 108 | return y 109 | } 110 | 111 | func (m *TCController) sortExecutor(taskorder map[uint32]int) ([]uint32, bool) { 112 | type item struct { 113 | taskid uint32 114 | taskname string 115 | order int 116 | } 117 | itemlist := make([]item, 0, len(taskorder)) 118 | res := make([]uint32, 0, len(taskorder)) 119 | for taskid, order := range taskorder { 120 | e := m.executors[taskid] 121 | if !e.dependencyExpr.allAnd { 122 | return nil, false 123 | } 124 | itemlist = append(itemlist, item{ 125 | taskid: taskid, 126 | taskname: e.name, 127 | order: order, 128 | }) 129 | } 130 | sort.Slice(itemlist, func(i, j int) bool { 131 | if itemlist[i].order == itemlist[j].order { 132 | return itemlist[i].taskname < itemlist[j].taskname 133 | } 134 | return itemlist[i].order < itemlist[j].order 135 | }) 136 | for i := range itemlist { 137 | res = append(res, itemlist[i].taskid) 138 | } 139 | return res, true 140 | } 141 | 142 | // default dependency expression: always return true 143 | var DefaultTrueExpr = DependencyExpression{ 144 | f: func() bool { 145 | return true 146 | }, 147 | allAnd: true, 148 | } 149 | 150 | // default dependency expression: always return false 151 | var DefaultFalseExpr = DependencyExpression{ 152 | f: func() bool { 153 | return false 154 | }, 155 | allAnd: true, 156 | } 157 | -------------------------------------------------------------------------------- /tcc.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // Task Concurrency Controller 12 | type TCController struct { 13 | executors map[uint32]*Executor 14 | 15 | cancelCtx context.Context 16 | cancelFunc context.CancelFunc 17 | 18 | termination *Executor 19 | 20 | cancelled cancelList 21 | errorMsgs errorLisk 22 | undoStack undoStack 23 | } 24 | 25 | // Create an empty task concurrency controller 26 | func NewTCController() *TCController { 27 | ctx, cf := context.WithCancel(context.Background()) 28 | return &TCController{ 29 | executors: map[uint32]*Executor{}, 30 | cancelCtx: ctx, 31 | cancelFunc: cf, 32 | termination: newExecutor("TERMINATION", nil, nil), 33 | cancelled: cancelList{}, 34 | errorMsgs: errorLisk{}, 35 | undoStack: undoStack{}, 36 | } 37 | } 38 | 39 | // Add a task to the controller. `name` is a user-defined string identifier of the task. 40 | // `f` is the task function. `args` is arguments bind with the task, which can be obtained 41 | // inside the task function from args["BIND"]. 42 | func (m *TCController) AddTask(name string, f func(args map[string]interface{}) (interface{}, error), args interface{}) *Executor { 43 | e := newExecutor(name, f, args) 44 | m.executors[e.id] = e 45 | return e 46 | } 47 | 48 | // Set termination condition for the controller. `Expr` is a dependency expression. 49 | func (m *TCController) SetTermination(Expr DependencyExpression) { 50 | m.termination.SetDependency(Expr) 51 | } 52 | 53 | // Get termination condition of the controller. 54 | func (m *TCController) TerminationExpr() DependencyExpression { 55 | return m.termination.dependencyExpr 56 | } 57 | 58 | // Create a termination dependency expression for the controller. 59 | // It means the execution termination may depend on task `d`. 60 | func (m *TCController) NewTerminationExpr(d *Executor) DependencyExpression { 61 | if _, exists := m.termination.dependency[d.id]; !exists { 62 | m.termination.dependency[d.id] = false 63 | m.termination.messageBuffer = make(chan message, cap(m.termination.messageBuffer)+1) 64 | d.subscribers = append(d.subscribers, &m.termination.messageBuffer) 65 | } 66 | return newDependencyExpr(m.termination.dependency, d.id) 67 | } 68 | 69 | // Run the execution. If success, return a map[name]value, where names are task 70 | // of termination dependent tasks and values are their return value. 71 | // If failed, return ErrNoTermination, ErrLoopDependency or ErrAborted 72 | func (m *TCController) BatchRun() (map[string]interface{}, error) { 73 | if len(m.termination.dependency) == 0 { 74 | return nil, ErrNoTermination{} 75 | } 76 | if _, noloop := m.analyzeDependency(); !noloop { 77 | return nil, ErrLoopDependency{} 78 | } 79 | 80 | defer m.reset() 81 | 82 | wg := sync.WaitGroup{} 83 | wg.Add(len(m.executors)) 84 | for taskid := range m.executors { 85 | e := m.executors[taskid] 86 | go m.launch(e, &wg) 87 | } 88 | 89 | // wait termination 90 | t := m.termination 91 | Results := map[string]interface{}{} 92 | Aborted := false 93 | 94 | waitLoop: 95 | for !t.dependencyExpr.f() { 96 | select { 97 | case <-m.cancelCtx.Done(): 98 | // aborted 99 | Aborted = true 100 | break waitLoop 101 | case msg := <-t.messageBuffer: 102 | t.markDependency(msg.senderId, true) 103 | Results[msg.senderName] = msg.value 104 | } 105 | } 106 | if !Aborted { 107 | // all done! 108 | m.cancelFunc() 109 | wg.Wait() 110 | return Results, nil 111 | } else { 112 | // aborted because of some error 113 | wg.Wait() 114 | returnErr := ErrAborted{ 115 | TaskErrors: m.errorMsgs.items, 116 | Cancelled: m.cancelled.items, 117 | } 118 | 119 | // do the rollback 120 | returnErr.UndoErrors = m.undoStack.undoAll(&m.errorMsgs, &m.cancelled).items 121 | // fmt.Println(returnErr.Error()) 122 | return nil, returnErr 123 | } 124 | } 125 | 126 | func (m *TCController) launch(e *Executor, wg *sync.WaitGroup) { 127 | defer wg.Done() 128 | args := map[string]interface{}{"BIND": e.bindArgs, "CANCEL": m.cancelCtx, "NAME": e.name} 129 | 130 | for !e.dependencyExpr.f() { 131 | // wait until dep ok 132 | select { 133 | case <-m.cancelCtx.Done(): 134 | return 135 | case msg := <-e.messageBuffer: 136 | e.markDependency(msg.senderId, true) 137 | args[msg.senderName] = msg.value 138 | } 139 | } 140 | 141 | outMsg := message{senderId: e.id, senderName: e.name} 142 | result, err := e.task(args) 143 | if err != nil { 144 | switch err := err.(type) { 145 | case ErrSilentFail: 146 | m.errorMsgs.append(newErrorMessage(e.name, err)) 147 | case ErrCancelled: 148 | m.cancelled.append(newStateMessage(e.name, err.State)) 149 | default: 150 | m.errorMsgs.append(newErrorMessage(e.name, err)) 151 | m.cancelFunc() 152 | } 153 | return 154 | } else { 155 | outMsg.value = result 156 | 157 | // add to finished stack... 158 | m.undoStack.push(newUndoFunc(e.name, e.undoSkipError, e.undo, args)) 159 | } 160 | 161 | for _, subscriber := range e.subscribers { 162 | *subscriber <- outMsg 163 | } 164 | } 165 | 166 | func (m *TCController) reset() { 167 | m.cancelCtx, m.cancelFunc = context.WithCancel(context.Background()) 168 | m.cancelled.reset() 169 | m.errorMsgs.reset() 170 | m.undoStack.reset() 171 | for _, e := range m.executors { 172 | e.messageBuffer = make(chan message, cap(e.messageBuffer)) 173 | for dep := range e.dependency { 174 | e.dependency[dep] = false 175 | } 176 | } 177 | for term := range m.termination.dependency { 178 | m.termination.dependency[term] = false 179 | } 180 | } 181 | 182 | // The inner state of the controller 183 | func (m *TCController) String() string { 184 | var sb strings.Builder 185 | sb.WriteString("\ncanncelled list:\n") 186 | sb.WriteString(m.cancelled.String()) 187 | sb.WriteString("errmessage list:\n") 188 | sb.WriteString(m.errorMsgs.String()) 189 | sb.WriteString("tasks:\n") 190 | ids := make([]uint32, 0, len(m.executors)) 191 | for id := range m.executors { 192 | ids = append(ids, id) 193 | } 194 | sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) 195 | for _, id := range ids { 196 | e := m.executors[id] 197 | sb.WriteString(e.name) 198 | sb.WriteString(fmt.Sprintf("[msgBuffer cap=%d]: (", cap(e.messageBuffer))) 199 | 200 | depids := make([]uint32, 0, len(e.dependency)) 201 | for depid := range e.dependency { 202 | depids = append(depids, depid) 203 | } 204 | sort.Slice(depids, func(i, j int) bool { return depids[i] < depids[j] }) 205 | for _, depid := range depids { 206 | sb.WriteString(m.executors[depid].name) 207 | sb.WriteString(", ") 208 | } 209 | sb.WriteString(")\n") 210 | } 211 | sb.WriteString(fmt.Sprintf("@termination[msgBuffer cap=%d]: (", cap(m.termination.messageBuffer))) 212 | termids := make([]uint32, 0, len(m.termination.dependency)) 213 | for termid := range m.termination.dependency { 214 | termids = append(termids, termid) 215 | } 216 | for _, termid := range termids { 217 | sb.WriteString(m.executors[termid].name) 218 | sb.WriteString(", ") 219 | } 220 | sb.WriteString(")\n") 221 | 222 | return sb.String() 223 | } 224 | -------------------------------------------------------------------------------- /coverage.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | github.com/piaodazhu/gotcc/message.go:19.64,24.2 1 1 3 | github.com/piaodazhu/gotcc/message.go:31.47,35.2 3 1 4 | github.com/piaodazhu/gotcc/message.go:37.30,41.2 3 1 5 | github.com/piaodazhu/gotcc/message.go:43.38,46.26 3 1 6 | github.com/piaodazhu/gotcc/message.go:52.2,53.20 2 1 7 | github.com/piaodazhu/gotcc/message.go:46.26,51.3 4 1 8 | github.com/piaodazhu/gotcc/message.go:65.66,70.2 1 1 9 | github.com/piaodazhu/gotcc/message.go:77.48,81.2 3 1 10 | github.com/piaodazhu/gotcc/message.go:83.31,87.2 3 1 11 | github.com/piaodazhu/gotcc/message.go:89.39,92.26 3 1 12 | github.com/piaodazhu/gotcc/message.go:98.2,99.20 2 1 13 | github.com/piaodazhu/gotcc/message.go:92.26,97.3 4 1 14 | github.com/piaodazhu/gotcc/pool.go:17.84,18.40 1 1 15 | github.com/piaodazhu/gotcc/pool.go:21.2,22.13 2 1 16 | github.com/piaodazhu/gotcc/pool.go:25.2,26.14 2 1 17 | github.com/piaodazhu/gotcc/pool.go:30.2,34.34 3 1 18 | github.com/piaodazhu/gotcc/pool.go:51.2,56.28 4 1 19 | github.com/piaodazhu/gotcc/pool.go:67.2,67.14 1 1 20 | github.com/piaodazhu/gotcc/pool.go:18.40,20.3 1 1 21 | github.com/piaodazhu/gotcc/pool.go:22.13,24.3 1 1 22 | github.com/piaodazhu/gotcc/pool.go:26.14,28.3 1 1 23 | github.com/piaodazhu/gotcc/pool.go:34.34,37.25 3 1 24 | github.com/piaodazhu/gotcc/pool.go:40.3,40.17 1 1 25 | github.com/piaodazhu/gotcc/pool.go:43.3,43.10 1 1 26 | github.com/piaodazhu/gotcc/pool.go:37.25,39.4 1 1 27 | github.com/piaodazhu/gotcc/pool.go:40.17,42.4 1 1 28 | github.com/piaodazhu/gotcc/pool.go:44.29,45.19 1 1 29 | github.com/piaodazhu/gotcc/pool.go:46.11,46.11 0 1 30 | github.com/piaodazhu/gotcc/pool.go:56.28,57.10 1 1 31 | github.com/piaodazhu/gotcc/pool.go:58.29,61.18 2 1 32 | github.com/piaodazhu/gotcc/pool.go:62.33,64.39 2 1 33 | github.com/piaodazhu/gotcc/pool.go:67.14,72.3 3 1 34 | github.com/piaodazhu/gotcc/pool.go:72.8,84.3 4 1 35 | github.com/piaodazhu/gotcc/pool.go:90.44,93.2 2 1 36 | github.com/piaodazhu/gotcc/pool.go:101.43,102.15 1 1 37 | github.com/piaodazhu/gotcc/pool.go:102.15,105.3 2 1 38 | github.com/piaodazhu/gotcc/pool.go:105.8,108.3 2 1 39 | github.com/piaodazhu/gotcc/pool.go:111.44,113.2 1 1 40 | github.com/piaodazhu/gotcc/pool.go:115.30,117.2 1 1 41 | github.com/piaodazhu/gotcc/rollback.go:19.132,26.2 1 1 42 | github.com/piaodazhu/gotcc/rollback.go:28.40,32.2 3 1 43 | github.com/piaodazhu/gotcc/rollback.go:34.29,38.2 3 1 44 | github.com/piaodazhu/gotcc/rollback.go:40.86,42.41 2 1 45 | github.com/piaodazhu/gotcc/rollback.go:55.2,55.19 1 1 46 | github.com/piaodazhu/gotcc/rollback.go:42.41,48.17 5 1 47 | github.com/piaodazhu/gotcc/rollback.go:48.17,50.29 2 1 48 | github.com/piaodazhu/gotcc/rollback.go:50.29,52.5 1 1 49 | github.com/piaodazhu/gotcc/rollback.go:59.61,61.2 1 1 50 | github.com/piaodazhu/gotcc/tcc.go:26.38,37.2 2 1 51 | github.com/piaodazhu/gotcc/tcc.go:42.131,46.2 3 1 52 | github.com/piaodazhu/gotcc/tcc.go:49.66,51.2 1 1 53 | github.com/piaodazhu/gotcc/tcc.go:54.63,56.2 1 1 54 | github.com/piaodazhu/gotcc/tcc.go:60.77,61.58 1 1 55 | github.com/piaodazhu/gotcc/tcc.go:66.2,66.58 1 1 56 | github.com/piaodazhu/gotcc/tcc.go:61.58,65.3 3 1 57 | github.com/piaodazhu/gotcc/tcc.go:72.67,73.40 1 1 58 | github.com/piaodazhu/gotcc/tcc.go:76.2,76.49 1 1 59 | github.com/piaodazhu/gotcc/tcc.go:80.2,84.34 4 1 60 | github.com/piaodazhu/gotcc/tcc.go:90.2,95.28 4 1 61 | github.com/piaodazhu/gotcc/tcc.go:106.2,106.14 1 1 62 | github.com/piaodazhu/gotcc/tcc.go:73.40,75.3 1 1 63 | github.com/piaodazhu/gotcc/tcc.go:76.49,78.3 1 1 64 | github.com/piaodazhu/gotcc/tcc.go:84.34,87.3 2 1 65 | github.com/piaodazhu/gotcc/tcc.go:95.28,96.10 1 1 66 | github.com/piaodazhu/gotcc/tcc.go:97.29,100.18 2 1 67 | github.com/piaodazhu/gotcc/tcc.go:101.33,103.39 2 1 68 | github.com/piaodazhu/gotcc/tcc.go:106.14,111.3 3 1 69 | github.com/piaodazhu/gotcc/tcc.go:111.8,123.3 4 1 70 | github.com/piaodazhu/gotcc/tcc.go:126.64,130.28 3 1 71 | github.com/piaodazhu/gotcc/tcc.go:141.2,143.16 3 1 72 | github.com/piaodazhu/gotcc/tcc.go:161.2,161.43 1 1 73 | github.com/piaodazhu/gotcc/tcc.go:130.28,132.10 1 1 74 | github.com/piaodazhu/gotcc/tcc.go:133.29,134.10 1 1 75 | github.com/piaodazhu/gotcc/tcc.go:135.33,137.36 2 1 76 | github.com/piaodazhu/gotcc/tcc.go:143.16,144.28 1 1 77 | github.com/piaodazhu/gotcc/tcc.go:153.3,153.9 1 1 78 | github.com/piaodazhu/gotcc/tcc.go:145.22,146.52 1 1 79 | github.com/piaodazhu/gotcc/tcc.go:147.21,148.58 1 1 80 | github.com/piaodazhu/gotcc/tcc.go:149.11,151.18 2 1 81 | github.com/piaodazhu/gotcc/tcc.go:154.8,159.3 2 1 82 | github.com/piaodazhu/gotcc/tcc.go:161.43,163.3 1 1 83 | github.com/piaodazhu/gotcc/tcc.go:166.32,171.32 5 1 84 | github.com/piaodazhu/gotcc/tcc.go:177.2,177.45 1 1 85 | github.com/piaodazhu/gotcc/tcc.go:171.32,173.33 2 1 86 | github.com/piaodazhu/gotcc/tcc.go:173.33,175.4 1 1 87 | github.com/piaodazhu/gotcc/tcc.go:177.45,179.3 1 1 88 | github.com/piaodazhu/gotcc/tcc.go:183.40,191.30 8 1 89 | github.com/piaodazhu/gotcc/tcc.go:194.2,194.38 1 1 90 | github.com/piaodazhu/gotcc/tcc.go:195.2,195.25 1 1 91 | github.com/piaodazhu/gotcc/tcc.go:211.2,213.47 3 1 92 | github.com/piaodazhu/gotcc/tcc.go:216.2,216.33 1 1 93 | github.com/piaodazhu/gotcc/tcc.go:220.2,222.20 2 1 94 | github.com/piaodazhu/gotcc/tcc.go:191.30,193.3 1 1 95 | github.com/piaodazhu/gotcc/tcc.go:194.38,194.64 1 1 96 | github.com/piaodazhu/gotcc/tcc.go:195.25,201.35 5 1 97 | github.com/piaodazhu/gotcc/tcc.go:204.3,204.42 1 1 98 | github.com/piaodazhu/gotcc/tcc.go:205.3,205.32 1 1 99 | github.com/piaodazhu/gotcc/tcc.go:209.3,209.24 1 1 100 | github.com/piaodazhu/gotcc/tcc.go:201.35,203.4 1 1 101 | github.com/piaodazhu/gotcc/tcc.go:204.42,204.74 1 1 102 | github.com/piaodazhu/gotcc/tcc.go:205.32,208.4 2 1 103 | github.com/piaodazhu/gotcc/tcc.go:213.47,215.3 1 1 104 | github.com/piaodazhu/gotcc/tcc.go:216.33,219.3 2 1 105 | github.com/piaodazhu/gotcc/dependency.go:14.66,16.18 1 1 106 | github.com/piaodazhu/gotcc/dependency.go:16.18,18.4 1 1 107 | github.com/piaodazhu/gotcc/dependency.go:23.95,25.18 1 1 108 | github.com/piaodazhu/gotcc/dependency.go:25.18,27.4 1 1 109 | github.com/piaodazhu/gotcc/dependency.go:32.94,34.18 1 1 110 | github.com/piaodazhu/gotcc/dependency.go:34.18,36.4 1 1 111 | github.com/piaodazhu/gotcc/dependency.go:41.95,43.18 1 1 112 | github.com/piaodazhu/gotcc/dependency.go:43.18,45.4 1 1 113 | github.com/piaodazhu/gotcc/dependency.go:50.81,52.18 1 1 114 | github.com/piaodazhu/gotcc/dependency.go:52.18,54.4 1 1 115 | github.com/piaodazhu/gotcc/dependency.go:59.67,68.31 5 1 116 | github.com/piaodazhu/gotcc/dependency.go:95.2,95.34 1 1 117 | github.com/piaodazhu/gotcc/dependency.go:101.2,101.20 1 1 118 | github.com/piaodazhu/gotcc/dependency.go:68.31,69.45 1 1 119 | github.com/piaodazhu/gotcc/dependency.go:75.3,77.54 3 1 120 | github.com/piaodazhu/gotcc/dependency.go:90.3,92.14 3 1 121 | github.com/piaodazhu/gotcc/dependency.go:69.45,73.4 3 1 122 | github.com/piaodazhu/gotcc/dependency.go:77.54,78.27 1 1 123 | github.com/piaodazhu/gotcc/dependency.go:79.15,80.23 1 1 124 | github.com/piaodazhu/gotcc/dependency.go:83.5,83.46 1 1 125 | github.com/piaodazhu/gotcc/dependency.go:84.14,85.17 1 1 126 | github.com/piaodazhu/gotcc/dependency.go:86.15,87.46 1 1 127 | github.com/piaodazhu/gotcc/dependency.go:80.23,82.6 1 1 128 | github.com/piaodazhu/gotcc/dependency.go:95.34,96.19 1 1 129 | github.com/piaodazhu/gotcc/dependency.go:96.19,98.4 1 1 130 | github.com/piaodazhu/gotcc/dependency.go:104.24,105.11 1 1 131 | github.com/piaodazhu/gotcc/dependency.go:108.2,108.10 1 1 132 | github.com/piaodazhu/gotcc/dependency.go:105.11,107.3 1 1 133 | github.com/piaodazhu/gotcc/dependency.go:111.80,119.39 4 1 134 | github.com/piaodazhu/gotcc/dependency.go:130.2,130.43 1 1 135 | github.com/piaodazhu/gotcc/dependency.go:136.2,136.26 1 1 136 | github.com/piaodazhu/gotcc/dependency.go:139.2,139.18 1 1 137 | github.com/piaodazhu/gotcc/dependency.go:119.39,121.31 2 1 138 | github.com/piaodazhu/gotcc/dependency.go:124.3,128.5 1 1 139 | github.com/piaodazhu/gotcc/dependency.go:121.31,123.4 1 1 140 | github.com/piaodazhu/gotcc/dependency.go:130.43,131.45 1 1 141 | github.com/piaodazhu/gotcc/dependency.go:134.3,134.47 1 1 142 | github.com/piaodazhu/gotcc/dependency.go:131.45,133.4 1 1 143 | github.com/piaodazhu/gotcc/dependency.go:136.26,138.3 1 1 144 | github.com/piaodazhu/gotcc/dependency.go:144.17,146.3 1 1 145 | github.com/piaodazhu/gotcc/dependency.go:152.17,154.3 1 1 146 | github.com/piaodazhu/gotcc/error.go:15.38,17.2 1 1 147 | github.com/piaodazhu/gotcc/error.go:23.39,25.2 1 1 148 | github.com/piaodazhu/gotcc/error.go:32.40,34.2 1 1 149 | github.com/piaodazhu/gotcc/error.go:41.43,43.2 1 1 150 | github.com/piaodazhu/gotcc/error.go:48.40,50.2 1 1 151 | github.com/piaodazhu/gotcc/error.go:61.36,70.2 8 1 152 | github.com/piaodazhu/gotcc/executor.go:23.117,38.2 1 1 153 | github.com/piaodazhu/gotcc/executor.go:42.72,43.46 1 1 154 | github.com/piaodazhu/gotcc/executor.go:48.2,48.46 1 1 155 | github.com/piaodazhu/gotcc/executor.go:43.46,47.3 3 1 156 | github.com/piaodazhu/gotcc/executor.go:52.58,54.2 1 1 157 | github.com/piaodazhu/gotcc/executor.go:57.71,60.2 2 1 158 | github.com/piaodazhu/gotcc/executor.go:62.42,64.2 1 1 159 | github.com/piaodazhu/gotcc/executor.go:66.61,68.2 1 1 160 | github.com/piaodazhu/gotcc/executor.go:71.104,75.2 3 1 161 | github.com/piaodazhu/gotcc/executor.go:78.34,80.2 1 1 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/piaodazhu/gotcc.svg)](https://pkg.go.dev/github.com/piaodazhu/gotcc) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/piaodazhu/gotcc)](https://goreportcard.com/report/github.com/piaodazhu/gotcc) 3 | [![codecov](https://codecov.io/gh/piaodazhu/gotcc/branch/master/graph/badge.svg?token=KMOWEKDPN5)](https://codecov.io/gh/piaodazhu/gotcc) 4 | 5 | # gotcc 6 | 7 | 🤖 `gotcc` is a Golang package for Task Concurrency Control. It allows you to define tasks, their dependencies, and the controller will run the tasks concurrently while respecting the dependencies. 8 | 9 | Features of `gotcc` 10 | - Automatic task concurrency control based on dependency declarations. 11 | - Support dependency logic expressions: `not`, `and`, `or`, `xor` and any combination of them. 12 | - Many-to-many result delivery between tasks. 13 | - Support tasks rollback in case of any error. 14 | - Support multiple errors collection. 15 | - Support coroutine pool: default(panjf2000/ants) or user-defined coroutine pool. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | go get github.com/piaodazhu/gotcc 21 | ``` 22 | 23 | ## Usage 24 | 25 | A simple usage: 26 | 27 | ```go 28 | import "github.com/piaodazhu/gotcc" 29 | 30 | // User-defined task function 31 | func ExampleFunc1(args map[string]interface{}) (interface{}, error) { 32 | fmt.Println(args["BIND"].(string)) 33 | return "DONE", nil 34 | } 35 | func ExampleFunc2(args map[string]interface{}) (interface{}, error) { 36 | return args["BIND"].(string), nil 37 | } 38 | 39 | // User-defined undo function 40 | func ExampleUndo(args map[string]interface{}) error { 41 | fmt.Println("Undo > ", args["BIND"].(string)) 42 | return nil 43 | } 44 | 45 | func main() { 46 | // 1. Create a new controller 47 | controller := gotcc.NewTCController() 48 | 49 | // 2. Add tasks to the controller 50 | // TaskA: bind arguments with ExampleFunc1 51 | taskA := controller.AddTask("taskA", ExampleFunc1, "BindArg-A") 52 | // TaskB: like TaskA, but set undoFunction 53 | taskB := controller.AddTask("taskB", ExampleFunc1, "BindArg-B").SetUndoFunc(ExampleUndo, true) 54 | // TaskC: bind arguments with ExampleFunc2 55 | taskC := controller.AddTask("taskC", ExampleFunc2, "BindArg-C") 56 | 57 | // TaskD: bind arguments with ExampleFunc2 58 | taskD := controller.AddTask("taskD", ExampleFunc2, "BindArg-D") 59 | 60 | // 3. Define dependencies 61 | // B depend on A 62 | taskB.SetDependency(taskB.NewDependencyExpr(taskA)) 63 | // C depend on A 64 | taskC.SetDependency(taskC.NewDependencyExpr(taskA)) 65 | // D depend on B and C 66 | taskD.SetDependency(gotcc.MakeAndExpr(taskD.NewDependencyExpr(taskB), taskD.NewDependencyExpr(taskC))) 67 | 68 | // 4. Define termination (Important) 69 | // set TaskD's finish as termination 70 | controller.SetTermination(controller.NewTerminationExpr(taskD)) 71 | 72 | // 5. Run the tasks 73 | result, err := controller.BatchRun() 74 | if err != nil { 75 | // get taskErrors: err.(ErrAborted).TaskErrors 76 | // get undoErrors: err.(ErrAborted).UndoErrors 77 | } 78 | 79 | // 6. Will Print "BindArg-D" 80 | fmt.Println(result["taskD"].(string)) 81 | } 82 | ``` 83 | Tasks will run concurrently, but taskB and taskC will not start until taskA completes, and taskD will not start until both taskB and taskC complete. But if taskD failed (return err!=nil), `ExampleUndo("BindArg-B")` will be executed. 84 | 85 | More detailed usage information can be found in test files, you can refer to `example_test.go` for a more complex dependency topology, `dependency_test.go` for the advanced usage of dependency logic expressions, and `tcc_test.go` for tasks rollback and error message collection. 86 | 87 | ## Specifications 88 | 89 | ### Execution 90 | In summary, a single execution of the TCController contains multiple tasks. There may be some dependencies between tasks, and the termination of the execution depends on the completion of some of these tasks. **Therefore, `controller.SetTermination` must be called before calling `controller.BatchRun` or `controller.PoolRun`.** 91 | 92 | There are 2 mode for the TCController to execute the tasks: `BatchRun` and `PoolRun`. `BatchRun` will create NumOf(tasks) goroutines at most. If we need to control the max number of running goroutines, `PoolRun` is recommand. A default coroutine pool is provided, based on [panjf2000/ants](https://github.com/panjf2000/ants). User-defined coroutine pool should implement this interface, where Go() is a task submission method and should **block** when workers are busy: 93 | ```go 94 | type GoroutinePool interface { 95 | Go(task func()) error 96 | } 97 | ``` 98 | 99 | > Node that `PoolRun` mode only avalible when all dependency expressions are `AND`. 100 | 101 | ### Task Function 102 | The task function must have this form: 103 | ```go 104 | func (args map[string]interface{}) (interface{}, error) 105 | ``` 106 | There are some built-in keys when running the task function: 107 | - `NAME`: the value is the name of this task. 108 | - `BIND`: the value is the third arguments when `controller.AddTask()` was called. 109 | - `CANCEL`: the value is a context.Context, with cancel. 110 | 111 | Other keys are the **names** of its dependent tasks, and the corresponding values are the return value of these tasks. 112 | 113 | **IMPORTANT**: Inside task functions, if the task is cancelled by receiving signal from `args["CANCEL"].(context.Context).done()`, it should return `gotcc.ErrCancelled` (with state if necessary). if the task failed but you don't want abort the execution, it should return `gotcc.ErrSilentFail`. 114 | 115 | ### Undo Function 116 | The undo function must have this form: 117 | ```go 118 | func (args map[string]interface{}) error 119 | ``` 120 | There are some built-in keys when running the undo function: 121 | - `NAME`: the value is the name of this task. 122 | - `BIND`: the value is the third arguments when `controller.AddTask()` was called. 123 | - `TASKERR`: the value type is `[]*gotcc.ErrorMessage`, recording the errors of tasks execution. 124 | - `UNDOERR`: the value type is `[]*gotcc.ErrorMessage`, recording the previous errors of undo execution. 125 | - `CANCELLED`: the value type is `[]*gotcc.StateMessage`, recording the state of canncelld task. (For example, what process in that task has been done before cancelled.) 126 | 127 | The undo functions will be run in the reverse order of the task function completion. And the second arguments of `SetUndoFunc` means whether to skip this error if the undo function errors out. 128 | 129 | The undo function will be executed when: 130 | 1. Some task return `err!=nil` when the controller execute the tasks. 131 | 2. The corresponding task has been completed. 132 | 3. The predecessor undo functions have been completed or skipped. 133 | 134 | When the undo function run, the arguments `args` is exactly the same as its corresponding task. 135 | 136 | ### Errors 137 | 138 | During the execution of TCController, multiple tasks may fail and after failure, multiple tasks may be cancelled. During rollback, multiple rollback functions may also encounter errors. Therefore, the error definitions in the return value of `Run` are as follows: 139 | ```go 140 | type ErrAborted struct { 141 | TaskErrors []*ErrorMessage 142 | UndoErrors []*ErrorMessage 143 | Cancelled []*StateMessage 144 | } 145 | 146 | type ErrorMessage struct { 147 | TaskName string 148 | Error error 149 | } 150 | 151 | type StateMessage struct { 152 | TaskName string 153 | State State 154 | } 155 | 156 | ``` 157 | 158 | ### Dependency Expression 159 | 160 | Supported dependency logic expressions are `not`, `and`, `or`, `xor` and any combination of them. 161 | 162 | For taskB, create a dependency expression about taskA: 163 | ```go 164 | ExprAB := taskB.NewDependencyExpr(taskA) 165 | ``` 166 | 167 | Combine existing dependency expressions to generate dependency expressions: 168 | ```go 169 | Expr3 := gotcc.MakeOrExpr(Expr1, Expr2) 170 | ``` 171 | 172 | Get the current dependency expression of taskA. 173 | ```go 174 | Expr := taskA.DependencyExpr() 175 | ``` 176 | 177 | Set the dependency expression for taskA. 178 | ```go 179 | taskA.SetDependencyExpr(Expr) 180 | ``` 181 | 182 | And termination setup has the same logic as above. 183 | 184 | ## Performance 185 | ```bash 186 | goos: linux 187 | goarch: amd64 188 | pkg: github.com/piaodazhu/gotcc 189 | cpu: 11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz 190 | BenchmarkBatchRunSerialized10-4 54928 19533 ns/op 7106 B/op 84 allocs/op 191 | BenchmarkBatchRunSerialized100-4 6452 172314 ns/op 68873 B/op 750 allocs/op 192 | BenchmarkBatchRunSerialized1000-4 507 2301349 ns/op 772775 B/op 7493 allocs/op 193 | BenchmarkBatchRunManyToOne10-4 59264 20095 ns/op 7673 B/op 77 allocs/op 194 | BenchmarkBatchRunManyToOne100-4 5623 201600 ns/op 78210 B/op 659 allocs/op 195 | BenchmarkBatchRunManyToOne1000-4 100 11388471 ns/op 899267 B/op 6178 allocs/op 196 | BenchmarkBatchRunManyToMany10-4 23252 50629 ns/op 21549 B/op 212 allocs/op 197 | BenchmarkBatchRunManyToMany100-4 410 2814498 ns/op 2270278 B/op 15945 allocs/op 198 | BenchmarkBatchRunBinaryTree10-4 34041 37472 ns/op 10857 B/op 116 allocs/op 199 | BenchmarkBatchRunBinaryTree100-4 6380 204777 ns/op 91623 B/op 880 allocs/op 200 | BenchmarkBatchRunBinaryTree1000-4 804 1506047 ns/op 749709 B/op 6848 allocs/op 201 | BenchmarkPoolRunSerialized10-4 49352 24033 ns/op 7622 B/op 91 allocs/op 202 | BenchmarkPoolRunSerialized100-4 5956 180609 ns/op 72491 B/op 754 allocs/op 203 | BenchmarkPoolRunSerialized1000-4 710 1617208 ns/op 783005 B/op 7222 allocs/op 204 | BenchmarkPoolRunManyToOne10-4 38798 31265 ns/op 8193 B/op 84 allocs/op 205 | BenchmarkPoolRunManyToOne100-4 5371 215742 ns/op 81924 B/op 664 allocs/op 206 | BenchmarkPoolRunManyToOne1000-4 100 11651428 ns/op 937995 B/op 6226 allocs/op 207 | BenchmarkPoolRunManyToMany10-4 22332 52499 ns/op 22833 B/op 218 allocs/op 208 | BenchmarkPoolRunManyToMany100-4 336 3698301 ns/op 2360297 B/op 15935 allocs/op 209 | BenchmarkPoolRunBinaryTree10-4 29043 42600 ns/op 11543 B/op 122 allocs/op 210 | BenchmarkPoolRunBinaryTree100-4 5572 216561 ns/op 96321 B/op 885 allocs/op 211 | BenchmarkPoolRunBinaryTree1000-4 826 1394863 ns/op 782612 B/op 6812 allocs/op 212 | ``` 213 | 214 | ## License 215 | `gotcc` is released under the MIT license. See [LICENSE](https://github.com/piaodazhu/gotcc/blob/master/LICENSE) for details. 216 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TaskForBench(args map[string]interface{}) (interface{}, error) { 12 | // time.Sleep(time.Microsecond) 13 | // i := 0 14 | // for i < 100000 { 15 | // i++ 16 | // } 17 | return nil, nil 18 | } 19 | 20 | func BenchmarkBatchRunSerialized10(b *testing.B) { 21 | controller := NewTCController() 22 | last := controller.AddTask("first", TaskForBench, 1) 23 | for i := 0; i < 9; i++ { 24 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 25 | task.SetDependency(task.NewDependencyExpr(last)) 26 | last = task 27 | } 28 | controller.SetTermination(controller.NewTerminationExpr(last)) 29 | 30 | b.ResetTimer() 31 | for i := 0; i < b.N; i++ { 32 | controller.BatchRun() 33 | } 34 | } 35 | func BenchmarkBatchRunSerialized100(b *testing.B) { 36 | controller := NewTCController() 37 | last := controller.AddTask("first", TaskForBench, 1) 38 | for i := 0; i < 99; i++ { 39 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 40 | task.SetDependency(task.NewDependencyExpr(last)) 41 | last = task 42 | } 43 | controller.SetTermination(controller.NewTerminationExpr(last)) 44 | 45 | b.ResetTimer() 46 | for i := 0; i < b.N; i++ { 47 | controller.BatchRun() 48 | } 49 | } 50 | func BenchmarkBatchRunSerialized1000(b *testing.B) { 51 | controller := NewTCController() 52 | last := controller.AddTask("first", TaskForBench, 1) 53 | for i := 0; i < 999; i++ { 54 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 55 | task.SetDependency(task.NewDependencyExpr(last)) 56 | last = task 57 | } 58 | controller.SetTermination(controller.NewTerminationExpr(last)) 59 | 60 | b.ResetTimer() 61 | for i := 0; i < b.N; i++ { 62 | controller.BatchRun() 63 | } 64 | } 65 | 66 | func BenchmarkBatchRunManyToOne10(b *testing.B) { 67 | controller := NewTCController() 68 | end := controller.AddTask("last", TaskForBench, 1) 69 | for i := 0; i < 9; i++ { 70 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 71 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 72 | } 73 | controller.SetTermination(controller.NewTerminationExpr(end)) 74 | 75 | b.ResetTimer() 76 | for i := 0; i < b.N; i++ { 77 | controller.BatchRun() 78 | } 79 | } 80 | 81 | func BenchmarkBatchRunManyToOne100(b *testing.B) { 82 | controller := NewTCController() 83 | end := controller.AddTask("last", TaskForBench, 1) 84 | for i := 0; i < 99; i++ { 85 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 86 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 87 | } 88 | controller.SetTermination(controller.NewTerminationExpr(end)) 89 | 90 | b.ResetTimer() 91 | for i := 0; i < b.N; i++ { 92 | controller.BatchRun() 93 | } 94 | } 95 | 96 | func BenchmarkBatchRunManyToOne1000(b *testing.B) { 97 | controller := NewTCController() 98 | end := controller.AddTask("last", TaskForBench, 1) 99 | for i := 0; i < 999; i++ { 100 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 101 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 102 | } 103 | controller.SetTermination(controller.NewTerminationExpr(end)) 104 | 105 | b.ResetTimer() 106 | for i := 0; i < b.N; i++ { 107 | controller.BatchRun() 108 | } 109 | } 110 | func BenchmarkBatchRunManyToMany10(b *testing.B) { 111 | controller := NewTCController() 112 | for j := 0; j < 5; j++ { 113 | l2node := controller.AddTask("l2node", TaskForBench, 1) 114 | for i := 0; i < 5; i++ { 115 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 116 | l2node.SetDependency(MakeAndExpr(l2node.DependencyExpr(), l2node.NewDependencyExpr(task))) 117 | } 118 | controller.SetTermination(MakeAndExpr(controller.TerminationExpr(), controller.NewTerminationExpr(l2node))) 119 | } 120 | 121 | b.ResetTimer() 122 | for i := 0; i < b.N; i++ { 123 | controller.BatchRun() 124 | } 125 | } 126 | 127 | func BenchmarkBatchRunManyToMany100(b *testing.B) { 128 | controller := NewTCController() 129 | for j := 0; j < 50; j++ { 130 | l2node := controller.AddTask("l2node", TaskForBench, 1) 131 | for i := 0; i < 50; i++ { 132 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 133 | l2node.SetDependency(MakeAndExpr(l2node.DependencyExpr(), l2node.NewDependencyExpr(task))) 134 | } 135 | controller.SetTermination(MakeAndExpr(controller.TerminationExpr(), controller.NewTerminationExpr(l2node))) 136 | } 137 | 138 | b.ResetTimer() 139 | for i := 0; i < b.N; i++ { 140 | controller.BatchRun() 141 | } 142 | } 143 | 144 | func BenchmarkBatchRunBinaryTree10(b *testing.B) { 145 | controller := NewTCController() 146 | root := controller.AddTask("root", TaskForBench, 1) 147 | controller.SetTermination(controller.NewTerminationExpr(root)) 148 | queue := []*Executor{root} 149 | for i := 0; i < 3; i++ { 150 | tmp := []*Executor{} 151 | for _, e := range queue { 152 | left := controller.AddTask("left", TaskForBench, 1) 153 | right := controller.AddTask("right", TaskForBench, 1) 154 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 155 | tmp = append(tmp, left) 156 | tmp = append(tmp, right) 157 | } 158 | queue = tmp 159 | } 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | controller.BatchRun() 164 | } 165 | } 166 | 167 | func BenchmarkBatchRunBinaryTree100(b *testing.B) { 168 | controller := NewTCController() 169 | root := controller.AddTask("root", TaskForBench, 1) 170 | controller.SetTermination(controller.NewTerminationExpr(root)) 171 | queue := []*Executor{root} 172 | for i := 0; i < 6; i++ { 173 | tmp := []*Executor{} 174 | for _, e := range queue { 175 | left := controller.AddTask("left", TaskForBench, 1) 176 | right := controller.AddTask("right", TaskForBench, 1) 177 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 178 | tmp = append(tmp, left) 179 | tmp = append(tmp, right) 180 | } 181 | queue = tmp 182 | } 183 | 184 | b.ResetTimer() 185 | for i := 0; i < b.N; i++ { 186 | controller.BatchRun() 187 | } 188 | } 189 | 190 | func BenchmarkBatchRunBinaryTree1000(b *testing.B) { 191 | controller := NewTCController() 192 | root := controller.AddTask("root", TaskForBench, 1) 193 | controller.SetTermination(controller.NewTerminationExpr(root)) 194 | queue := []*Executor{root} 195 | for i := 0; i < 9; i++ { 196 | tmp := []*Executor{} 197 | for _, e := range queue { 198 | left := controller.AddTask("left", TaskForBench, 1) 199 | right := controller.AddTask("right", TaskForBench, 1) 200 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 201 | tmp = append(tmp, left) 202 | tmp = append(tmp, right) 203 | } 204 | queue = tmp 205 | } 206 | 207 | b.ResetTimer() 208 | for i := 0; i < b.N; i++ { 209 | controller.BatchRun() 210 | } 211 | } 212 | 213 | func BenchmarkPoolRunSerialized10(b *testing.B) { 214 | controller := NewTCController() 215 | last := controller.AddTask("first", TaskForBench, 1) 216 | for i := 0; i < 9; i++ { 217 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 218 | task.SetDependency(task.NewDependencyExpr(last)) 219 | last = task 220 | } 221 | controller.SetTermination(controller.NewTerminationExpr(last)) 222 | 223 | pool := NewDefaultPool(9) 224 | b.ResetTimer() 225 | b.StartTimer() 226 | for i := 0; i < b.N; i++ { 227 | controller.PoolRun(pool) 228 | } 229 | b.StopTimer() 230 | pool.Close() 231 | } 232 | 233 | func BenchmarkPoolRunSerialized100(b *testing.B) { 234 | controller := NewTCController() 235 | last := controller.AddTask("first", TaskForBench, 1) 236 | for i := 0; i < 99; i++ { 237 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 238 | task.SetDependency(task.NewDependencyExpr(last)) 239 | last = task 240 | } 241 | controller.SetTermination(controller.NewTerminationExpr(last)) 242 | 243 | pool := NewDefaultPool(99) 244 | b.ResetTimer() 245 | b.StartTimer() 246 | for i := 0; i < b.N; i++ { 247 | controller.PoolRun(pool) 248 | } 249 | b.StopTimer() 250 | pool.Close() 251 | } 252 | 253 | func BenchmarkPoolRunSerialized1000(b *testing.B) { 254 | controller := NewTCController() 255 | last := controller.AddTask("first", TaskForBench, 1) 256 | for i := 0; i < 999; i++ { 257 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 258 | task.SetDependency(task.NewDependencyExpr(last)) 259 | last = task 260 | } 261 | controller.SetTermination(controller.NewTerminationExpr(last)) 262 | 263 | pool := NewDefaultPool(999) 264 | b.ResetTimer() 265 | b.StartTimer() 266 | for i := 0; i < b.N; i++ { 267 | controller.PoolRun(pool) 268 | } 269 | b.StopTimer() 270 | pool.Close() 271 | } 272 | 273 | func BenchmarkPoolRunManyToOne10(b *testing.B) { 274 | controller := NewTCController() 275 | end := controller.AddTask("last", TaskForBench, 1) 276 | for i := 0; i < 9; i++ { 277 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 278 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 279 | } 280 | controller.SetTermination(controller.NewTerminationExpr(end)) 281 | 282 | pool := NewDefaultPool(9) 283 | b.ResetTimer() 284 | b.StartTimer() 285 | for i := 0; i < b.N; i++ { 286 | controller.PoolRun(pool) 287 | } 288 | b.StopTimer() 289 | pool.Close() 290 | } 291 | 292 | func BenchmarkPoolRunManyToOne100(b *testing.B) { 293 | controller := NewTCController() 294 | end := controller.AddTask("last", TaskForBench, 1) 295 | for i := 0; i < 99; i++ { 296 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 297 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 298 | } 299 | controller.SetTermination(controller.NewTerminationExpr(end)) 300 | 301 | pool := NewDefaultPool(99) 302 | b.ResetTimer() 303 | b.StartTimer() 304 | for i := 0; i < b.N; i++ { 305 | controller.PoolRun(pool) 306 | } 307 | b.StopTimer() 308 | pool.Close() 309 | } 310 | 311 | func BenchmarkPoolRunManyToOne1000(b *testing.B) { 312 | controller := NewTCController() 313 | end := controller.AddTask("last", TaskForBench, 1) 314 | for i := 0; i < 999; i++ { 315 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 316 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 317 | } 318 | controller.SetTermination(controller.NewTerminationExpr(end)) 319 | 320 | pool := NewDefaultPool(999) 321 | b.ResetTimer() 322 | b.StartTimer() 323 | for i := 0; i < b.N; i++ { 324 | controller.PoolRun(pool) 325 | } 326 | b.StopTimer() 327 | pool.Close() 328 | } 329 | 330 | func BenchmarkPoolRunManyToMany10(b *testing.B) { 331 | controller := NewTCController() 332 | for j := 0; j < 5; j++ { 333 | l2node := controller.AddTask("l2node", TaskForBench, 1) 334 | for i := 0; i < 5; i++ { 335 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 336 | l2node.SetDependency(MakeAndExpr(l2node.DependencyExpr(), l2node.NewDependencyExpr(task))) 337 | } 338 | controller.SetTermination(MakeAndExpr(controller.TerminationExpr(), controller.NewTerminationExpr(l2node))) 339 | } 340 | 341 | pool := NewDefaultPool(5) 342 | b.ResetTimer() 343 | b.StartTimer() 344 | for i := 0; i < b.N; i++ { 345 | controller.PoolRun(pool) 346 | } 347 | b.StopTimer() 348 | pool.Close() 349 | } 350 | 351 | func BenchmarkPoolRunManyToMany100(b *testing.B) { 352 | controller := NewTCController() 353 | for j := 0; j < 50; j++ { 354 | l2node := controller.AddTask("l2node", TaskForBench, 1) 355 | for i := 0; i < 50; i++ { 356 | task := controller.AddTask(strconv.Itoa(i), TaskForBench, 1) 357 | l2node.SetDependency(MakeAndExpr(l2node.DependencyExpr(), l2node.NewDependencyExpr(task))) 358 | } 359 | controller.SetTermination(MakeAndExpr(controller.TerminationExpr(), controller.NewTerminationExpr(l2node))) 360 | } 361 | 362 | pool := NewDefaultPool(50) 363 | b.ResetTimer() 364 | b.StartTimer() 365 | for i := 0; i < b.N; i++ { 366 | controller.PoolRun(pool) 367 | } 368 | b.StopTimer() 369 | pool.Close() 370 | } 371 | 372 | func BenchmarkPoolRunBinaryTree10(b *testing.B) { 373 | controller := NewTCController() 374 | root := controller.AddTask("root", TaskForBench, 1) 375 | controller.SetTermination(controller.NewTerminationExpr(root)) 376 | queue := []*Executor{root} 377 | for i := 0; i < 3; i++ { 378 | tmp := []*Executor{} 379 | for _, e := range queue { 380 | left := controller.AddTask("left", TaskForBench, 1) 381 | right := controller.AddTask("right", TaskForBench, 1) 382 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 383 | tmp = append(tmp, left) 384 | tmp = append(tmp, right) 385 | } 386 | queue = tmp 387 | } 388 | 389 | pool := NewDefaultPool(8) 390 | b.ResetTimer() 391 | b.StartTimer() 392 | for i := 0; i < b.N; i++ { 393 | controller.PoolRun(pool) 394 | } 395 | b.StopTimer() 396 | pool.Close() 397 | } 398 | 399 | func BenchmarkPoolRunBinaryTree100(b *testing.B) { 400 | controller := NewTCController() 401 | root := controller.AddTask("root", TaskForBench, 1) 402 | controller.SetTermination(controller.NewTerminationExpr(root)) 403 | queue := []*Executor{root} 404 | for i := 0; i < 6; i++ { 405 | tmp := []*Executor{} 406 | for _, e := range queue { 407 | left := controller.AddTask("left", TaskForBench, 1) 408 | right := controller.AddTask("right", TaskForBench, 1) 409 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 410 | tmp = append(tmp, left) 411 | tmp = append(tmp, right) 412 | } 413 | queue = tmp 414 | } 415 | 416 | pool := NewDefaultPool(64) 417 | b.ResetTimer() 418 | b.StartTimer() 419 | for i := 0; i < b.N; i++ { 420 | controller.PoolRun(pool) 421 | } 422 | b.StopTimer() 423 | pool.Close() 424 | } 425 | 426 | func BenchmarkPoolRunBinaryTree1000(b *testing.B) { 427 | controller := NewTCController() 428 | root := controller.AddTask("root", TaskForBench, 1) 429 | controller.SetTermination(controller.NewTerminationExpr(root)) 430 | queue := []*Executor{root} 431 | for i := 0; i < 9; i++ { 432 | tmp := []*Executor{} 433 | for _, e := range queue { 434 | left := controller.AddTask("left", TaskForBench, 1) 435 | right := controller.AddTask("right", TaskForBench, 1) 436 | e.SetDependency(MakeAndExpr(e.NewDependencyExpr(left), e.NewDependencyExpr(right))) 437 | tmp = append(tmp, left) 438 | tmp = append(tmp, right) 439 | } 440 | queue = tmp 441 | } 442 | 443 | pool := NewDefaultPool(512) 444 | b.ResetTimer() 445 | b.StartTimer() 446 | for i := 0; i < b.N; i++ { 447 | controller.PoolRun(pool) 448 | } 449 | b.StopTimer() 450 | pool.Close() 451 | } 452 | 453 | // From tasktree.go by @lesismal :) 454 | type Node struct { 455 | F func() error 456 | Nodes []*Node 457 | 458 | selfCnt int32 459 | parentCnt *int32 460 | parentWg *sync.WaitGroup 461 | parentFunc func() error 462 | rollback func(error) 463 | } 464 | 465 | func (node *Node) doNodes(parentWg *sync.WaitGroup, parentCnt *int32, parentFunc func() error, rollback func(error)) { 466 | node.selfCnt = int32(len(node.Nodes)) 467 | node.parentWg = parentWg 468 | node.parentCnt = parentCnt 469 | node.parentFunc = parentFunc 470 | node.rollback = rollback 471 | if node.selfCnt > 0 { 472 | for _, v := range node.Nodes { 473 | v.doNodes(nil, &node.selfCnt, node.doSelf, rollback) 474 | } 475 | return 476 | } 477 | go node.doSelf() 478 | } 479 | 480 | func (node *Node) doSelf() error { 481 | defer func() { 482 | if node.parentWg != nil { 483 | node.parentWg.Done() 484 | } else if atomic.AddInt32(node.parentCnt, -1) == 0 && node.parentFunc != nil { 485 | if err := node.parentFunc(); err != nil && node.rollback != nil { 486 | node.rollback(err) 487 | } 488 | } 489 | }() 490 | if err := node.F(); err != nil && node.rollback != nil { 491 | node.rollback(err) 492 | } 493 | return nil 494 | } 495 | 496 | type TaskTree struct{} 497 | 498 | func (t *TaskTree) Go(root *Node, rollback func(error)) func() <-chan error { 499 | wg := &sync.WaitGroup{} 500 | chErr := make(chan error, 1) 501 | wg.Add(1) 502 | waitFunc := func() <-chan error { 503 | wg.Wait() 504 | if len(chErr) == 0 { 505 | chErr <- nil 506 | } 507 | return chErr 508 | } 509 | var n int32 510 | var rollbackCalled int32 511 | root.doNodes(wg, &n, nil, func(err error) { 512 | if atomic.AddInt32(&rollbackCalled, 1) == 1 { 513 | rollback(err) 514 | chErr <- err 515 | } 516 | }) 517 | return waitFunc 518 | } 519 | 520 | func (t *TaskTree) GoN(nodes []*Node, rollback func(error)) func() <-chan error { 521 | wg := &sync.WaitGroup{} 522 | chErr := make(chan error, 1) 523 | waitFunc := func() <-chan error { 524 | wg.Wait() 525 | if len(chErr) == 0 { 526 | chErr <- nil 527 | } 528 | return chErr 529 | } 530 | if len(nodes) == 0 { 531 | return waitFunc 532 | } 533 | wg.Add(len(nodes)) 534 | var n int32 535 | var rollbackCalled int32 536 | for _, v := range nodes { 537 | v.doNodes(wg, &n, nil, func(err error) { 538 | if atomic.AddInt32(&rollbackCalled, 1) == 1 { 539 | rollback(err) 540 | chErr <- err 541 | } 542 | }) 543 | } 544 | return waitFunc 545 | } 546 | 547 | func TaskForComparison() error { 548 | time.Sleep(time.Microsecond) 549 | i := 0 550 | for i < 100000 { 551 | i++ 552 | } 553 | return nil 554 | } 555 | 556 | func BenchmarkTaskTreeSerialized10(b *testing.B) { 557 | t := &TaskTree{} 558 | root := &Node{F: TaskForComparison} 559 | last := root 560 | for i := 0; i < 9; i++ { 561 | node := &Node{F: TaskForComparison} 562 | last.Nodes = append(last.Nodes, node) 563 | last = node 564 | } 565 | 566 | b.ResetTimer() 567 | for i := 0; i < b.N; i++ { 568 | chWait := t.Go(root, nil) 569 | <-chWait() 570 | } 571 | } 572 | func BenchmarkTaskTreeSerialized100(b *testing.B) { 573 | t := &TaskTree{} 574 | root := &Node{F: TaskForComparison} 575 | last := root 576 | for i := 0; i < 99; i++ { 577 | node := &Node{F: TaskForComparison} 578 | last.Nodes = append(last.Nodes, node) 579 | last = node 580 | } 581 | 582 | b.ResetTimer() 583 | for i := 0; i < b.N; i++ { 584 | chWait := t.Go(root, nil) 585 | <-chWait() 586 | } 587 | } 588 | func BenchmarkTaskTreeSerialized1000(b *testing.B) { 589 | t := &TaskTree{} 590 | root := &Node{F: TaskForComparison} 591 | last := root 592 | for i := 0; i < 999; i++ { 593 | node := &Node{F: TaskForComparison} 594 | last.Nodes = append(last.Nodes, node) 595 | last = node 596 | } 597 | 598 | b.ResetTimer() 599 | for i := 0; i < b.N; i++ { 600 | chWait := t.Go(root, nil) 601 | <-chWait() 602 | } 603 | } 604 | 605 | func BenchmarkTaskTreeManyToOne10(b *testing.B) { 606 | t := &TaskTree{} 607 | root := &Node{F: TaskForComparison} 608 | for i := 0; i < 9; i++ { 609 | node := &Node{F: TaskForComparison} 610 | root.Nodes = append(root.Nodes, node) 611 | } 612 | 613 | b.ResetTimer() 614 | for i := 0; i < b.N; i++ { 615 | chWait := t.Go(root, nil) 616 | <-chWait() 617 | } 618 | } 619 | 620 | func BenchmarkTaskTreeManyToOne100(b *testing.B) { 621 | t := &TaskTree{} 622 | root := &Node{F: TaskForComparison} 623 | for i := 0; i < 99; i++ { 624 | node := &Node{F: TaskForComparison} 625 | root.Nodes = append(root.Nodes, node) 626 | } 627 | 628 | b.ResetTimer() 629 | for i := 0; i < b.N; i++ { 630 | chWait := t.Go(root, nil) 631 | <-chWait() 632 | } 633 | } 634 | 635 | func BenchmarkTaskTreeManyToOne1000(b *testing.B) { 636 | t := &TaskTree{} 637 | root := &Node{F: TaskForComparison} 638 | for i := 0; i < 999; i++ { 639 | node := &Node{F: TaskForComparison} 640 | root.Nodes = append(root.Nodes, node) 641 | } 642 | 643 | b.ResetTimer() 644 | for i := 0; i < b.N; i++ { 645 | chWait := t.Go(root, nil) 646 | <-chWait() 647 | } 648 | } 649 | 650 | func BenchmarkTaskTreeBinaryTree10(b *testing.B) { 651 | t := &TaskTree{} 652 | root := &Node{F: TaskForComparison} 653 | queue := []*Node{root} 654 | for i := 0; i < 3; i++ { 655 | tmp := []*Node{} 656 | for _, node := range queue { 657 | left := &Node{F: TaskForComparison} 658 | right := &Node{F: TaskForComparison} 659 | node.Nodes = append(node.Nodes, left) 660 | node.Nodes = append(node.Nodes, right) 661 | tmp = append(tmp, left) 662 | tmp = append(tmp, right) 663 | } 664 | queue = tmp 665 | } 666 | 667 | b.ResetTimer() 668 | for i := 0; i < b.N; i++ { 669 | chWait := t.Go(root, nil) 670 | <-chWait() 671 | } 672 | } 673 | 674 | func BenchmarkTaskTreeBinaryTree100(b *testing.B) { 675 | t := &TaskTree{} 676 | root := &Node{F: TaskForComparison} 677 | queue := []*Node{root} 678 | for i := 0; i < 6; i++ { 679 | tmp := []*Node{} 680 | for _, node := range queue { 681 | left := &Node{F: TaskForComparison} 682 | right := &Node{F: TaskForComparison} 683 | node.Nodes = append(node.Nodes, left) 684 | node.Nodes = append(node.Nodes, right) 685 | tmp = append(tmp, left) 686 | tmp = append(tmp, right) 687 | } 688 | queue = tmp 689 | } 690 | 691 | b.ResetTimer() 692 | for i := 0; i < b.N; i++ { 693 | chWait := t.Go(root, nil) 694 | <-chWait() 695 | } 696 | } 697 | 698 | func BenchmarkTaskTreeBinaryTree1000(b *testing.B) { 699 | t := &TaskTree{} 700 | root := &Node{F: TaskForComparison} 701 | queue := []*Node{root} 702 | for i := 0; i < 9; i++ { 703 | tmp := []*Node{} 704 | for _, node := range queue { 705 | left := &Node{F: TaskForComparison} 706 | right := &Node{F: TaskForComparison} 707 | node.Nodes = append(node.Nodes, left) 708 | node.Nodes = append(node.Nodes, right) 709 | tmp = append(tmp, left) 710 | tmp = append(tmp, right) 711 | } 712 | queue = tmp 713 | } 714 | 715 | b.ResetTimer() 716 | for i := 0; i < b.N; i++ { 717 | chWait := t.Go(root, nil) 718 | <-chWait() 719 | } 720 | } 721 | -------------------------------------------------------------------------------- /tcc_test.go: -------------------------------------------------------------------------------- 1 | package gotcc 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/panjf2000/ants/v2" 11 | ) 12 | 13 | func TaskDefault(args map[string]interface{}) (interface{}, error) { 14 | sum := 0 15 | for _, v := range args { 16 | if num, ok := v.(int); ok { 17 | sum += num 18 | } 19 | } 20 | return sum, nil 21 | } 22 | 23 | func TestNoTermination(t *testing.T) { 24 | controller := NewTCController() 25 | A := controller.AddTask("A", TaskDefault, 1) 26 | B := controller.AddTask("B", TaskDefault, 2) 27 | C := controller.AddTask("C", TaskDefault, 3) 28 | D := controller.AddTask("D", TaskDefault, 4) 29 | E := controller.AddTask("E", TaskDefault, 5) 30 | F := controller.AddTask("F", TaskDefault, 6) 31 | 32 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) // 3 + 1 + 2 = 6 33 | D.SetDependency(D.NewDependencyExpr(C)) // 4 + 6 = 10 34 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) // 5 + 2 + 6 = 13 35 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) // 6 + 10 + 13 = 29 36 | 37 | // controller.SetTermination(controller.NewTerminationExpr(F)) 38 | 39 | _, err := controller.BatchRun() 40 | if _, ok := err.(ErrNoTermination); !ok { 41 | t.Fatal(err) 42 | } 43 | t.Log(err.Error()) 44 | 45 | pool := NewDefaultPool(20) 46 | defer pool.Close() 47 | _, err = controller.PoolRun(pool) 48 | if _, ok := err.(ErrNoTermination); !ok { 49 | t.Fatal(err) 50 | } 51 | t.Log(err.Error()) 52 | } 53 | 54 | func TestLoopDependencyCond1(t *testing.T) { 55 | controller := NewTCController() 56 | A := controller.AddTask("A", TaskDefault, 1) 57 | B := controller.AddTask("B", TaskDefault, 2) 58 | C := controller.AddTask("C", TaskDefault, 3) 59 | 60 | // C <- A and B A <- C 61 | 62 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) 63 | 64 | // make loop dependency 65 | A.SetDependency(A.NewDependencyExpr(C)) 66 | 67 | controller.SetTermination(controller.NewTerminationExpr(C)) 68 | 69 | _, err := controller.BatchRun() 70 | if _, ok := err.(ErrLoopDependency); !ok { 71 | t.Fatal(err) 72 | } 73 | t.Log(err.Error()) 74 | 75 | pool := NewDefaultPool(2) 76 | defer pool.Close() 77 | _, err = controller.PoolRun(pool) 78 | if _, ok := err.(ErrLoopDependency); !ok { 79 | t.Fatal(err) 80 | } 81 | t.Log(err.Error()) 82 | } 83 | 84 | func TestLoopDependencyCond2(t *testing.T) { 85 | controller := NewTCController() 86 | A := controller.AddTask("A", TaskDefault, 1) 87 | B := controller.AddTask("B", TaskDefault, 2) 88 | C := controller.AddTask("C", TaskDefault, 3) 89 | 90 | // C <- A A <- B B <- C 91 | 92 | // make loop dependency 93 | C.SetDependency(C.NewDependencyExpr(A)) 94 | A.SetDependency(A.NewDependencyExpr(B)) 95 | B.SetDependency(B.NewDependencyExpr(C)) 96 | 97 | controller.SetTermination(controller.NewTerminationExpr(C)) 98 | 99 | _, err := controller.BatchRun() 100 | if _, ok := err.(ErrLoopDependency); !ok { 101 | t.Fatal(err) 102 | } 103 | t.Log(err.Error()) 104 | } 105 | 106 | func TestLoopDependencyCond3(t *testing.T) { 107 | controller := NewTCController() 108 | A := controller.AddTask("A", TaskDefault, 1) 109 | B := controller.AddTask("B", TaskDefault, 2) 110 | C := controller.AddTask("C", TaskDefault, 3) 111 | D := controller.AddTask("D", TaskDefault, 4) 112 | 113 | // B <- A D <- A C <- A and B and D 114 | 115 | C.SetDependency(MakeAndExpr(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B)), C.NewDependencyExpr(D))) 116 | 117 | B.SetDependency(B.NewDependencyExpr(A)) 118 | D.SetDependency(D.NewDependencyExpr(A)) 119 | 120 | controller.SetTermination(controller.NewTerminationExpr(C)) 121 | 122 | res, err := controller.BatchRun() 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | if sum, ok := res["C"]; !ok { 127 | t.Fatal(err) 128 | } else if sum != 12 { 129 | t.Fatal("Sum Error", sum) 130 | } 131 | } 132 | 133 | func TestLoopDependencyCond4(t *testing.T) { 134 | controller := NewTCController() 135 | A := controller.AddTask("A", TaskDefault, 1) 136 | B := controller.AddTask("B", TaskDefault, 2) 137 | C := controller.AddTask("C", TaskDefault, 3) 138 | D := controller.AddTask("D", TaskDefault, 4) 139 | 140 | // B <- A D <- A C <- B and D A <- C 141 | 142 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(B), C.NewDependencyExpr(D))) 143 | B.SetDependency(B.NewDependencyExpr(A)) 144 | D.SetDependency(D.NewDependencyExpr(A)) 145 | 146 | // make loop dependency 147 | A.SetDependency(A.NewDependencyExpr(C)) 148 | 149 | controller.SetTermination(controller.NewTerminationExpr(C)) 150 | 151 | _, err := controller.BatchRun() 152 | if _, ok := err.(ErrLoopDependency); !ok { 153 | t.Fatal(err) 154 | } 155 | t.Log(err.Error()) 156 | } 157 | 158 | func TestPoolUnsupport(t *testing.T) { 159 | controller := NewTCController() 160 | A := controller.AddTask("A", TaskDefault, 1) 161 | B := controller.AddTask("B", TaskDefault, 2) 162 | C := controller.AddTask("C", TaskDefault, 3) 163 | D := controller.AddTask("D", TaskDefault, 4) 164 | E := controller.AddTask("E", TaskDefault, 5) 165 | F := controller.AddTask("F", TaskDefault, 6) 166 | 167 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) 168 | D.SetDependency(D.NewDependencyExpr(C)) 169 | // make an 'OR' expr 170 | E.SetDependency(MakeOrExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) 171 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) 172 | 173 | controller.SetTermination(controller.NewTerminationExpr(F)) 174 | 175 | pool := NewDefaultPool(2) 176 | defer pool.Close() 177 | _, err := controller.PoolRun(pool) 178 | if _, ok := err.(ErrPoolUnsupport); !ok { 179 | t.Fatal(err) 180 | } 181 | t.Log(err.Error()) 182 | } 183 | 184 | func TestBatchRun(t *testing.T) { 185 | controller := NewTCController() 186 | A := controller.AddTask("A", TaskDefault, 1) 187 | B := controller.AddTask("B", TaskDefault, 2) 188 | C := controller.AddTask("C", TaskDefault, 3) 189 | D := controller.AddTask("D", TaskDefault, 4) 190 | E := controller.AddTask("E", TaskDefault, 5) 191 | F := controller.AddTask("F", TaskDefault, 6) 192 | 193 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) // 3 + 1 + 2 = 6 194 | D.SetDependency(D.NewDependencyExpr(C)) // 4 + 6 = 10 195 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) // 5 + 2 + 6 = 13 196 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) // 6 + 10 + 13 = 29 197 | 198 | controller.SetTermination(controller.NewTerminationExpr(F)) 199 | 200 | res, err := controller.BatchRun() 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | if sum, ok := res["F"]; !ok { 205 | t.Fatal(err) 206 | } else if sum != 29 { 207 | t.Fatal("Sum Error", sum) 208 | } 209 | } 210 | 211 | func TestPoolRun(t *testing.T) { 212 | controller := NewTCController() 213 | A := controller.AddTask("A", TaskDefault, 1) 214 | B := controller.AddTask("B", TaskDefault, 2) 215 | C := controller.AddTask("C", TaskDefault, 3) 216 | D := controller.AddTask("D", TaskDefault, 4) 217 | E := controller.AddTask("E", TaskDefault, 5) 218 | F := controller.AddTask("F", TaskDefault, 6) 219 | 220 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) // 3 + 1 + 2 = 6 221 | D.SetDependency(D.NewDependencyExpr(C)) // 4 + 6 = 10 222 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) // 5 + 2 + 6 = 13 223 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) // 6 + 10 + 13 = 29 224 | 225 | controller.SetTermination(controller.NewTerminationExpr(F)) 226 | 227 | // auto-sized pool 228 | pool := NewDefaultPool(0) 229 | res, err := controller.PoolRun(pool) 230 | if err != nil { 231 | t.Fatal(err) 232 | } 233 | if sum, ok := res["F"]; !ok { 234 | t.Fatal(err) 235 | } else if sum != 29 { 236 | t.Fatal("Sum Error", sum) 237 | } 238 | pool.Close() 239 | 240 | // size=2 pool 241 | pool = NewDefaultPool(2) 242 | res, err = controller.PoolRun(pool) 243 | if err != nil { 244 | t.Fatal(err) 245 | } 246 | if sum, ok := res["F"]; !ok { 247 | t.Fatal(err) 248 | } else if sum != 29 { 249 | t.Fatal("Sum Error", sum) 250 | } 251 | pool.Close() 252 | 253 | // just goroutine 254 | res, err = controller.PoolRun(DefaultNoPool{}) 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | if sum, ok := res["F"]; !ok { 259 | t.Fatal(err) 260 | } else if sum != 29 { 261 | t.Fatal("Sum Error", sum) 262 | } 263 | pool.Close() 264 | } 265 | 266 | func TestPoolRunError(t *testing.T) { 267 | controller := NewTCController() 268 | A := controller.AddTask("A", TaskDefault, 1) 269 | B := controller.AddTask("B", TaskDefault, 2) 270 | C := controller.AddTask("C", TaskDefault, 3) 271 | D := controller.AddTask("D", TaskDefault, 4) 272 | E := controller.AddTask("E", TaskDefault, 5) 273 | F := controller.AddTask("F", TaskDefault, 6) 274 | 275 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) // 3 + 1 + 2 = 6 276 | D.SetDependency(D.NewDependencyExpr(C)) // 4 + 6 = 10 277 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) // 5 + 2 + 6 = 13 278 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) // 6 + 10 + 13 = 29 279 | 280 | controller.SetTermination(controller.NewTerminationExpr(F)) 281 | 282 | // should error! 283 | pool, err := ants.NewPool(2, ants.WithNonblocking(true)) 284 | if err != nil { 285 | panic(err) 286 | } 287 | defer pool.Release() 288 | 289 | _, err = controller.PoolRun(DefaultPool{pool}) 290 | if err == nil { 291 | t.Fatal("should error") 292 | } 293 | t.Log(err.Error()) 294 | } 295 | 296 | func TestRunMultiple(t *testing.T) { 297 | controller := NewTCController() 298 | A := controller.AddTask("A", TaskDefault, 1) 299 | B := controller.AddTask("B", TaskDefault, 2) 300 | C := controller.AddTask("C", TaskDefault, 3) 301 | D := controller.AddTask("D", TaskDefault, 4) 302 | E := controller.AddTask("E", TaskDefault, 5) 303 | F := controller.AddTask("F", TaskDefault, 6) 304 | 305 | C.SetDependency(MakeAndExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) // 3 + 1 + 2 = 6 306 | D.SetDependency(D.NewDependencyExpr(C)) // 4 + 6 = 10 307 | E.SetDependency(MakeAndExpr(E.NewDependencyExpr(B), E.NewDependencyExpr(C))) // 5 + 2 + 6 = 13 308 | F.SetDependency(MakeAndExpr(F.NewDependencyExpr(D), F.NewDependencyExpr(E))) // 6 + 10 + 13 = 29 309 | 310 | controller.SetTermination(controller.NewTerminationExpr(F)) 311 | 312 | innerState := controller.String() 313 | for i := 0; i < 4; i++ { 314 | res, err := controller.BatchRun() 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | if sum, ok := res["F"]; !ok { 319 | t.Fatal(err) 320 | } else if sum != 29 { 321 | t.Fatal("Sum Error", sum) 322 | } 323 | if controller.String() != innerState { 324 | t.Fatal("Reset Error", controller.String(), innerState) 325 | } 326 | } 327 | 328 | pool := NewDefaultPool(2) 329 | defer pool.Close() 330 | 331 | for i := 0; i < 4; i++ { 332 | res, err := controller.PoolRun(pool) 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | if sum, ok := res["F"]; !ok { 337 | t.Fatal(err) 338 | } else if sum != 29 { 339 | t.Fatal("Sum Error", sum) 340 | } 341 | if controller.String() != innerState { 342 | t.Fatal("Reset Error", controller.String(), innerState) 343 | } 344 | } 345 | } 346 | 347 | func TestRunOneByOne(t *testing.T) { 348 | controller := NewTCController() 349 | last := controller.AddTask("first", TaskDefault, 1) 350 | for i := 0; i < 999; i++ { 351 | task := controller.AddTask(strconv.Itoa(i), TaskDefault, 1) 352 | task.SetDependency(task.NewDependencyExpr(last)) 353 | last = task 354 | } 355 | controller.SetTermination(controller.NewTerminationExpr(last)) 356 | res, err := controller.BatchRun() 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | if sum, ok := res[last.Name()]; !ok { 361 | t.Fatal(err) 362 | } else if sum != 1000 { 363 | t.Fatal("Sum Error", sum) 364 | } 365 | } 366 | 367 | func TestRunAll(t *testing.T) { 368 | controller := NewTCController() 369 | end := controller.AddTask("last", TaskDefault, 1) 370 | for i := 0; i < 999; i++ { 371 | task := controller.AddTask(strconv.Itoa(i), TaskDefault, 1) 372 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 373 | } 374 | controller.SetTermination(controller.NewTerminationExpr(end)) 375 | res, err := controller.BatchRun() 376 | if err != nil { 377 | t.Fatal(err) 378 | } 379 | if sum, ok := res[end.Name()]; !ok { 380 | t.Fatal(err) 381 | } else if sum != 1000 { 382 | t.Fatal("Sum Error", sum) 383 | } 384 | } 385 | 386 | type ErrTaskFailed struct{ id int } 387 | 388 | func (e ErrTaskFailed) Error() string { return "ErrTaskFailed: " + strconv.Itoa(e.id) } 389 | 390 | type ErrTaskCanceled struct{ id int } 391 | 392 | func (e ErrTaskCanceled) Error() string { return "ErrTaskCanceled: " + strconv.Itoa(e.id) } 393 | 394 | type ErrUndoFailed struct{ id int } 395 | 396 | func (e ErrUndoFailed) Error() string { return "ErrUndoFailed: " + strconv.Itoa(e.id) } 397 | 398 | type argsForTest struct { 399 | id int 400 | taskShouldFailed bool 401 | undoShouldFailed bool 402 | infoShouldPrint bool 403 | sleepTime int 404 | } 405 | 406 | func TaskMayFailed(args map[string]interface{}) (interface{}, error) { 407 | select { 408 | case <-time.After(time.Duration(args["BIND"].(argsForTest).sleepTime) * time.Millisecond * 3): 409 | if args["BIND"].(argsForTest).taskShouldFailed { 410 | return nil, ErrTaskFailed{args["BIND"].(argsForTest).id} 411 | } else { 412 | return "DONE", nil 413 | } 414 | case <-args["CANCEL"].(context.Context).Done(): 415 | return nil, ErrTaskCanceled{args["BIND"].(argsForTest).id} 416 | } 417 | } 418 | 419 | func UndoMayFailed(args map[string]interface{}) error { 420 | if args["BIND"].(argsForTest).undoShouldFailed == false { 421 | return nil 422 | } else { 423 | return ErrUndoFailed{args["BIND"].(argsForTest).id} 424 | } 425 | } 426 | 427 | func TestCollectTaskErrors(t *testing.T) { 428 | controller := NewTCController() 429 | endarg := argsForTest{ 430 | id: 100, 431 | taskShouldFailed: false, 432 | sleepTime: 100, 433 | } 434 | end := controller.AddTask("mid", TaskMayFailed, endarg) 435 | 436 | for i := 0; i < 99; i++ { 437 | arg := argsForTest{ 438 | id: i, 439 | taskShouldFailed: i == 96, // task 96 will fail 440 | undoShouldFailed: i == 5, 441 | sleepTime: i, 442 | } 443 | task := controller.AddTask(strconv.Itoa(i), TaskMayFailed, arg) 444 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 445 | } 446 | controller.SetTermination(controller.NewTerminationExpr(end)) 447 | _, err := controller.BatchRun() 448 | if err == nil { 449 | t.Fatal("Task not fail!") 450 | } else { 451 | t.Log(err) 452 | } 453 | } 454 | 455 | func TestCollectRollbackErrors(t *testing.T) { 456 | controller := NewTCController() 457 | endarg := argsForTest{ 458 | id: 100, 459 | taskShouldFailed: false, 460 | sleepTime: 100, 461 | } 462 | end := controller.AddTask("mid", TaskMayFailed, endarg).SetUndoFunc(UndoMayFailed, false) 463 | 464 | for i := 0; i < 99; i++ { 465 | arg := argsForTest{ 466 | id: i, 467 | taskShouldFailed: i == 96, // task 96 will fail 468 | undoShouldFailed: i == 5, 469 | sleepTime: i, 470 | } 471 | task := controller.AddTask(strconv.Itoa(i), TaskMayFailed, arg).SetUndoFunc(UndoMayFailed, false) 472 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 473 | } 474 | controller.SetTermination(controller.NewTerminationExpr(end)) 475 | _, err := controller.BatchRun() 476 | if err == nil { 477 | t.Fatal("Task not fail!") 478 | } else { 479 | t.Log(err) 480 | } 481 | 482 | pool := NewDefaultPool(20) 483 | defer pool.Close() 484 | _, err = controller.PoolRun(pool) 485 | if err == nil { 486 | t.Fatal("Task not fail!") 487 | } else { 488 | t.Log(err) 489 | } 490 | } 491 | 492 | func TestCollectRollbackErrorsNoSkip(t *testing.T) { 493 | controller := NewTCController() 494 | endarg := argsForTest{ 495 | id: 100, 496 | taskShouldFailed: false, 497 | sleepTime: 100, 498 | } 499 | end := controller.AddTask("mid", TaskMayFailed, endarg).SetUndoFunc(UndoMayFailed, true) 500 | 501 | for i := 0; i < 99; i++ { 502 | arg := argsForTest{ 503 | id: i, 504 | taskShouldFailed: i == 96, // task 96 will fail 505 | undoShouldFailed: i == 5 || i == 2 || i == 1, // undo 1 2 5 will fail 506 | sleepTime: i, 507 | } 508 | task := controller.AddTask(strconv.Itoa(i), TaskMayFailed, arg).SetUndoFunc(UndoMayFailed, true) 509 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 510 | } 511 | controller.SetTermination(controller.NewTerminationExpr(end)) 512 | _, err := controller.BatchRun() 513 | if err == nil { 514 | t.Fatal("Task not fail!") 515 | } else { 516 | t.Log(err) 517 | } 518 | 519 | pool := NewDefaultPool(20) 520 | defer pool.Close() 521 | _, err = controller.PoolRun(pool) 522 | if err == nil { 523 | t.Fatal("Task not fail!") 524 | } else { 525 | t.Log(err) 526 | } 527 | } 528 | 529 | type TaskState struct { 530 | TS string 531 | } 532 | 533 | func (t TaskState) String() string { 534 | return t.TS 535 | } 536 | 537 | func TaskMayFailedOrCancelled(args map[string]interface{}) (interface{}, error) { 538 | select { 539 | case <-time.After(time.Duration(args["BIND"].(argsForTest).sleepTime) * time.Millisecond * 3): 540 | if args["BIND"].(argsForTest).taskShouldFailed { 541 | return nil, ErrTaskFailed{args["BIND"].(argsForTest).id} 542 | } else { 543 | return "DONE", nil 544 | } 545 | case <-args["CANCEL"].(context.Context).Done(): 546 | err := ErrCancelled{} 547 | return nil, ErrCancelled{TaskState{"ID-" + strconv.Itoa(args["BIND"].(argsForTest).id) + err.Error()}} 548 | } 549 | } 550 | 551 | type ErrUndoFailedWithInfo struct{ info string } 552 | 553 | func (e ErrUndoFailedWithInfo) Error() string { return e.info } 554 | 555 | func UndoMayFailedWithInfo(args map[string]interface{}) error { 556 | if args["BIND"].(argsForTest).undoShouldFailed == false { 557 | return nil 558 | } else { 559 | if args["BIND"].(argsForTest).infoShouldPrint == false { 560 | return ErrUndoFailed{args["BIND"].(argsForTest).id} 561 | } 562 | var info strings.Builder 563 | info.WriteString("\n=====INFO======\n") 564 | info.WriteString("Task errors: \n") 565 | ems := args["TASKERR"].([]*ErrorMessage) 566 | for _, em := range ems { 567 | info.WriteString(em.TaskName) 568 | info.WriteByte(':') 569 | info.WriteString(em.Error.Error()) 570 | info.WriteByte('\n') 571 | } 572 | info.WriteString("Undo errors: \n") 573 | ums := args["UNDOERR"].([]*ErrorMessage) 574 | for _, um := range ums { 575 | info.WriteString(um.TaskName) 576 | info.WriteByte(':') 577 | info.WriteString(um.Error.Error()) 578 | info.WriteByte('\n') 579 | } 580 | info.WriteString("Cancelled: \n") 581 | sms := args["CANCELLED"].([]*StateMessage) 582 | for _, sm := range sms { 583 | info.WriteString(sm.TaskName) 584 | info.WriteByte(':') 585 | info.WriteString(sm.State.String()) 586 | info.WriteByte('\n') 587 | } 588 | return ErrUndoFailedWithInfo{info.String()} 589 | } 590 | } 591 | 592 | func TestCollectAllErrorsNoSkip(t *testing.T) { 593 | controller := NewTCController() 594 | endarg := argsForTest{ 595 | id: 100, 596 | taskShouldFailed: false, 597 | sleepTime: 100, 598 | } 599 | end := controller.AddTask("mid", TaskMayFailedOrCancelled, endarg).SetUndoFunc(UndoMayFailedWithInfo, true) 600 | 601 | for i := 0; i < 99; i++ { 602 | arg := argsForTest{ 603 | id: i, 604 | taskShouldFailed: i == 96, // task 96 will fail 605 | undoShouldFailed: i == 5 || i == 2 || i == 1, // undo 1 2 5 will fail 606 | infoShouldPrint: i == 1, 607 | sleepTime: i, 608 | } 609 | task := controller.AddTask(strconv.Itoa(i), TaskMayFailedOrCancelled, arg).SetUndoFunc(UndoMayFailedWithInfo, true) 610 | end.SetDependency(MakeAndExpr(end.DependencyExpr(), end.NewDependencyExpr(task))) 611 | } 612 | controller.SetTermination(controller.NewTerminationExpr(end)) 613 | _, err := controller.BatchRun() 614 | if err == nil { 615 | t.Fatal("Task not fail!") 616 | } else { 617 | t.Log(err) 618 | } 619 | 620 | pool := NewDefaultPool(2) 621 | defer pool.Close() 622 | _, err = controller.PoolRun(pool) 623 | if err == nil { 624 | t.Fatal("Task not fail!") 625 | } else { 626 | t.Log(err) 627 | } 628 | } 629 | 630 | func TaskSilentFail(args map[string]interface{}) (interface{}, error) { 631 | return nil, ErrSilentFail{} 632 | } 633 | 634 | func TestSuccessWithSilentFail(t *testing.T) { 635 | controller := NewTCController() 636 | A := controller.AddTask("A", TaskDefault, 1) 637 | B := controller.AddTask("B", TaskSilentFail, nil) 638 | C := controller.AddTask("C", TaskDefault, 3) 639 | D := controller.AddTask("D", TaskDefault, 4) 640 | 641 | C.SetDependency(MakeOrExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) 642 | D.SetDependency(D.NewDependencyExpr(C)) 643 | 644 | controller.SetTermination(controller.NewTerminationExpr(D)) 645 | 646 | res, err := controller.BatchRun() 647 | if err != nil { 648 | t.Fatal(err) 649 | } 650 | if sum, ok := res["D"]; !ok { 651 | t.Fatal(err) 652 | } else if sum != 8 { 653 | t.Fatal("Sum Error", sum) 654 | } 655 | } 656 | 657 | func TaskMustFail(args map[string]interface{}) (interface{}, error) { 658 | return nil, ErrTaskFailed{args["BIND"].(int)} 659 | } 660 | 661 | func TestFailedWithSilentFail(t *testing.T) { 662 | controller := NewTCController() 663 | A := controller.AddTask("A", TaskDefault, 1) 664 | B := controller.AddTask("B", TaskSilentFail, nil) 665 | C := controller.AddTask("C", TaskDefault, 3) 666 | D := controller.AddTask("D", TaskMustFail, 4) 667 | 668 | C.SetDependency(MakeOrExpr(C.NewDependencyExpr(A), C.NewDependencyExpr(B))) 669 | D.SetDependency(D.NewDependencyExpr(C)) 670 | 671 | controller.SetTermination(MakeAndExpr(controller.TerminationExpr(), controller.NewTerminationExpr(D))) 672 | 673 | _, err := controller.BatchRun() 674 | if err == nil { 675 | t.Fatal("Task not fail!") 676 | } else { 677 | t.Log(err) 678 | } 679 | } 680 | --------------------------------------------------------------------------------