├── .github
└── workflows
│ └── go.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── benchmark
└── benchmark_test.go
├── examples
├── conditional
│ └── condition.go
├── fibonacci
│ └── fibonacci.go
├── loop
│ └── loop.go
├── parallel_merge_sort
│ └── parallel_merge_sort.go
├── priority
│ └── priority.go
├── simple
│ └── simple.go
└── word_count
│ └── word_count.go
├── executor.go
├── executor_test.go
├── flow.go
├── go.mod
├── go.sum
├── graph.go
├── image
├── condition.svg
├── desc.svg
├── fl.svg
├── loop.svg
├── simple.svg
└── subflow.svg
├── node.go
├── profiler.go
├── profiler_test.go
├── task.go
├── taskflow.go
├── taskflow_test.go
├── utils
├── copool.go
├── copool_test.go
├── obj_pool.go
├── pprof.go
├── pprof_test.go
├── queue.go
├── queue_test.go
├── utils.go
└── utils_test.go
├── visualizer.go
├── visualizer_dot.go
└── visualizer_dot_test.go
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Generate code coverage badge
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | label:
8 | types:
9 | - created
10 | push:
11 | branches:
12 | - main
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 | name: Update coverage badge
17 | timeout-minutes: 30 # 添加超时设置(单位:分钟)
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | with:
23 | ref: ${{ github.event.pull_request.head.sha }}
24 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
25 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
26 |
27 | - name: Setup go
28 | uses: actions/setup-go@v4
29 | with:
30 | go-version-file: 'go.mod'
31 |
32 | - name: Run Test
33 | run: |
34 | go test -count=100 -timeout=1800s -v ./... -covermode=count -coverprofile=coverage.out
35 |
36 | - name: Upload coverage to Codecov
37 | uses: codecov/codecov-action@v4
38 | with:
39 | token: ${{ secrets.CODECOV_TOKEN }}
40 | files: ./coverage.out # optional
41 | flags: unittests # optional
42 |
--------------------------------------------------------------------------------
/.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 | bin/
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 | *.dot
17 |
18 | # Dependency directories (remove the comment below to include it)
19 | # vendor/
20 |
21 | # Go workspace file
22 | go.work
23 | *.prof
24 | example
25 | .vscode/*
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: "10m"
3 | skip-files:
4 | - "*/_test\\.go$" # 使用双反斜杠转义点
5 |
6 | linters:
7 | disable-all: true
8 | enable:
9 | # basic
10 | - govet
11 | - staticcheck
12 | - errcheck
13 | - ineffassign
14 | - gosimple
15 | - unused
16 | # style
17 | - gofmt
18 | - goimports
19 | - misspell
20 | - stylecheck
21 | - dupl
22 | - wsl
23 | - goconst
24 | # complexity
25 | - funlen
26 | - gocyclo
27 | - lll
28 | # security
29 | - gosec
30 |
31 | linters-settings:
32 | funlen:
33 | # Checks the number of lines in a function.
34 | lines: 80
35 | # Checks the number of statements in a function.
36 | statements: 40
37 | # Ignore comments when counting lines.
38 | ignore-comments: true
39 | lines-in-file: 800
40 | gocyclo:
41 | # Minimal code complexity to report.
42 | # Default: 30 (but we recommend 10-20)
43 | min-complexity: 15
44 | lll:
45 | # Max line length, lines longer will be reported.
46 | # Default: 120.
47 | line-length: 120
48 | dupl:
49 | # Tokens count to trigger issue.
50 | # Default: 150
51 | threshold: 100
52 |
53 | output:
54 | format: colored-line-number
55 | print-issued-lines: true
56 | print-config: true
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-taskflow
2 |
3 | [](https://codecov.io/github/noneback/go-taskflow)
4 | [](https://pkg.go.dev/github.com/noneback/go-taskflow)
5 | [](https://goreportcard.com/report/github.com/noneback/go-taskflow)
6 | [](https://github.com/avelino/awesome-go)
7 | [![DeepWiki][deepwiki-image]][deepwiki-url]
8 |
9 | [deepwiki-url]: https://deepwiki.com/noneback/go-taskflow
10 | [deepwiki-image]: https://img.shields.io/badge/Chat%20with-DeepWiki%20🤖-20B2AA
11 |
12 | 
13 |
14 | go-taskflow is a general-purpose task-parallel programming framework for Go, inspired by [taskflow-cpp](https://github.com/taskflow/taskflow). It leverages Go's native capabilities and simplicity, making it ideal for managing complex dependencies in concurrent tasks.
15 |
16 | ## Features
17 |
18 | - **High Extensibility**: Easily extend the framework to adapt to various specific use cases.
19 | - **Native Go Concurrency Model**: Leverages Go's goroutines for efficient concurrent task execution.
20 | - **User-Friendly Programming Interface**: Simplifies complex task dependency management in Go.
21 | - **Static, Subflow, Conditional, and Cyclic Tasking**: Define static tasks, condition nodes, nested subflows, and cyclic flows to enhance modularity and programmability.
22 |
23 | | Static | Subflow | Condition | Cyclic |
24 | |:-----------|:------------:|------------:|------------:|
25 | |  |  |  |  |
26 |
27 | - **Priority Task Scheduling**: Assign task priorities to ensure higher-priority tasks are executed first.
28 | - **Built-in Visualization and Profiling Tools**: Generate visual representations of tasks and profile task execution performance using integrated tools, simplifying debugging and optimization.
29 |
30 | ## Use Cases
31 |
32 | - **Data Pipelines**: Orchestrate data processing stages with complex dependencies.
33 | - **AI Agent Workflow Automation**: Define and execute AI agent workflows with clear sequences and dependency structures.
34 | - **Parallel Graph Tasking**: Execute graph-based tasks concurrently to maximize CPU utilization.
35 |
36 | ## Installation
37 |
38 | Import the latest version of go-taskflow using:
39 |
40 | ```bash
41 | go get -u github.com/noneback/go-taskflow
42 | ```
43 | ## Documentation
44 | [DeepWiki Page](https://deepwiki.com/noneback/go-taskflow)
45 | ## Example
46 |
47 | Below is an example of using go-taskflow to implement a parallel merge sort:
48 |
49 | ```go
50 | package main
51 |
52 | import (
53 | "fmt"
54 | "log"
55 | "math/rand"
56 | "os"
57 | "slices"
58 | "strconv"
59 | "sync"
60 |
61 | gtf "github.com/noneback/go-taskflow"
62 | )
63 |
64 | // mergeInto merges a sorted source array into a sorted destination array.
65 | func mergeInto(dest, src []int) []int {
66 | size := len(dest) + len(src)
67 | tmp := make([]int, 0, size)
68 | i, j := 0, 0
69 | for i < len(dest) && j < len(src) {
70 | if dest[i] < src[j] {
71 | tmp = append(tmp, dest[i])
72 | i++
73 | } else {
74 | tmp = append(tmp, src[j])
75 | j++
76 | }
77 | }
78 |
79 | if i < len(dest) {
80 | tmp = append(tmp, dest[i:]...)
81 | } else {
82 | tmp = append(tmp, src[j:]...)
83 | }
84 |
85 | return tmp
86 | }
87 |
88 | func main() {
89 | size := 100
90 | randomArr := make([][]int, 10)
91 | sortedArr := make([]int, 0, 10*size)
92 | mutex := &sync.Mutex{}
93 |
94 | for i := 0; i < 10; i++ {
95 | for j := 0; j < size; j++ {
96 | randomArr[i] = append(randomArr[i], rand.Int())
97 | }
98 | }
99 |
100 | sortTasks := make([]*gtf.Task, 10)
101 | tf := gtf.NewTaskFlow("merge sort")
102 | done := tf.NewTask("Done", func() {
103 | if !slices.IsSorted(sortedArr) {
104 | log.Fatal("Sorting failed")
105 | }
106 | fmt.Println("Sorted successfully")
107 | fmt.Println(sortedArr[:1000])
108 | })
109 |
110 | for i := 0; i < 10; i++ {
111 | sortTasks[i] = tf.NewTask("sort_"+strconv.Itoa(i), func() {
112 | arr := randomArr[i]
113 | slices.Sort(arr)
114 | mutex.Lock()
115 | defer mutex.Unlock()
116 | sortedArr = mergeInto(sortedArr, arr)
117 | })
118 | }
119 | done.Succeed(sortTasks...)
120 |
121 | executor := gtf.NewExecutor(1000)
122 |
123 | executor.Run(tf).Wait()
124 |
125 | if err := tf.Dump(os.Stdout); err != nil {
126 | log.Fatal("Error dumping taskflow:", err)
127 | }
128 |
129 | if err := executor.Profile(os.Stdout); err != nil {
130 | log.Fatal("Error profiling taskflow:", err)
131 | }
132 | }
133 | ```
134 |
135 | For more examples, visit the [examples directory](https://github.com/noneback/go-taskflow/tree/main/examples).
136 |
137 | ## Benchmark
138 |
139 | The following benchmark provides a rough estimate of performance. Note that most realistic workloads are I/O-bound, and their performance cannot be accurately reflected by these results. For CPU-intensive tasks, consider using [taskflow-cpp](https://github.com/taskflow/taskflow).
140 |
141 | ```plaintext
142 | $ go test -bench=. -benchmem
143 | goos: linux
144 | goarch: amd64
145 | pkg: github.com/noneback/go-taskflow/benchmark
146 | cpu: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
147 | BenchmarkC32-4 23282 51891 ns/op 7295 B/op 227 allocs/op
148 | BenchmarkS32-4 7047 160199 ns/op 6907 B/op 255 allocs/op
149 | BenchmarkC6-4 66397 18289 ns/op 1296 B/op 47 allocs/op
150 | BenchmarkC8x8-4 7946 143474 ns/op 16914 B/op 504 allocs/op
151 | PASS
152 | ok github.com/noneback/go-taskflow/benchmark 5.606s
153 | ```
154 |
155 | ## Understanding Conditional Tasks
156 |
157 | Conditional nodes in go-taskflow behave similarly to those in [taskflow-cpp](https://github.com/taskflow/taskflow). They participate in both conditional control and looping. To avoid common pitfalls, refer to the [Conditional Tasking documentation](https://taskflow.github.io/taskflow/ConditionalTasking.html).
158 |
159 | ## Error Handling in go-taskflow
160 |
161 | In Go, `errors` are values, and it is the user's responsibility to handle them appropriately. Only unrecovered `panic` events are managed by the framework. If a `panic` occurs, the entire parent graph is canceled, leaving the remaining tasks incomplete. This behavior may evolve in the future. If you have suggestions, feel free to share them.
162 |
163 | To prevent interruptions caused by `panic`, you can handle them manually when registering tasks:
164 |
165 | ```go
166 | tf.NewTask("not interrupt", func() {
167 | defer func() {
168 | if r := recover(); r != nil {
169 | // Handle the panic.
170 | }
171 | }()
172 | // User-defined logic.
173 | })
174 | ```
175 |
176 | ## Visualizing Taskflows
177 |
178 | To generate a visual representation of a taskflow, use the `Dump` method:
179 |
180 | ```go
181 | if err := tf.Dump(os.Stdout); err != nil {
182 | log.Fatal(err)
183 | }
184 | ```
185 |
186 | The `Dump` method generates raw strings in DOT format. Use the `dot` tool to create a graph SVG.
187 |
188 | 
189 |
190 | ## Profiling Taskflows
191 |
192 | To profile a taskflow, use the `Profile` method:
193 |
194 | ```go
195 | if err := executor.Profile(os.Stdout); err != nil {
196 | log.Fatal(err)
197 | }
198 | ```
199 |
200 | The `Profile` method generates raw strings in flamegraph format. Use the `flamegraph` tool to create a flamegraph SVG.
201 |
202 | 
203 |
204 | ## Stargazer
205 |
206 | [](https://star-history.com/#noneback/go-taskflow&Date)
207 |
208 |
--------------------------------------------------------------------------------
/benchmark/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package benchmark
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | gotaskflow "github.com/noneback/go-taskflow"
8 | )
9 |
10 | var executor = gotaskflow.NewExecutor(6400)
11 |
12 | func BenchmarkC32(b *testing.B) {
13 | tf := gotaskflow.NewTaskFlow("G")
14 | for i := 0; i < 32; i++ {
15 | tf.NewTask(fmt.Sprintf("N%d", i), func() {})
16 | }
17 |
18 | for i := 0; i < b.N; i++ {
19 | executor.Run(tf).Wait()
20 | }
21 | }
22 |
23 | func BenchmarkS32(b *testing.B) {
24 | tf := gotaskflow.NewTaskFlow("G")
25 | prev := tf.NewTask("N0", func() {})
26 | for i := 1; i < 32; i++ {
27 | next := tf.NewTask(fmt.Sprintf("N%d", i), func() {})
28 | prev.Precede(next)
29 | prev = next
30 | }
31 |
32 | for i := 0; i < b.N; i++ {
33 | executor.Run(tf).Wait()
34 | }
35 | }
36 |
37 | func BenchmarkC6(b *testing.B) {
38 | tf := gotaskflow.NewTaskFlow("G")
39 | n0 := tf.NewTask("N0", func() {})
40 | n1 := tf.NewTask("N1", func() {})
41 | n2 := tf.NewTask("N2", func() {})
42 | n3 := tf.NewTask("N3", func() {})
43 | n4 := tf.NewTask("N4", func() {})
44 | n5 := tf.NewTask("N5", func() {})
45 |
46 | n0.Precede(n1, n2)
47 | n1.Precede(n3)
48 | n2.Precede(n4)
49 | n5.Succeed(n3, n4)
50 |
51 | for i := 0; i < b.N; i++ {
52 | executor.Run(tf).Wait()
53 | }
54 | }
55 |
56 | func BenchmarkC8x8(b *testing.B) {
57 | tf := gotaskflow.NewTaskFlow("G")
58 |
59 | layersCount := 8
60 | layerNodesCount := 8
61 |
62 | var curLayer, upperLayer []*gotaskflow.Task
63 |
64 | for i := 0; i < layersCount; i++ {
65 | for j := 0; j < layerNodesCount; j++ {
66 | task := tf.NewTask(fmt.Sprintf("N%d", i*layersCount+j), func() {})
67 |
68 | for i := range upperLayer {
69 | upperLayer[i].Precede(task)
70 | }
71 |
72 | curLayer = append(curLayer, task)
73 | }
74 |
75 | upperLayer = curLayer
76 | curLayer = []*gotaskflow.Task{}
77 | }
78 |
79 | for i := 0; i < b.N; i++ {
80 | executor.Run(tf).Wait()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/examples/conditional/condition.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "runtime"
8 | "time"
9 |
10 | gotaskflow "github.com/noneback/go-taskflow"
11 | )
12 |
13 | func main() {
14 | executor := gotaskflow.NewExecutor(uint(runtime.NumCPU()-1) * 10000)
15 | tf := gotaskflow.NewTaskFlow("G")
16 | A, B, C :=
17 | tf.NewTask("A", func() {
18 | fmt.Println("A")
19 | }),
20 | tf.NewTask("B", func() {
21 | fmt.Println("B")
22 | }),
23 | tf.NewTask("C", func() {
24 | fmt.Println("C")
25 | })
26 |
27 | A1, B1, _ :=
28 | tf.NewTask("A1", func() {
29 | fmt.Println("A1")
30 | }),
31 | tf.NewTask("B1", func() {
32 | fmt.Println("B1")
33 | }),
34 | tf.NewTask("C1", func() {
35 | fmt.Println("C1")
36 | })
37 | A.Precede(B)
38 | C.Precede(B)
39 | A1.Precede(B)
40 | C.Succeed(A1)
41 | C.Succeed(B1)
42 |
43 | subflow := tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
44 | A2, B2, C2 :=
45 | sf.NewTask("A2", func() {
46 | fmt.Println("A2")
47 | }),
48 | sf.NewTask("B2", func() {
49 | fmt.Println("B2")
50 | }),
51 | sf.NewTask("C2", func() {
52 | fmt.Println("C2")
53 | })
54 | A2.Precede(B2)
55 | C2.Precede(B2)
56 | })
57 |
58 | subflow2 := tf.NewSubflow("sub2", func(sf *gotaskflow.Subflow) {
59 | A3, B3, C3 :=
60 | sf.NewTask("A3", func() {
61 | fmt.Println("A3")
62 | }),
63 | sf.NewTask("B3", func() {
64 | fmt.Println("B3")
65 | }),
66 | sf.NewTask("C3", func() {
67 | fmt.Println("C3")
68 | // time.Sleep(10 * time.Second)
69 | })
70 | A3.Precede(B3)
71 | C3.Precede(B3)
72 | })
73 |
74 | cond := tf.NewCondition("binary", func() uint {
75 | return uint(time.Now().Second() % 2)
76 | })
77 | B.Precede(cond)
78 | cond.Precede(subflow, subflow2)
79 | executor.Run(tf).Wait()
80 | fmt.Println("Print DOT")
81 | if err := tf.Dump(os.Stdout); err != nil {
82 | log.Fatal(err)
83 | }
84 | fmt.Println("Print Flamegraph")
85 | if err := executor.Profile(os.Stdout); err != nil {
86 | log.Fatal(err)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/examples/fibonacci/fibonacci.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "runtime"
8 |
9 | gotaskflow "github.com/noneback/go-taskflow"
10 | )
11 |
12 | func main() {
13 | n := 10
14 | fib := make([]int, n+1)
15 |
16 | executor := gotaskflow.NewExecutor(uint(runtime.NumCPU()))
17 | tf := gotaskflow.NewTaskFlow("fibonacci")
18 |
19 | f0 := tf.NewTask("F0", func() { fib[0] = 1 })
20 | f1 := tf.NewTask("F1", func() { fib[1] = 1 })
21 |
22 | tasks := []*gotaskflow.Task{f0, f1}
23 | for k := 2; k <= n; k++ {
24 | k := k
25 | task := tf.NewTask(fmt.Sprintf("F%d", k), func() {
26 | fib[k] = fib[k-1] + fib[k-2]
27 | })
28 | tasks[k-1].Precede(task) // F(n-1) -> F(n)
29 | tasks[k-2].Precede(task) // F(n-2) -> F(n)
30 | tasks = append(tasks, task)
31 | }
32 | if err := tf.Dump(os.Stdout); err != nil {
33 | log.Fatal(err)
34 | }
35 | executor.Run(tf).Wait()
36 |
37 | fmt.Printf("F(%d) = %d\n", n+1, fib[n])
38 | }
39 |
--------------------------------------------------------------------------------
/examples/loop/loop.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "runtime"
8 | "time"
9 |
10 | gotaskflow "github.com/noneback/go-taskflow"
11 | )
12 |
13 | func main() {
14 |
15 | executor := gotaskflow.NewExecutor(uint(runtime.NumCPU()) * 100)
16 | i := 0
17 | tf := gotaskflow.NewTaskFlow("G")
18 | init, cond, body, back, done :=
19 | tf.NewTask("init", func() {
20 | i = 0
21 | fmt.Println("i=0")
22 | }),
23 | tf.NewCondition("while i < 5", func() uint {
24 | time.Sleep(100 * time.Millisecond)
25 | if i < 5 {
26 | return 0
27 | } else {
28 | return 1
29 | }
30 | }),
31 | tf.NewTask("body", func() {
32 | i += 1
33 | fmt.Println("i++ =", i)
34 | }),
35 | tf.NewCondition("back", func() uint {
36 | fmt.Println("back")
37 | return 0
38 | }),
39 | tf.NewTask("done", func() {
40 | fmt.Println("done")
41 | })
42 |
43 | init.Precede(cond)
44 | cond.Precede(body, done)
45 | body.Precede(back)
46 | back.Precede(cond)
47 |
48 | executor.Run(tf).Wait()
49 | if i < 5 {
50 | log.Fatal("i < 5")
51 | }
52 |
53 | if err := tf.Dump(os.Stdout); err != nil {
54 | log.Fatal(err)
55 | }
56 | executor.Profile(os.Stdout)
57 | }
58 |
--------------------------------------------------------------------------------
/examples/parallel_merge_sort/parallel_merge_sort.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "os"
8 | "slices"
9 | "strconv"
10 | "sync"
11 |
12 | gtf "github.com/noneback/go-taskflow"
13 | "github.com/noneback/go-taskflow/utils"
14 | )
15 |
16 | // merge sorted src to sorted dest
17 | func mergeInto(dest, src []int) []int {
18 | size := len(dest) + len(src)
19 | tmp := make([]int, 0, size)
20 | i, j := 0, 0
21 | for i < len(dest) && j < len(src) {
22 | if dest[i] < src[j] {
23 | tmp = append(tmp, dest[i])
24 | i++
25 | } else {
26 | tmp = append(tmp, src[j])
27 | j++
28 | }
29 | }
30 |
31 | if i < len(dest) {
32 | tmp = append(tmp, dest[i:]...)
33 | } else {
34 | tmp = append(tmp, src[j:]...)
35 | }
36 |
37 | return tmp
38 | }
39 | func main() {
40 | pprof := utils.NewPprofUtils(utils.CPU, "./out.prof")
41 | pprof.StartProfile()
42 | defer pprof.StopProfile()
43 |
44 | size := 10000
45 | share := 1000
46 | randomArr := make([][]int, share)
47 | sortedArr := make([]int, 0, share*size)
48 | mutex := &sync.Mutex{}
49 |
50 | for i := 0; i < share; i++ {
51 | for j := 0; j < size; j++ {
52 | randomArr[i] = append(randomArr[i], rand.Int())
53 | }
54 | }
55 |
56 | sortTasks := make([]*gtf.Task, share)
57 | tf := gtf.NewTaskFlow("merge sort")
58 | done := tf.NewTask("Done", func() {
59 | if !slices.IsSorted(sortedArr) {
60 | log.Fatal("Failed")
61 | }
62 | fmt.Println("Sorted")
63 | fmt.Println(sortedArr[:1000])
64 | })
65 |
66 | for i := 0; i < share; i++ {
67 | idx := i
68 | sortTasks[idx] = tf.NewTask("sort_"+strconv.Itoa(idx), func() {
69 | arr := randomArr[idx]
70 | slices.Sort(arr)
71 | mutex.Lock()
72 | defer mutex.Unlock()
73 | sortedArr = mergeInto(sortedArr, arr)
74 | })
75 | }
76 | done.Succeed(sortTasks...)
77 |
78 | executor := gtf.NewExecutor(1000000)
79 |
80 | executor.Run(tf).Wait()
81 |
82 | if err := tf.Dump(os.Stdout); err != nil {
83 | log.Fatal("V->", err)
84 | }
85 |
86 | if err := executor.Profile(os.Stdout); err != nil {
87 | log.Fatal("P->", err)
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/examples/priority/priority.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | gotaskflow "github.com/noneback/go-taskflow"
7 | "github.com/noneback/go-taskflow/utils"
8 | )
9 |
10 | func main() {
11 | exector := gotaskflow.NewExecutor(uint(2))
12 | q := utils.NewQueue[byte](true)
13 | tf := gotaskflow.NewTaskFlow("G")
14 |
15 | tf.NewTask("B", func() {
16 | fmt.Println("B")
17 | q.Put('B')
18 | }).Priority(gotaskflow.NORMAL)
19 | tf.NewTask("C", func() {
20 | fmt.Println("C")
21 | q.Put('C')
22 | }).Priority(gotaskflow.HIGH)
23 | tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
24 | sf.NewTask("A2", func() {
25 | fmt.Println("A2")
26 | q.Put('a')
27 | }).Priority(gotaskflow.LOW)
28 | sf.NewTask("B2", func() {
29 | fmt.Println("B2")
30 | q.Put('b')
31 | }).Priority(gotaskflow.HIGH)
32 | sf.NewTask("C2", func() {
33 | fmt.Println("C2")
34 | q.Put('c')
35 | }).Priority(gotaskflow.NORMAL)
36 |
37 | }).Priority(gotaskflow.LOW)
38 |
39 | exector.Run(tf).Wait()
40 | }
41 |
--------------------------------------------------------------------------------
/examples/simple/simple.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "runtime"
8 |
9 | gotaskflow "github.com/noneback/go-taskflow"
10 | )
11 |
12 | func main() {
13 | executor := gotaskflow.NewExecutor(uint(runtime.NumCPU()-1) * 10000)
14 |
15 | tf := gotaskflow.NewTaskFlow("G")
16 | A, B, C :=
17 | tf.NewTask("A", func() {
18 | fmt.Println("A")
19 | }),
20 | tf.NewTask("B", func() {
21 | fmt.Println("B")
22 | }),
23 | tf.NewTask("C", func() {
24 | fmt.Println("C")
25 | })
26 |
27 | A1, B1, _ :=
28 | tf.NewTask("A1", func() {
29 | fmt.Println("A1")
30 | }),
31 | tf.NewTask("B1", func() {
32 | fmt.Println("B1")
33 | }),
34 | tf.NewTask("C1", func() {
35 | fmt.Println("C1")
36 | })
37 | A.Precede(B)
38 | C.Precede(B)
39 | A1.Precede(B)
40 | C.Succeed(A1)
41 | C.Succeed(B1)
42 |
43 | subflow := tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
44 | A2, B2, C2 :=
45 | sf.NewTask("A2", func() {
46 | fmt.Println("A2")
47 | }),
48 | sf.NewTask("B2", func() {
49 | fmt.Println("B2")
50 | }),
51 | sf.NewTask("C2", func() {
52 | fmt.Println("C2")
53 | })
54 | A2.Precede(B2)
55 | C2.Precede(B2)
56 | })
57 |
58 | subflow2 := tf.NewSubflow("sub2", func(sf *gotaskflow.Subflow) {
59 | A3, B3, C3 :=
60 | sf.NewTask("A3", func() {
61 | fmt.Println("A3")
62 | }),
63 | sf.NewTask("B3", func() {
64 | fmt.Println("B3")
65 | }),
66 | sf.NewTask("C3", func() {
67 | fmt.Println("C3")
68 | // time.Sleep(10 * time.Second)
69 | })
70 | A3.Precede(B3)
71 | C3.Precede(B3)
72 | })
73 |
74 | subflow.Precede(B)
75 | subflow.Precede(subflow2)
76 | executor.Run(tf).Wait()
77 | fmt.Println("Print DOT")
78 | if err := tf.Dump(os.Stdout); err != nil {
79 | log.Fatal(err)
80 | }
81 | fmt.Println("Print Flamegraph")
82 | if err := executor.Profile(os.Stdout); err != nil {
83 | log.Fatal(err)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/examples/word_count/word_count.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | "runtime"
11 | "sort"
12 | "strings"
13 |
14 | gotaskflow "github.com/noneback/go-taskflow"
15 | )
16 |
17 | // 配置参数结构体
18 | type MRConfig struct {
19 | NumMappers int
20 | NumReducers int
21 | ChunkSize int
22 | TempDir string
23 | OutputPath string
24 | }
25 |
26 | type WordCount struct {
27 | Key string
28 | Value int
29 | }
30 |
31 | type MapReduce struct {
32 | cfg MRConfig
33 | executor gotaskflow.Executor
34 | input string
35 | mapOutputs [][]string
36 | }
37 |
38 | func NewMapReduce(input string, cfg MRConfig) *MapReduce {
39 | os.MkdirAll(cfg.TempDir, 0755)
40 | return &MapReduce{
41 | cfg: cfg,
42 | executor: gotaskflow.NewExecutor(uint(runtime.NumCPU())),
43 | input: input,
44 | }
45 | }
46 | func (mr *MapReduce) Run() {
47 | tf := gotaskflow.NewTaskFlow("wordcount")
48 | mapper := make([][]int, mr.cfg.NumMappers)
49 | // spilt doc
50 | splitTask := tf.NewTask("split_input", func() {
51 | chunks := splitString(mr.input, mr.cfg.ChunkSize)
52 | for i, chunk := range chunks {
53 | path := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("input_%d.txt", i))
54 | if err := os.WriteFile(path, []byte(chunk), 0644); err != nil {
55 | log.Fatal(err)
56 | }
57 | mapper[i%mr.cfg.NumMappers] = append(mapper[i%mr.cfg.NumMappers], i)
58 | }
59 | })
60 |
61 | mapTasks := make([]*gotaskflow.Task, mr.cfg.NumMappers)
62 | mr.mapOutputs = make([][]string, mr.cfg.NumMappers)
63 |
64 | for i := 0; i < mr.cfg.NumMappers; i++ {
65 | // scan input
66 | idx := i
67 | mapTasks[idx] = tf.NewTask(fmt.Sprintf("map_%d", idx), func() {
68 | for _, val := range mapper[idx] {
69 | mr.processMap(idx, val)
70 | }
71 | })
72 | }
73 | splitTask.Precede(mapTasks...)
74 |
75 | reduceTasks := make([]*gotaskflow.Task, mr.cfg.NumReducers)
76 | for r := 0; r < mr.cfg.NumReducers; r++ {
77 | r := r
78 | reduceTasks[r] = tf.NewTask(fmt.Sprintf("reduce_%d", r), func() {
79 | log.Println("reduce:", r)
80 | mr.processReduce(r)
81 | })
82 | }
83 |
84 | for _, mapTask := range mapTasks {
85 | mapTask.Precede(reduceTasks...)
86 | }
87 |
88 | mergeTask := tf.NewTask("merge_results", mr.mergeResults)
89 | for _, rt := range reduceTasks {
90 | rt.Precede(mergeTask)
91 | }
92 |
93 | if err := tf.Dump(os.Stdout); err != nil {
94 | log.Fatal(err)
95 | }
96 | mr.executor.Run(tf).Wait()
97 | }
98 |
99 | func (mr *MapReduce) processMap(mapID, inputID int) {
100 | inputPath := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("input_%d.txt", inputID))
101 | data, err := os.ReadFile(inputPath)
102 | if err != nil {
103 | log.Fatal(err)
104 | }
105 | fmt.Println(mapID, "process", inputPath)
106 |
107 | intermediate := make([]map[string]int, mr.cfg.NumReducers)
108 | for i := range intermediate {
109 | intermediate[i] = make(map[string]int)
110 | }
111 |
112 | scanner := bufio.NewScanner(strings.NewReader(string(data)))
113 | scanner.Split(bufio.ScanWords)
114 |
115 | for scanner.Scan() {
116 | word := scanner.Text()
117 | r := hash(word) % mr.cfg.NumReducers
118 | intermediate[r][word]++
119 | }
120 |
121 | outputs := make([]string, mr.cfg.NumReducers)
122 | for r := 0; r < mr.cfg.NumReducers; r++ {
123 | fpath := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("map-%d-reduce-%d.json", mapID, r))
124 | file, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
125 | if err != nil {
126 | log.Fatal(err)
127 | }
128 | defer file.Close()
129 |
130 | enc := json.NewEncoder(file)
131 | for word, count := range intermediate[r] {
132 | enc.Encode(WordCount{word, count})
133 | }
134 | outputs[r] = fpath
135 | }
136 | mr.mapOutputs[mapID] = outputs
137 | }
138 |
139 | func (mr *MapReduce) processReduce(reduceID int) {
140 | var intermediate []WordCount
141 |
142 | for m := 0; m < mr.cfg.NumMappers; m++ {
143 | fpath := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("map-%d-reduce-%d.json", m, reduceID))
144 | file, err := os.Open(fpath)
145 | if err != nil {
146 | continue
147 | }
148 | defer file.Close()
149 |
150 | dec := json.NewDecoder(file)
151 | for dec.More() {
152 | var wc WordCount
153 | if err := dec.Decode(&wc); err != nil {
154 | log.Fatal(err)
155 | }
156 | intermediate = append(intermediate, wc)
157 | }
158 | }
159 |
160 | results := make(map[string]int)
161 | for _, item := range intermediate {
162 | results[item.Key] += item.Value
163 | }
164 |
165 | outputPath := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("reduce-out-%d.txt", reduceID))
166 | file, err := os.Create(outputPath)
167 | if err != nil {
168 | log.Fatal(err)
169 | }
170 | defer file.Close()
171 |
172 | for word, count := range results {
173 | fmt.Fprintf(file, "%s\t%d\n", word, count)
174 | }
175 | }
176 |
177 | func (mr *MapReduce) mergeResults() {
178 | var finalResults map[string]int = make(map[string]int)
179 |
180 | for r := 0; r < mr.cfg.NumReducers; r++ {
181 | path := filepath.Join(mr.cfg.TempDir, fmt.Sprintf("reduce-out-%d.txt", r))
182 | file, err := os.Open(path)
183 | if err != nil {
184 | continue
185 | }
186 | defer file.Close()
187 |
188 | scanner := bufio.NewScanner(file)
189 | for scanner.Scan() {
190 | var word string
191 | var count int
192 | fmt.Sscanf(scanner.Text(), "%s\t%d", &word, &count)
193 | finalResults[word] += count
194 | }
195 | }
196 |
197 | keys := make([]string, 0, len(finalResults))
198 | for k := range finalResults {
199 | keys = append(keys, k)
200 | }
201 | sort.Strings(keys)
202 |
203 | outputFile, err := os.Create(mr.cfg.OutputPath)
204 | if err != nil {
205 | log.Fatal(err)
206 | }
207 | defer outputFile.Close()
208 |
209 | for _, word := range keys {
210 | fmt.Fprintf(outputFile, "%s\t%d\n", word, finalResults[word])
211 | }
212 | }
213 |
214 | func splitString(s string, chunkSize int) []string {
215 | var chunks []string
216 | for len(s) > 0 {
217 | if len(s) < chunkSize {
218 | chunks = append(chunks, s)
219 | break
220 | }
221 | chunk := s[:chunkSize]
222 | if lastSpace := strings.LastIndex(chunk, " "); lastSpace != -1 {
223 | chunk = chunk[:lastSpace]
224 | s = s[lastSpace+1:]
225 | } else {
226 | s = s[chunkSize:]
227 | }
228 | chunks = append(chunks, chunk)
229 | }
230 | return chunks
231 | }
232 |
233 | func hash(s string) int {
234 | h := 0
235 | for _, c := range s {
236 | h = 31*h + int(c)
237 | }
238 | if h < 0 {
239 | h = -h
240 | }
241 | return h
242 | }
243 |
244 | func main() {
245 | log.SetFlags(log.Llongfile)
246 | input := `Navigating This Book Now that you know who you’ll be hearing from, the next logical step would be to find out what you’ll be hearing about, which brings us to the second thing I wanted to mention. There are conceptually two major parts to this book, each with four chapters, and each followed up by a chapter that stands relatively independently on its own.
247 | The fun begins with Part I, The Beam Model (Chapters 1–4), which focuses on the high-level batch plus streaming data processing model originally developed for Google Cloud Dataflow, later donated to the Apache Software Foundation as Apache Beam, and also now seen in whole or in part across most other systems in the industry. It’s composed of four chapters: Chapter 1, Streaming 101, which covers the basics of stream processing, establishing some terminology, discussing the capabilities of streaming systems, distinguishing between two important domains of time (processing time and event time), and finally looking at some common data processing patterns.
248 | Chapter 2, The What, Where, When, and How of Data Processing, which covers in detail the core concepts of robust stream processing over out-of-order data, each analyzed within the context of a concrete running example and with animated diagrams to highlight the dimension of time.
249 | Chapter 3, Watermarks (written by Slava), which provides a deep survey of temporal progress metrics, how they are created, and how they propagate through pipelines. It ends by examining the details of two real-world watermark implementations.
250 | Chapter 4, Advanced Windowing, which picks up where Chapter 2 left off, diving into some advanced windowing and triggering concepts like processing-time windows, sessions, and continuation triggers.
251 | Between Parts I and II, providing an interlude as timely as the details contained therein are important, stands Chapter 5, Exactly-Once and Side Effects (written by Reuven). In it, he enumerates the challenges of providing end-to-end exactly-once (or effectively-once) processing semantics and walks through the implementation details of three different approaches to exactlyonce processing: Apache Flink, Apache Spark, and Google Cloud Dataflow.
252 | Next begins Part II, Streams and Tables (Chapters 6–9), which dives deeper into the conceptual and investigates the lower-level “streams and tables” way of thinking about stream processing, recently popularized by some upstanding citizens in the Apache Kafka community but, of course, invented decades ago by folks in the database community, because wasn’t everything? It too is composed of four chapters: Chapter 6, Streams and Tables, which introduces the basic idea of streams and tables, analyzes the classic MapReduce approach through a streams-and-tables lens, and then constructs a theory of streams and tables sufficiently general to encompass the full breadth of the Beam Model (and beyond).
253 | Chapter 7, The Practicalities of Persistent State, which considers the motivations for persistent state in streaming pipelines, looks at two common types of implicit state, and then analyzes a practical use case (advertising attribution) to inform the necessary characteristics of a general state management mechanism.
254 | Chapter 8, Streaming SQL, which investigates the meaning of streaming within the context of relational algebra and SQL, contrasts the inherent stream and table biases within the Beam Model and classic SQL as they exist today, and proposes a set of possible paths forward toward incorporating robust streaming semantics in SQL.
255 | Chapter 9, Streaming Joins, which surveys a variety of different join types, analyzes their behavior within the context of streaming, and finally looks in detail at a useful but ill-supported streaming join use case: temporal validity windows.
256 | Finally, closing out the book is Chapter 10, The Evolution of Large-Scale Data Processing, which strolls through a focused history of the MapReduce lineage of data processing systems, examining some of the important contributions that have evolved streaming systems into what they are today.`
257 |
258 | // 配置参数
259 | cfg := MRConfig{
260 | NumMappers: 4,
261 | NumReducers: 2,
262 | ChunkSize: 300,
263 | TempDir: "./mr-tmp",
264 | OutputPath: "final-count.txt",
265 | }
266 |
267 | mr := NewMapReduce(input, cfg)
268 | mr.Run()
269 |
270 | fmt.Println("Final word count:")
271 | data, _ := os.ReadFile(cfg.OutputPath)
272 | fmt.Println(string(data))
273 | }
274 |
--------------------------------------------------------------------------------
/executor.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "cmp"
5 | "fmt"
6 | "io"
7 | "log"
8 | "runtime/debug"
9 | "slices"
10 | "sync"
11 | "time"
12 |
13 | "github.com/noneback/go-taskflow/utils"
14 | )
15 |
16 | // Executor schedule and execute taskflow
17 | type Executor interface {
18 | Wait() // Wait block until all tasks finished
19 | Profile(w io.Writer) error // Profile write flame graph raw text into w
20 | Run(tf *TaskFlow) Executor // Run start to schedule and execute taskflow
21 | }
22 |
23 | type innerExecutorImpl struct {
24 | concurrency uint
25 | pool *utils.Copool
26 | wq *utils.Queue[*innerNode]
27 | wg *sync.WaitGroup
28 | profiler *profiler
29 | mu *sync.Mutex
30 | }
31 |
32 | // NewExecutor return a Executor with a specified max goroutine concurrency(recommend a value bigger than Runtime.NumCPU, **MUST** bigger than num(subflows). )
33 | func NewExecutor(concurrency uint) Executor {
34 | if concurrency == 0 {
35 | panic("executor concurrency cannot be zero")
36 | }
37 | t := newProfiler()
38 | return &innerExecutorImpl{
39 | concurrency: concurrency,
40 | pool: utils.NewCopool(concurrency),
41 | wq: utils.NewQueue[*innerNode](false),
42 | wg: &sync.WaitGroup{},
43 | profiler: t,
44 | mu: &sync.Mutex{},
45 | }
46 | }
47 |
48 | // Run start to schedule and execute taskflow
49 | func (e *innerExecutorImpl) Run(tf *TaskFlow) Executor {
50 | tf.frozen = true
51 | e.scheduleGraph(nil, tf.graph, nil)
52 | return e
53 | }
54 |
55 | func (e *innerExecutorImpl) invokeGraph(g *eGraph, parentSpan *span) bool {
56 | for {
57 | g.scheCond.L.Lock()
58 | e.mu.Lock()
59 | for !g.recyclable() && e.wq.Len() == 0 && !g.canceled.Load() {
60 | e.mu.Unlock()
61 | g.scheCond.Wait()
62 | e.mu.Lock()
63 | }
64 |
65 | g.scheCond.L.Unlock()
66 |
67 | // tasks can only be executed after sched, and joinCounter incr when sched, so here no need to lock up.
68 | if g.recyclable() || g.canceled.Load() {
69 | e.mu.Unlock()
70 | break
71 | }
72 | node := e.wq.Pop()
73 | e.mu.Unlock()
74 |
75 | e.invokeNode(node, parentSpan)
76 | }
77 | return !g.canceled.Load()
78 | }
79 |
80 | func (e *innerExecutorImpl) sche_successors(node *innerNode) {
81 | candidate := make([]*innerNode, 0, len(node.successors))
82 |
83 | for _, n := range node.successors {
84 | n.mu.Lock()
85 | if (n.recyclable() && n.state.Load() == kNodeStateIdle) || n.Typ == nodeCondition {
86 | // deps all done or condition node or task has been sched.
87 | n.state.Store(kNodeStateWaiting)
88 | candidate = append(candidate, n)
89 | }
90 | n.mu.Unlock()
91 | }
92 |
93 | slices.SortFunc(candidate, func(i, j *innerNode) int {
94 | return cmp.Compare(i.priority, j.priority)
95 | })
96 | node.setup() // make node repeatable
97 | e.schedule(candidate...)
98 | }
99 |
100 | func (e *innerExecutorImpl) invokeStatic(node *innerNode, parentSpan *span, p *Static) func() {
101 | return func() {
102 | span := span{extra: attr{
103 | typ: nodeStatic,
104 | name: node.name,
105 | }, begin: time.Now(), parent: parentSpan}
106 |
107 | defer func() {
108 | span.cost = time.Since(span.begin)
109 | if r := recover(); r != nil {
110 | node.g.canceled.Store(true)
111 | log.Printf("graph %v is canceled, since static node %v panics", node.g.name, node.name)
112 | log.Printf("[recovered] static node %s, panic: %v, stack: %s", node.name, r, debug.Stack())
113 | } else {
114 | e.profiler.AddSpan(&span) // remove canceled node span
115 | }
116 |
117 | node.drop()
118 | e.sche_successors(node)
119 | node.g.deref()
120 | e.wg.Done()
121 | }()
122 | if !node.g.canceled.Load() {
123 | node.state.Store(kNodeStateRunning)
124 | p.handle()
125 | node.state.Store(kNodeStateFinished)
126 | }
127 | }
128 | }
129 |
130 | func (e *innerExecutorImpl) invokeSubflow(node *innerNode, parentSpan *span, p *Subflow) func() {
131 | return func() {
132 | span := span{extra: attr{
133 | typ: nodeSubflow,
134 | name: node.name,
135 | }, begin: time.Now(), parent: parentSpan}
136 |
137 | defer func() {
138 | span.cost = time.Since(span.begin)
139 | if r := recover(); r != nil {
140 | log.Printf("graph %v is canceled, since subflow %v panics", node.g.name, node.name)
141 | log.Printf("[recovered] subflow %s, panic: %v, stack: %s", node.name, r, debug.Stack())
142 | node.g.canceled.Store(true)
143 | p.g.canceled.Store(true)
144 | } else {
145 | e.profiler.AddSpan(&span) // remove canceled node span
146 | }
147 |
148 | e.scheduleGraph(node.g, p.g, &span)
149 | node.drop()
150 | e.sche_successors(node)
151 | node.g.deref()
152 | e.wg.Done()
153 | }()
154 |
155 | if !node.g.canceled.Load() {
156 | node.state.Store(kNodeStateRunning)
157 | if !p.g.instantiated {
158 | p.handle(p)
159 | }
160 | p.g.instantiated = true
161 | node.state.Store(kNodeStateFinished)
162 | }
163 | }
164 | }
165 |
166 | func (e *innerExecutorImpl) invokeCondition(node *innerNode, parentSpan *span, p *Condition) func() {
167 | return func() {
168 | span := span{extra: attr{
169 | typ: nodeCondition,
170 | name: node.name,
171 | }, begin: time.Now(), parent: parentSpan}
172 |
173 | defer func() {
174 | span.cost = time.Since(span.begin)
175 | if r := recover(); r != nil {
176 | node.g.canceled.Store(true)
177 | log.Printf("graph %v is canceled, since condition node %v panics", node.g.name, node.name)
178 | log.Printf("[recovered] condition node %s, panic: %v, stack: %s", node.name, r, debug.Stack())
179 | } else {
180 | e.profiler.AddSpan(&span) // remove canceled node span
181 | }
182 | node.drop()
183 | // e.sche_successors(node)
184 | node.g.deref()
185 | node.setup()
186 | e.wg.Done()
187 | }()
188 |
189 | if !node.g.canceled.Load() {
190 | node.state.Store(kNodeStateRunning)
191 |
192 | choice := p.handle()
193 | if choice > uint(len(p.mapper)) {
194 | panic(fmt.Sprintln("condition task failed, successors of condition should be more than precondition choice", p.handle()))
195 | }
196 | // do choice and cancel others
197 | node.state.Store(kNodeStateFinished)
198 | e.schedule(p.mapper[choice])
199 | }
200 | }
201 | }
202 |
203 | func (e *innerExecutorImpl) invokeNode(node *innerNode, parentSpan *span) {
204 | switch p := node.ptr.(type) {
205 | case *Static:
206 | e.pool.Go(e.invokeStatic(node, parentSpan, p))
207 | case *Subflow:
208 | e.pool.Go(e.invokeSubflow(node, parentSpan, p))
209 | case *Condition:
210 | e.pool.Go(e.invokeCondition(node, parentSpan, p))
211 | default:
212 | panic("unsupported node")
213 | }
214 | }
215 |
216 | func (e *innerExecutorImpl) pushIntoQueue(node *innerNode) {
217 | e.mu.Lock()
218 | defer e.mu.Unlock()
219 | e.wq.Put(node)
220 | }
221 |
222 | func (e *innerExecutorImpl) schedule(nodes ...*innerNode) {
223 | for _, node := range nodes {
224 | if node.g.canceled.Load() {
225 | // no need
226 | node.g.scheCond.L.Lock()
227 | node.g.scheCond.Signal()
228 | node.g.scheCond.L.Unlock()
229 | log.Printf("node %v is not scheduled, since graph %v is canceled\n", node.name, node.g.name)
230 | return
231 | }
232 | e.wg.Add(1)
233 | node.g.scheCond.L.Lock()
234 |
235 | node.g.ref()
236 | e.pushIntoQueue(node)
237 |
238 | node.g.scheCond.Signal()
239 | node.g.scheCond.L.Unlock()
240 | }
241 | }
242 |
243 | func (e *innerExecutorImpl) scheduleGraph(parentg, g *eGraph, parentSpan *span) {
244 | g.setup()
245 | slices.SortFunc(g.entries, func(i, j *innerNode) int {
246 | return cmp.Compare(i.priority, j.priority)
247 | })
248 | e.schedule(g.entries...)
249 | if !e.invokeGraph(g, parentSpan) && parentg != nil {
250 | parentg.canceled.Store(true)
251 | log.Printf("graph %s canceled, since subgraph %s is canceled\n", parentg.name, g.name)
252 | }
253 |
254 | g.scheCond.Signal()
255 | }
256 |
257 | // Wait: block until all tasks finished
258 | func (e *innerExecutorImpl) Wait() {
259 | e.wg.Wait()
260 | }
261 |
262 | // Profile write flame graph raw text into w
263 | func (e *innerExecutorImpl) Profile(w io.Writer) error {
264 | return e.profiler.draw(w)
265 | }
266 |
--------------------------------------------------------------------------------
/executor_test.go:
--------------------------------------------------------------------------------
1 | package gotaskflow_test
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "testing"
8 | "time"
9 |
10 | gotaskflow "github.com/noneback/go-taskflow"
11 | )
12 |
13 | func TestExecutor(t *testing.T) {
14 | executor := gotaskflow.NewExecutor(uint(runtime.NumCPU()))
15 | tf := gotaskflow.NewTaskFlow("G")
16 | A, B, C :=
17 | tf.NewTask("A", func() {
18 | fmt.Println("A")
19 | }),
20 | tf.NewTask("B", func() {
21 | fmt.Println("B")
22 | }),
23 | tf.NewTask("C", func() {
24 | fmt.Println("C")
25 | })
26 |
27 | A1, B1, _ :=
28 | tf.NewTask("A1", func() {
29 | fmt.Println("A1")
30 | }),
31 | tf.NewTask("B1", func() {
32 | fmt.Println("B1")
33 | }),
34 | tf.NewTask("C1", func() {
35 | fmt.Println("C1")
36 | })
37 | A.Precede(B)
38 | C.Precede(B)
39 | A1.Precede(B)
40 | C.Succeed(A1)
41 | C.Succeed(B1)
42 |
43 | executor.Run(tf).Wait()
44 | executor.Profile(os.Stdout)
45 | }
46 |
47 | func TestPanicInSubflow(t *testing.T) {
48 | executor := gotaskflow.NewExecutor(100000)
49 | tf := gotaskflow.NewTaskFlow("G")
50 | copy_gcc_source_file := tf.NewTask("copy_gcc_source_file", func() {
51 | time.Sleep(1 * time.Second)
52 | fmt.Println("copy_gcc_source_file")
53 | })
54 | tar_gcc_source_file := tf.NewTask("tar_gcc_source_file", func() {
55 | time.Sleep(1 * time.Second)
56 | fmt.Println("tar_gcc_source_file")
57 | })
58 | download_prerequisites := tf.NewSubflow("download_prerequisites", func(sf *gotaskflow.Subflow) {
59 | sf.NewTask("", func() {
60 | time.Sleep(1 * time.Second)
61 | fmt.Println("download_prerequisites")
62 | panic(1)
63 | })
64 | })
65 | yum_install_dependency_package := tf.NewTask("yum_install_dependency_package", func() {
66 | time.Sleep(1 * time.Second)
67 | fmt.Println("yum_install_dependency_package")
68 | })
69 | mkdir_and_prepare_build := tf.NewTask("mkdir_and_prepare_build", func() {
70 | time.Sleep(1 * time.Second)
71 | fmt.Println("mkdir_and_prepare_build")
72 | })
73 | make_build := tf.NewTask("make_build", func() {
74 | time.Sleep(1 * time.Second)
75 | fmt.Println("make_build")
76 | })
77 | make_install := tf.NewTask("make_install", func() {
78 | time.Sleep(1 * time.Second)
79 | fmt.Println("make_install")
80 | })
81 | relink := tf.NewTask("relink", func() {
82 | time.Sleep(1 * time.Second)
83 | fmt.Println("relink")
84 | })
85 | copy_gcc_source_file.Precede(tar_gcc_source_file)
86 | yum_install_dependency_package.Precede(download_prerequisites)
87 | tar_gcc_source_file.Precede(download_prerequisites)
88 | download_prerequisites.Precede(mkdir_and_prepare_build)
89 | mkdir_and_prepare_build.Precede(make_build)
90 | make_build.Precede(make_install)
91 | make_install.Precede(relink)
92 | executor.Run(tf).Wait()
93 | }
94 |
--------------------------------------------------------------------------------
/flow.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | var builder = flowBuilder{}
4 |
5 | type flowBuilder struct{}
6 |
7 | // Condition Wrapper
8 | type Condition struct {
9 | handle func() uint
10 | mapper map[uint]*innerNode
11 | }
12 |
13 | // Static Wrapper
14 | type Static struct {
15 | handle func()
16 | }
17 |
18 | // Subflow Wrapper
19 | type Subflow struct {
20 | handle func(sf *Subflow)
21 | g *eGraph
22 | }
23 |
24 | // Push pushs all tasks into subflow
25 | func (sf *Subflow) push(tasks ...*Task) {
26 | for _, task := range tasks {
27 | sf.g.push(task.node)
28 | }
29 | }
30 |
31 | func (tf *flowBuilder) NewStatic(name string, f func()) *innerNode {
32 | node := newNode(name)
33 | node.ptr = &Static{
34 | handle: f,
35 | }
36 | node.Typ = nodeStatic
37 | return node
38 | }
39 |
40 | func (fb *flowBuilder) NewSubflow(name string, f func(sf *Subflow)) *innerNode {
41 | node := newNode(name)
42 | node.ptr = &Subflow{
43 | handle: f,
44 | g: newGraph(name),
45 | }
46 | node.Typ = nodeSubflow
47 | return node
48 | }
49 |
50 | func (fb *flowBuilder) NewCondition(name string, f func() uint) *innerNode {
51 | node := newNode(name)
52 | node.ptr = &Condition{
53 | handle: f,
54 | mapper: make(map[uint]*innerNode),
55 | }
56 | node.Typ = nodeCondition
57 | return node
58 | }
59 |
60 | // NewStaticTask returns a static task
61 | func (sf *Subflow) NewTask(name string, f func()) *Task {
62 | task := &Task{
63 | node: builder.NewStatic(name, f),
64 | }
65 | sf.push(task)
66 | return task
67 | }
68 |
69 | // NewSubflow returns a subflow task
70 | func (sf *Subflow) NewSubflow(name string, f func(sf *Subflow)) *Task {
71 | task := &Task{
72 | node: builder.NewSubflow(name, f),
73 | }
74 | sf.push(task)
75 | return task
76 | }
77 |
78 | // NewCondition returns a condition task. The predict func return value determines its successor.
79 | func (sf *Subflow) NewCondition(name string, predict func() uint) *Task {
80 | task := &Task{
81 | node: builder.NewCondition(name, predict),
82 | }
83 | sf.push(task)
84 | return task
85 | }
86 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/noneback/go-taskflow
2 |
3 | go 1.21.6
4 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noneback/go-taskflow/51d5d70e0308a3ece6025de3ee6860a41a4d1866/go.sum
--------------------------------------------------------------------------------
/graph.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "sync"
5 | "sync/atomic"
6 | )
7 |
8 | type eGraph struct { // execution graph
9 | name string
10 | nodes []*innerNode
11 | joinCounter atomic.Int32
12 | entries []*innerNode
13 | scheCond *sync.Cond
14 | instantiated bool
15 | canceled atomic.Bool // only changes when task in graph panic
16 | }
17 |
18 | func newGraph(name string) *eGraph {
19 | return &eGraph{
20 | name: name,
21 | nodes: make([]*innerNode, 0),
22 | scheCond: sync.NewCond(&sync.Mutex{}),
23 | joinCounter: atomic.Int32{},
24 | }
25 | }
26 |
27 | func (g *eGraph) ref() {
28 | g.joinCounter.Add(1)
29 | }
30 |
31 | func (g *eGraph) deref() {
32 | g.scheCond.L.Lock()
33 | defer g.scheCond.L.Unlock()
34 | defer g.scheCond.Signal()
35 |
36 | g.joinCounter.Add(-1)
37 | }
38 |
39 | func (g *eGraph) reset() {
40 | g.joinCounter.Store(0)
41 | g.entries = g.entries[:0]
42 | for _, n := range g.nodes {
43 | n.joinCounter.Store(0)
44 | }
45 | }
46 |
47 | func (g *eGraph) push(n ...*innerNode) {
48 | g.nodes = append(g.nodes, n...)
49 | for _, node := range n {
50 | node.g = g
51 | }
52 | }
53 |
54 | func (g *eGraph) setup() {
55 | g.reset()
56 |
57 | for _, node := range g.nodes {
58 | node.setup()
59 |
60 | if len(node.dependents) == 0 {
61 | g.entries = append(g.entries, node)
62 | }
63 | }
64 | }
65 |
66 | func (g *eGraph) recyclable() bool {
67 | return g.joinCounter.Load() == 0
68 | }
69 |
--------------------------------------------------------------------------------
/image/condition.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/desc.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/fl.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/image/loop.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/simple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/image/subflow.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/node.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "strconv"
5 | "sync"
6 | "sync/atomic"
7 | "time"
8 | )
9 |
10 | const (
11 | kNodeStateIdle = int32(iota + 1)
12 | kNodeStateWaiting
13 | kNodeStateRunning
14 | kNodeStateFinished
15 | )
16 |
17 | type nodeType string
18 |
19 | const (
20 | nodeSubflow nodeType = "subflow" // subflow
21 | nodeStatic nodeType = "static" // static
22 | nodeCondition nodeType = "condition" // static
23 | )
24 |
25 | type innerNode struct {
26 | name string
27 | successors []*innerNode
28 | dependents []*innerNode
29 | Typ nodeType
30 | ptr interface{}
31 | mu *sync.Mutex
32 | state atomic.Int32
33 | joinCounter atomic.Int32
34 | g *eGraph
35 | priority TaskPriority
36 | }
37 |
38 | func (n *innerNode) recyclable() bool {
39 | return n.joinCounter.Load() == 0
40 | }
41 |
42 | func (n *innerNode) ref() {
43 | n.joinCounter.Add(1)
44 | }
45 |
46 | func (n *innerNode) deref() {
47 | n.joinCounter.Add(-1)
48 | }
49 |
50 | func (n *innerNode) setup() {
51 | n.mu.Lock()
52 | defer n.mu.Unlock()
53 | n.state.Store(kNodeStateIdle)
54 | for _, dep := range n.dependents {
55 | if dep.Typ == nodeCondition {
56 | continue
57 | }
58 |
59 | n.ref()
60 | }
61 | }
62 | func (n *innerNode) drop() {
63 | // release every deps
64 | for _, node := range n.successors {
65 | if n.Typ != nodeCondition {
66 | node.deref()
67 | }
68 | }
69 | }
70 |
71 | // set dependency: V deps on N, V is input node
72 | func (n *innerNode) precede(v *innerNode) {
73 | n.successors = append(n.successors, v)
74 | v.dependents = append(v.dependents, n)
75 | }
76 |
77 | func newNode(name string) *innerNode {
78 | if len(name) == 0 {
79 | name = "N_" + strconv.Itoa(time.Now().Nanosecond())
80 | }
81 | return &innerNode{
82 | name: name,
83 | state: atomic.Int32{},
84 | successors: make([]*innerNode, 0),
85 | dependents: make([]*innerNode, 0),
86 | mu: &sync.Mutex{},
87 | priority: NORMAL,
88 | joinCounter: atomic.Int32{},
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/profiler.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "sync"
7 | "time"
8 |
9 | "github.com/noneback/go-taskflow/utils"
10 | )
11 |
12 | type profiler struct {
13 | spans map[attr]*span
14 |
15 | mu *sync.Mutex
16 | }
17 |
18 | func newProfiler() *profiler {
19 | return &profiler{
20 | spans: make(map[attr]*span),
21 | mu: &sync.Mutex{},
22 | }
23 | }
24 |
25 | func (t *profiler) AddSpan(s *span) {
26 | t.mu.Lock()
27 | defer t.mu.Unlock()
28 |
29 | if span, ok := t.spans[s.extra]; ok {
30 | s.cost += span.cost
31 | }
32 | t.spans[s.extra] = s
33 | }
34 |
35 | type attr struct {
36 | typ nodeType
37 | name string
38 | }
39 |
40 | type span struct {
41 | extra attr
42 | begin time.Time
43 | cost time.Duration
44 | parent *span
45 | }
46 |
47 | func (s *span) String() string {
48 | return fmt.Sprintf("%s,%s,cost %v", s.extra.typ, s.extra.name, utils.NormalizeDuration(s.cost))
49 | }
50 |
51 | func (t *profiler) draw(w io.Writer) error {
52 | // compact spans base on name
53 | t.mu.Lock()
54 | defer t.mu.Unlock()
55 |
56 | for _, s := range t.spans {
57 | path := ""
58 | if s.extra.typ != nodeSubflow {
59 | path = s.String()
60 | cur := s
61 |
62 | for cur.parent != nil {
63 | path = cur.parent.String() + ";" + path
64 | cur = cur.parent
65 | }
66 | msg := fmt.Sprintf("%s %v\n", path, s.cost.Microseconds())
67 |
68 | if _, err := w.Write([]byte(msg)); err != nil {
69 | return fmt.Errorf("write profile -> %w", err)
70 | }
71 | }
72 |
73 | }
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/profiler_test.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | "time"
7 |
8 | "github.com/noneback/go-taskflow/utils"
9 | )
10 |
11 | func TestProfilerAddSpan(t *testing.T) {
12 | profiler := newProfiler()
13 | mark := attr{
14 | typ: nodeStatic,
15 | name: "test-span",
16 | }
17 | span := &span{
18 | extra: mark,
19 | begin: time.Now(),
20 | cost: 5 * time.Millisecond,
21 | }
22 | profiler.AddSpan(span)
23 |
24 | if len(profiler.spans) != 1 {
25 | t.Errorf("expected 1 span, got %d", len(profiler.spans))
26 | }
27 |
28 | if profiler.spans[mark] != span {
29 | t.Errorf("expected span to be added correctly, got %v", profiler.spans[mark])
30 | }
31 | }
32 |
33 | func TestSpanString(t *testing.T) {
34 | now := time.Now()
35 | span := &span{
36 | extra: attr{
37 | typ: nodeStatic,
38 | name: "test-span",
39 | },
40 | begin: now,
41 | cost: 10 * time.Millisecond,
42 | }
43 |
44 | expected := "static,test-span,cost " + utils.NormalizeDuration(10*time.Millisecond)
45 | actual := span.String()
46 |
47 | if actual != expected {
48 | t.Errorf("expected %s, got %s", expected, actual)
49 | }
50 | }
51 |
52 | func TestProfilerDraw(t *testing.T) {
53 | profiler := newProfiler()
54 | now := time.Now()
55 | parentSpan := &span{
56 | extra: attr{
57 | typ: nodeStatic,
58 | name: "parent",
59 | },
60 | begin: now,
61 | cost: 10 * time.Millisecond,
62 | }
63 |
64 | childSpan := &span{
65 | extra: attr{
66 | typ: nodeStatic,
67 | name: "child",
68 | },
69 | begin: now,
70 | cost: 5 * time.Millisecond,
71 | parent: parentSpan,
72 | }
73 |
74 | profiler.AddSpan(parentSpan)
75 | profiler.AddSpan(childSpan)
76 |
77 | var buf bytes.Buffer
78 | err := profiler.draw(&buf)
79 | if err != nil {
80 | t.Errorf("unexpected error: %v", err)
81 | }
82 |
83 | output := buf.String()
84 | if len(output) == 0 {
85 | t.Errorf("expected output, got empty string")
86 | }
87 |
88 | expectedOutput := "static,parent,cost 10ms 10000\nstatic,parent,cost 10ms;static,child,cost 5ms 5000\n"
89 | expectedOutput2 := "static,parent,cost 10ms;static,child,cost 5ms 5000\nstatic,parent,cost 10ms 10000\n"
90 | if output != expectedOutput && output != expectedOutput2 {
91 | t.Errorf("expected output: %v\ngot: %v", expectedOutput, output)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/task.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | // Basic component of Taskflow
4 | type Task struct {
5 | node *innerNode
6 | }
7 |
8 | // Precede: Tasks all depend on *this*.
9 | // In Addition, order of tasks is correspond to predict result, ranging from 0...len(tasks)
10 | func (t *Task) Precede(tasks ...*Task) {
11 | if cond, ok := t.node.ptr.(*Condition); ok {
12 | for i, task := range tasks {
13 | cond.mapper[uint(i)] = task.node
14 | }
15 | }
16 |
17 | for _, task := range tasks {
18 | t.node.precede(task.node)
19 | }
20 | }
21 |
22 | // Succeed: *this* deps on tasks
23 | func (t *Task) Succeed(tasks ...*Task) {
24 | for _, task := range tasks {
25 | task.node.precede(t.node)
26 | }
27 | }
28 |
29 | func (t *Task) Name() string {
30 | return t.node.name
31 | }
32 |
33 | // Priority sets task's sche priority. Noted that due to goroutine concurrent mode, it can only assure task schedule priority, rather than its execution.
34 | func (t *Task) Priority(p TaskPriority) *Task {
35 | t.node.priority = p
36 | return t
37 | }
38 |
39 | // Task sche priority
40 | type TaskPriority uint
41 |
42 | const (
43 | HIGH = TaskPriority(iota + 1)
44 | NORMAL
45 | LOW
46 | )
47 |
--------------------------------------------------------------------------------
/taskflow.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import "io"
4 |
5 | // TaskFlow represents a series of tasks
6 | type TaskFlow struct {
7 | graph *eGraph
8 | frozen bool
9 | }
10 |
11 | // Reset resets taskflow
12 | func (tf *TaskFlow) Reset() {
13 | // tf.graph.reset()
14 | tf.frozen = false
15 | }
16 |
17 | // NewTaskFlow returns a taskflow struct
18 | func NewTaskFlow(name string) *TaskFlow {
19 | return &TaskFlow{
20 | graph: newGraph(name),
21 | }
22 | }
23 |
24 | // Push pushs all task into taskflow
25 | func (tf *TaskFlow) push(tasks ...*Task) {
26 | if tf.frozen {
27 | panic("Taskflow is frozen, cannot new tasks")
28 | }
29 |
30 | for _, task := range tasks {
31 | tf.graph.push(task.node)
32 | }
33 | }
34 |
35 | func (tf *TaskFlow) Name() string {
36 | return tf.graph.name
37 | }
38 |
39 | // NewStaticTask returns a attached static task
40 | func (tf *TaskFlow) NewTask(name string, f func()) *Task {
41 | task := &Task{
42 | node: builder.NewStatic(name, f),
43 | }
44 | tf.push(task)
45 | return task
46 | }
47 |
48 | // NewSubflow returns a attached subflow task
49 | // NOTICE: instantiate will be invoke only once to instantiate itself
50 | func (tf *TaskFlow) NewSubflow(name string, instantiate func(sf *Subflow)) *Task {
51 | task := &Task{
52 | node: builder.NewSubflow(name, instantiate),
53 | }
54 | tf.push(task)
55 | return task
56 | }
57 |
58 | // NewCondition returns a attached condition task. NOTICE: The predict func return value determines its successor.
59 | func (tf *TaskFlow) NewCondition(name string, predict func() uint) *Task {
60 | task := &Task{
61 | node: builder.NewCondition(name, predict),
62 | }
63 | tf.push(task)
64 | return task
65 | }
66 |
67 | // Dump writes graph dot data into writer
68 | func (tf *TaskFlow) Dump(writer io.Writer) error {
69 | return dot.Visualize(tf, writer)
70 | }
71 |
--------------------------------------------------------------------------------
/taskflow_test.go:
--------------------------------------------------------------------------------
1 | package gotaskflow_test
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | _ "net/http/pprof"
7 | "os"
8 | "sync/atomic"
9 | "testing"
10 | "time"
11 |
12 | gotaskflow "github.com/noneback/go-taskflow"
13 | "github.com/noneback/go-taskflow/utils"
14 | )
15 |
16 | type rgChain[R comparable] struct {
17 | rgs []*rgroup[R]
18 | }
19 |
20 | func newRgChain[R comparable]() *rgChain[R] {
21 | return &rgChain[R]{
22 | rgs: make([]*rgroup[R], 0),
23 | }
24 | }
25 |
26 | func (c *rgChain[R]) grouping(rs ...R) {
27 | g := newRg[R]()
28 | g.push(rs...)
29 | c.rgs = append(c.rgs, g)
30 | }
31 |
32 | // result group
33 | type rgroup[R comparable] struct {
34 | pre, next *rgroup[R]
35 | elems map[R]struct{}
36 | }
37 |
38 | func newRg[R comparable]() *rgroup[R] {
39 | return &rgroup[R]{
40 | elems: make(map[R]struct{}),
41 | }
42 | }
43 |
44 | func (g *rgroup[R]) push(rs ...R) {
45 | for _, r := range rs {
46 | g.elems[r] = struct{}{}
47 | }
48 | }
49 |
50 | func (g *rgroup[R]) chain(successor *rgroup[R]) {
51 | g.next = successor
52 | successor.pre = g.next
53 | }
54 |
55 | func (g *rgroup[R]) contains(r R) bool {
56 | _, ok := g.elems[r]
57 | return ok
58 | }
59 |
60 | func checkTopology[R comparable](t *testing.T, q *utils.Queue[R], chain *rgChain[R]) {
61 | for _, g := range chain.rgs {
62 | for len(g.elems) != 0 {
63 | node := q.Pop()
64 | if g.contains(node) {
65 | delete(g.elems, node)
66 | } else {
67 | fmt.Println("failed in", node)
68 | t.Fail()
69 | }
70 | }
71 | }
72 | }
73 |
74 | var executor = gotaskflow.NewExecutor(10)
75 |
76 | func TestTaskFlow(t *testing.T) {
77 | q := utils.NewQueue[string](true)
78 | tf := gotaskflow.NewTaskFlow("G")
79 | A, B, C :=
80 | tf.NewTask("A", func() {
81 | fmt.Println("A")
82 | q.Put("A")
83 | }),
84 | tf.NewTask("B", func() {
85 | fmt.Println("B")
86 | q.Put("B")
87 | }),
88 | tf.NewTask("C", func() {
89 | fmt.Println("C")
90 | q.Put("C")
91 | })
92 |
93 | A1, B1, _ :=
94 | tf.NewTask("A1", func() {
95 | fmt.Println("A1")
96 | q.Put("A1")
97 | }),
98 | tf.NewTask("B1", func() {
99 | fmt.Println("B1")
100 | q.Put("B1")
101 | }),
102 | tf.NewTask("C1", func() {
103 | fmt.Println("C1")
104 | q.Put("C1")
105 | })
106 | chains := newRgChain[string]()
107 | chains.grouping("C1", "A1", "B1", "A", "C")
108 | chains.grouping("B")
109 |
110 | A.Precede(B)
111 | C.Precede(B)
112 | A1.Precede(B)
113 | C.Succeed(A1)
114 | C.Succeed(B1)
115 |
116 | t.Run("TestViz", func(t *testing.T) {
117 | if err := tf.Dump(os.Stdout); err != nil {
118 | panic(err)
119 | }
120 | })
121 |
122 | executor.Run(tf).Wait()
123 | if q.Len() != 6 {
124 | t.Fail()
125 | }
126 |
127 | // checkTopology(t, q, chains)
128 | }
129 |
130 | func TestSubflow(t *testing.T) {
131 | q := utils.NewQueue[string](true)
132 | // chains := newRgChain[string]()
133 | tf := gotaskflow.NewTaskFlow("G")
134 | A, B, C :=
135 | tf.NewTask("A", func() {
136 | fmt.Println("A")
137 | q.Put("A")
138 | }),
139 | tf.NewTask("B", func() {
140 | fmt.Println("B")
141 | q.Put("B")
142 | }),
143 | tf.NewTask("C", func() {
144 | fmt.Println("C")
145 | q.Put("C")
146 | })
147 |
148 | A1, B1, C1 :=
149 | tf.NewTask("A1", func() {
150 | fmt.Println("A1")
151 | q.Put("A1")
152 | }),
153 | tf.NewTask("B1", func() {
154 | fmt.Println("B1")
155 | q.Put("B1")
156 | }),
157 | tf.NewTask("C1", func() {
158 | fmt.Println("C1")
159 | q.Put("C1")
160 | })
161 | A.Precede(B)
162 | C.Precede(B)
163 | C.Succeed(A1)
164 | C.Succeed(B1)
165 |
166 | subflow := tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
167 | A2, B2, C2 :=
168 | sf.NewTask("A2", func() {
169 | fmt.Println("A2")
170 | q.Put("A2")
171 | }),
172 | sf.NewTask("B2", func() {
173 | fmt.Println("B2")
174 | q.Put("B2")
175 | }),
176 | sf.NewTask("C2", func() {
177 | fmt.Println("C2")
178 | q.Put("C2")
179 | })
180 | A2.Precede(B2)
181 | C2.Precede(B2)
182 | cond := sf.NewCondition("cond", func() uint {
183 | return 0
184 | })
185 |
186 | ssub := sf.NewSubflow("sub in sub", func(sf *gotaskflow.Subflow) {
187 | sf.NewTask("done", func() {
188 | fmt.Println("done")
189 | })
190 | })
191 |
192 | cond.Precede(ssub, cond)
193 |
194 | })
195 |
196 | subflow2 := tf.NewSubflow("sub2", func(sf *gotaskflow.Subflow) {
197 | A3, B3, C3 :=
198 | sf.NewTask("A3", func() {
199 | fmt.Println("A3")
200 | q.Put("A3")
201 | }),
202 | sf.NewTask("B3", func() {
203 | fmt.Println("B3")
204 | q.Put("B3")
205 | }),
206 | sf.NewTask("C3", func() {
207 | fmt.Println("C3")
208 | q.Put("C3")
209 | // time.Sleep(10 * time.Second)
210 | })
211 | A3.Precede(B3)
212 | C3.Precede(B3)
213 |
214 | })
215 |
216 | subflow.Precede(subflow2)
217 | C1.Precede(subflow)
218 | C1.Succeed(C)
219 |
220 | executor.Run(tf)
221 | executor.Wait()
222 | if err := tf.Dump(os.Stdout); err != nil {
223 | log.Fatal(err)
224 | }
225 | executor.Profile(os.Stdout)
226 |
227 | chain := newRgChain[string]()
228 |
229 | // Group 1 - Top-level nodes
230 | chain.grouping("A1", "B1", "A")
231 | chain.grouping("C")
232 | chain.grouping("B", "C1")
233 | chain.grouping("A2", "C2")
234 | chain.grouping("B2")
235 |
236 | // Group 2 - Connections under A, B, C
237 | chain.grouping("A3", "C3")
238 | chain.grouping("B3")
239 |
240 | // validate
241 | if q.Len() != 12 {
242 | t.Fail()
243 | }
244 | // checkTopology(t, q, chain)
245 | }
246 |
247 | // ERROR robust testing
248 | func TestTaskflowPanic(t *testing.T) {
249 | tf := gotaskflow.NewTaskFlow("G")
250 | A, B, C :=
251 | tf.NewTask("A", func() {
252 | fmt.Println("A")
253 | }),
254 | tf.NewTask("B", func() {
255 | fmt.Println("B")
256 | }),
257 | tf.NewTask("C", func() {
258 | fmt.Println("C")
259 | panic("panic C")
260 | })
261 | A.Precede(B)
262 | C.Precede(B)
263 |
264 | executor.Run(tf).Wait()
265 | }
266 |
267 | func TestSubflowPanic(t *testing.T) {
268 | tf := gotaskflow.NewTaskFlow("G")
269 | A, B, C :=
270 | tf.NewTask("A", func() {
271 | fmt.Println("A")
272 | }),
273 | tf.NewTask("B", func() {
274 | fmt.Println("B")
275 | }),
276 | tf.NewTask("C", func() {
277 | fmt.Println("C")
278 | })
279 | A.Precede(B)
280 | C.Precede(B)
281 |
282 | subflow := tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
283 | A2, B2, C2 :=
284 | tf.NewTask("A2", func() {
285 | fmt.Println("A2")
286 | time.Sleep(1 * time.Second)
287 | }),
288 | tf.NewTask("B2", func() {
289 | fmt.Println("B2")
290 | }),
291 | tf.NewTask("C2", func() {
292 | fmt.Println("C2")
293 | panic("C2 panicked")
294 | })
295 | A2.Precede(B2)
296 | panic("subflow panic")
297 | C2.Precede(B2)
298 | })
299 |
300 | subflow.Precede(B)
301 | executor.Run(tf)
302 | executor.Wait()
303 | if err := tf.Dump(os.Stdout); err != nil {
304 | fmt.Errorf("%v", err)
305 | }
306 | executor.Profile(os.Stdout)
307 | }
308 |
309 | func TestTaskflowCondition(t *testing.T) {
310 | q := utils.NewQueue[string](true)
311 | chain := newRgChain[string]()
312 | tf := gotaskflow.NewTaskFlow("G")
313 | t.Run("normal", func(t *testing.T) {
314 | A, B, C :=
315 | tf.NewTask("A", func() {
316 | fmt.Println("A")
317 | q.Put("A")
318 | }),
319 | tf.NewTask("B", func() {
320 | fmt.Println("B")
321 | q.Put("B")
322 | }),
323 | tf.NewTask("C", func() {
324 | fmt.Println("C")
325 | q.Put("C")
326 | })
327 | A.Precede(B)
328 | C.Precede(B)
329 |
330 | fail, success := tf.NewTask("failed", func() {
331 | fmt.Println("Failed")
332 | q.Put("failed")
333 | t.Fail()
334 | }), tf.NewTask("success", func() {
335 | fmt.Println("success")
336 | q.Put("success")
337 | })
338 |
339 | cond := tf.NewCondition("cond", func() uint {
340 | q.Put("cond")
341 | return 0
342 | })
343 | B.Precede(cond)
344 | cond.Precede(success, fail)
345 |
346 | suc := tf.NewSubflow("sub1", func(sf *gotaskflow.Subflow) {
347 | A2, B2, C2 :=
348 | sf.NewTask("A2", func() {
349 | fmt.Println("A2")
350 | q.Put("A2")
351 | }),
352 | sf.NewTask("B2", func() {
353 | fmt.Println("B2")
354 | q.Put("B2")
355 | }),
356 | sf.NewTask("C2", func() {
357 | fmt.Println("C2")
358 | q.Put("C2")
359 | })
360 | A2.Precede(B2)
361 | C2.Precede(B2)
362 | }).Priority(gotaskflow.HIGH)
363 | fs := tf.NewTask("fail_single", func() {
364 | fmt.Println("it should be canceled")
365 | q.Put("fail_single")
366 | })
367 | fail.Precede(fs, suc)
368 | // success.Precede(suc)
369 | if err := tf.Dump(os.Stdout); err != nil {
370 | fmt.Errorf("%v", err)
371 | }
372 | executor.Run(tf).Wait()
373 |
374 | executor.Profile(os.Stdout)
375 | chain.grouping("A", "C")
376 | chain.grouping("B")
377 | chain.grouping("cond")
378 | chain.grouping("success")
379 |
380 | checkTopology(t, q, chain)
381 |
382 | })
383 |
384 | t.Run("start with condition node", func(t *testing.T) {
385 | i := 0
386 | tf := gotaskflow.NewTaskFlow("G")
387 |
388 | cond := tf.NewCondition("cond", func() uint {
389 | if i == 0 {
390 | return 0
391 | } else {
392 | return 1
393 | }
394 | })
395 |
396 | zero, one := tf.NewTask("zero", func() {
397 | fmt.Println("zero")
398 | }), tf.NewTask("one", func() {
399 | fmt.Println("one")
400 | })
401 | cond.Precede(zero, one)
402 |
403 | executor.Run(tf).Wait()
404 |
405 | if err := tf.Dump(os.Stdout); err != nil {
406 | log.Fatal(err)
407 | }
408 | executor.Profile(os.Stdout)
409 |
410 | })
411 |
412 | }
413 |
414 | func TestTaskflowLoop(t *testing.T) {
415 | t.Run("normal", func(t *testing.T) {
416 | i := 0
417 | tf := gotaskflow.NewTaskFlow("G")
418 | init, cond, body, back, done :=
419 | tf.NewTask("init", func() {
420 | i = 0
421 | fmt.Println("i=0")
422 | }),
423 | tf.NewCondition("while i < 5", func() uint {
424 | if i < 5 {
425 | return 0
426 | } else {
427 | return 1
428 | }
429 | }),
430 | tf.NewTask("body", func() {
431 | i += 1
432 | fmt.Println("i++ =", i)
433 | }),
434 | tf.NewCondition("back", func() uint {
435 | fmt.Println("back")
436 | return 0
437 | }),
438 | tf.NewTask("done", func() {
439 | fmt.Println("done")
440 | })
441 |
442 | init.Precede(cond)
443 | cond.Precede(body, done)
444 | body.Precede(back)
445 | back.Precede(cond)
446 | if err := tf.Dump(os.Stdout); err != nil {
447 | // log.Fatal(err)
448 | }
449 | executor.Run(tf).Wait()
450 | if i < 5 {
451 | t.Fail()
452 | }
453 |
454 | executor.Profile(os.Stdout)
455 | })
456 |
457 | t.Run("simple loop", func(t *testing.T) {
458 | i := 0
459 | tf := gotaskflow.NewTaskFlow("G")
460 | init := tf.NewTask("init", func() {
461 | i = 0
462 | })
463 | cond := tf.NewCondition("cond", func() uint {
464 | i++
465 | fmt.Println("i++ =", i)
466 | if i > 2 {
467 | return 0
468 | } else {
469 | return 1
470 | }
471 | })
472 |
473 | done := tf.NewTask("done", func() {
474 | fmt.Println("done")
475 | })
476 |
477 | init.Precede(cond)
478 | cond.Precede(done, cond)
479 |
480 | executor.Run(tf).Wait()
481 | if i <= 2 {
482 | t.Fail()
483 | }
484 |
485 | if err := tf.Dump(os.Stdout); err != nil {
486 | // log.Fatal(err)
487 | }
488 | executor.Profile(os.Stdout)
489 | })
490 | }
491 |
492 | func TestTaskflowPriority(t *testing.T) {
493 | executor := gotaskflow.NewExecutor(uint(1))
494 | q := utils.NewQueue[byte](true)
495 | tf := gotaskflow.NewTaskFlow("G")
496 | tf.NewTask("B", func() {
497 | fmt.Println("B")
498 | q.Put('B')
499 | }).Priority(gotaskflow.NORMAL)
500 |
501 | tf.NewTask("C", func() {
502 | fmt.Println("C")
503 | q.Put('C')
504 | }).Priority(gotaskflow.HIGH)
505 | A := tf.NewTask("A", func() {
506 | fmt.Println("A")
507 | q.Put('A')
508 | }).Priority(gotaskflow.LOW)
509 |
510 | A.Precede(tf.NewTask("A2", func() {
511 | fmt.Println("A2")
512 | q.Put('a')
513 | }).Priority(gotaskflow.LOW),
514 | tf.NewTask("B2", func() {
515 | fmt.Println("B2")
516 | q.Put('b')
517 | }).Priority(gotaskflow.HIGH),
518 | tf.NewTask("C2", func() {
519 | fmt.Println("C2")
520 | q.Put('c')
521 | }).Priority(gotaskflow.NORMAL))
522 |
523 | executor.Run(tf).Wait()
524 | tf.Dump(os.Stdout)
525 | fmt.Println("validate")
526 | for _, val := range []byte{'C', 'B', 'A', 'b', 'c', 'a'} {
527 | real := q.Pop()
528 | fmt.Printf("%c, ", real)
529 | if val != real {
530 | t.Fatal("[FAILED]", string(val), string(real))
531 | t.FailNow()
532 | }
533 | }
534 | }
535 |
536 | func TestTaskflowNotInFlow(t *testing.T) {
537 | tf := gotaskflow.NewTaskFlow("tf")
538 | task := tf.NewTask("init", func() {
539 | fmt.Println("task init")
540 | })
541 | cnt := 0
542 | for i := 0; i < 10; i++ {
543 | task.Precede(tf.NewTask("test", func() {
544 | fmt.Println(cnt)
545 | cnt++
546 | }))
547 | }
548 |
549 | executor.Run(tf).Wait()
550 | }
551 |
552 | func TestTaskflowFrozen(t *testing.T) {
553 | tf := gotaskflow.NewTaskFlow("G")
554 | A, B, C :=
555 | tf.NewTask("A", func() {
556 | fmt.Println("A")
557 | }),
558 | tf.NewTask("B", func() {
559 | fmt.Println("B")
560 | }),
561 | tf.NewTask("C", func() {
562 | fmt.Println("C")
563 | })
564 | A.Precede(B)
565 | C.Precede(B)
566 |
567 | executor.Run(tf).Wait()
568 | utils.AssertPanics(t, "frozen", func() {
569 | tf.NewTask("tt", func() {
570 | fmt.Println("should not")
571 | })
572 | })
573 | }
574 |
575 | func TestLoopRunManyTimes(t *testing.T) {
576 | tf := gotaskflow.NewTaskFlow("G")
577 | var count atomic.Int32
578 | // add := func() {
579 | // count.Add(1)
580 | // }
581 | add := func(name string) func() {
582 | return func() {
583 | fmt.Println(name)
584 | count.Add(1)
585 | }
586 | }
587 | A, B, C :=
588 | tf.NewTask("A", add("A")),
589 | tf.NewTask("B", add("B")),
590 | tf.NewTask("C", add("C"))
591 | A.Precede(B)
592 | C.Precede(B)
593 | t.Run("static", func(t *testing.T) {
594 | for i := 0; i < 10000; i++ {
595 | log.Println("static iter ---> ", i)
596 | if cnt := count.Load(); cnt%3 != 0 {
597 | t.Error("static unexpected count", cnt)
598 | return
599 | }
600 | executor.Run(tf).Wait()
601 | }
602 | })
603 | // tf.Dump(os.Stdout)
604 |
605 | tf.Reset()
606 | count.Store(0)
607 |
608 | sf := tf.NewSubflow("sub", func(sf *gotaskflow.Subflow) {
609 | fmt.Println("sub")
610 | A1, B1, C1 :=
611 | sf.NewTask("A1", add("A1")),
612 | sf.NewTask("B1", add("B1")),
613 | sf.NewTask("C1", add("C1"))
614 | A1.Precede(B1)
615 | C1.Precede(B1)
616 | })
617 | additional := tf.NewTask("additional", add("Additional"))
618 | B.Precede(sf)
619 | additional.Precede(sf)
620 | executor.Run(tf).Wait()
621 | // dot, _ := os.OpenFile("./dot.data", os.O_RDWR, os.ModeAppend)
622 |
623 | tf.Dump(os.Stdout)
624 | t.Run("subflow", func(t *testing.T) {
625 | for i := 0; i < 10000; i++ {
626 | log.Println("subflow iter ---> ", i)
627 | if cnt := count.Load(); cnt%7 != 0 {
628 | t.Error("subflow unexpected count", cnt)
629 | return
630 | }
631 | executor.Run(tf).Wait()
632 | }
633 | })
634 |
635 | tf.Reset()
636 | count.Store(0)
637 |
638 | cond := tf.NewCondition("if count %10 % 7 == 0", func() uint {
639 | if count.Load()%7 == 0 {
640 | return 0
641 | } else {
642 | return 1
643 | }
644 | })
645 | plus7 := tf.NewTask("7 plus 7", func() {
646 | count.Add(7)
647 | })
648 | cond.Precede(plus7, tf.NewTask("7 minus 3", func() {
649 | log.Println(count.Load())
650 | log.Println("should not minus 3")
651 | }))
652 | sf.Precede(cond)
653 |
654 | t.Run("condition", func(t *testing.T) {
655 | for i := 0; i < 10000; i++ {
656 | log.Println("condition iter ---> ", i)
657 | if cnt := count.Load(); cnt%7 != 0 {
658 | t.Error("condition unexpect count", cnt)
659 | return
660 | }
661 | executor.Run(tf).Wait()
662 | }
663 | })
664 |
665 | tf.Reset()
666 | count.Store(0)
667 | cond2 := tf.NewCondition("bigger than 10000", func() uint {
668 | if count.Load() > 10000 {
669 | return 1
670 | } else {
671 | return 0
672 | }
673 | })
674 |
675 | plus7.Precede(cond2)
676 | done := tf.NewTask("done", func() {
677 | if count.Load() < 10000 {
678 | t.Fail()
679 | }
680 | })
681 |
682 | new_plus7 := tf.NewTask("new plus 7", func() {
683 | count.Add(7)
684 | })
685 | cond2.Precede(new_plus7, done)
686 | new_plus7.Precede(cond2)
687 |
688 | tf.Dump(os.Stdout)
689 | t.Run("loop", func(t *testing.T) {
690 | for i := 0; i < 10000; i++ {
691 | log.Println("loop iter ---> ", i)
692 | if cnt := count.Load(); cnt%7 != 0 {
693 | log.Println(cnt)
694 | t.Error("loop unexpect count", cnt)
695 | return
696 | }
697 | executor.Run(tf).Wait()
698 | }
699 | })
700 | }
701 |
702 | func TestSequencialTaskingPanic(t *testing.T) {
703 | exe := gotaskflow.NewExecutor(1)
704 | tfl := gotaskflow.NewTaskFlow("test")
705 | q := utils.NewQueue[string](true)
706 | tfl.NewTask("task1", func() {
707 | q.Put("panic")
708 | fmt.Println("task1")
709 | panic(1)
710 | })
711 | tfl.NewTask("task2", func() {
712 | q.Put("2")
713 | fmt.Println("task2")
714 | })
715 | tfl.NewTask("task3", func() {
716 | q.Put("3")
717 | fmt.Println("task3")
718 | })
719 | exe.Run(tfl).Wait()
720 | if q.Top() != "panic" {
721 | t.Fail()
722 | }
723 | }
724 |
--------------------------------------------------------------------------------
/utils/copool.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "runtime/debug"
8 | "sync"
9 | "sync/atomic"
10 | )
11 |
12 | type cotask struct {
13 | ctx *context.Context
14 | f func()
15 | }
16 |
17 | func (ct *cotask) zero() {
18 | ct.ctx = nil
19 | ct.f = nil
20 | }
21 |
22 | type Copool struct {
23 | panicHandler func(*context.Context, interface{})
24 | cap uint
25 | taskQ *Queue[*cotask]
26 | corun atomic.Int32
27 | coworker atomic.Int32
28 | mu *sync.Mutex
29 | taskObjPool *ObjectPool[*cotask]
30 | }
31 |
32 | // NewCopool return a goroutine pool with specified cap
33 | func NewCopool(cap uint) *Copool {
34 | return &Copool{
35 | panicHandler: nil,
36 | taskQ: NewQueue[*cotask](false),
37 | cap: cap,
38 | corun: atomic.Int32{},
39 | coworker: atomic.Int32{},
40 | mu: &sync.Mutex{},
41 | taskObjPool: NewObjectPool(func() *cotask {
42 | return &cotask{}
43 | }),
44 | }
45 | }
46 |
47 | // Go executes f.
48 | func (cp *Copool) Go(f func()) {
49 | ctx := context.Background()
50 | cp.CtxGo(&ctx, f)
51 | }
52 |
53 | // CtxGo executes f and accepts the context.
54 | func (cp *Copool) CtxGo(ctx *context.Context, f func()) {
55 | cp.corun.Add(1)
56 | task := cp.taskObjPool.Get()
57 | task.f = func() {
58 | defer func() {
59 | if r := recover(); r != nil {
60 | if cp.panicHandler != nil {
61 | cp.panicHandler(ctx, r)
62 | } else {
63 | msg := fmt.Sprintf("[panic] copool: %v: %s", r, debug.Stack())
64 | fmt.Println(msg)
65 | os.Exit(-1)
66 | }
67 | }
68 | }()
69 | defer cp.corun.Add(-1)
70 | f()
71 | }
72 |
73 | task.ctx = ctx
74 | cp.mu.Lock()
75 | cp.taskQ.Put(task)
76 |
77 | if cp.coworker.Load() == 0 || cp.taskQ.Len() != 0 && uint(cp.coworker.Load()) < uint(cp.cap) {
78 | cp.mu.Unlock()
79 | cp.coworker.Add(1)
80 |
81 | go func() {
82 | defer cp.coworker.Add(-1)
83 |
84 | for {
85 | cp.mu.Lock()
86 | if cp.taskQ.Len() == 0 {
87 | cp.mu.Unlock()
88 | return
89 | }
90 |
91 | task := cp.taskQ.Pop()
92 | cp.mu.Unlock()
93 | task.f()
94 | task.zero()
95 | cp.taskObjPool.Put(task)
96 | }
97 |
98 | }()
99 | } else {
100 | cp.mu.Unlock()
101 | }
102 | }
103 |
104 | // SetPanicHandler sets the panic handler.
105 | func (cp *Copool) SetPanicHandler(f func(*context.Context, interface{})) *Copool {
106 | cp.panicHandler = f
107 | return cp
108 | }
109 |
--------------------------------------------------------------------------------
/utils/copool_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 | "sync/atomic"
8 | "testing"
9 | "time"
10 | )
11 |
12 | const benchmarkTimes = 100000
13 |
14 | func DoCopyStack(a, b int) int {
15 | if b < 100 {
16 | return DoCopyStack(0, b+1)
17 | }
18 | return 0
19 | }
20 |
21 | func testFunc() {
22 | DoCopyStack(0, 0)
23 | }
24 |
25 | func TestPool(t *testing.T) {
26 | p := NewCopool(10000)
27 | var n int32
28 | var wg sync.WaitGroup
29 | for i := 0; i < 2000; i++ {
30 | // fmt.Print(i)
31 | wg.Add(1)
32 | p.Go(func() {
33 | defer wg.Done()
34 | atomic.AddInt32(&n, 1)
35 | })
36 | }
37 | wg.Wait()
38 | if n != 2000 {
39 | t.Error(n)
40 | }
41 | }
42 |
43 | func testPanic() {
44 | n := 0
45 | fmt.Println(1 / n)
46 | }
47 |
48 | func TestPoolPanic(t *testing.T) {
49 | p := NewCopool(10000)
50 | p.SetPanicHandler(func(ctx *context.Context, i interface{}) {
51 | fmt.Sprintln(i)
52 | })
53 | var wg sync.WaitGroup
54 | p.Go(testPanic)
55 | wg.Wait()
56 | time.Sleep(time.Second)
57 | }
58 |
59 | func TestPoolSequentialExec(t *testing.T) {
60 | p := NewCopool(1)
61 | q := make([]int, 0, 10000)
62 | // mutex := &sync.Mutex{}
63 | idx := 0
64 |
65 | for i := 0; i < 10000; i++ {
66 | p.Go(func() {
67 | q = append(q, idx)
68 | idx++
69 | })
70 | }
71 | time.Sleep(1 * time.Second)
72 |
73 | fmt.Println("len", len(q))
74 |
75 | for idx, v := range q {
76 | if idx != v {
77 | fmt.Println(idx, v)
78 | t.Fail()
79 | }
80 | }
81 |
82 | }
83 |
84 | func BenchmarkCopool(b *testing.B) {
85 | p := NewCopool(10000)
86 | var wg sync.WaitGroup
87 | b.ReportAllocs()
88 | b.ResetTimer()
89 | for i := 0; i < b.N; i++ {
90 | wg.Add(benchmarkTimes)
91 | for j := 0; j < benchmarkTimes; j++ {
92 | p.Go(func() {
93 | testFunc()
94 | wg.Done()
95 | })
96 | }
97 | wg.Wait()
98 | }
99 | }
100 | func BenchmarkGo(b *testing.B) {
101 | var wg sync.WaitGroup
102 | b.ReportAllocs()
103 | b.ResetTimer()
104 | for i := 0; i < b.N; i++ {
105 | wg.Add(benchmarkTimes)
106 | for j := 0; j < benchmarkTimes; j++ {
107 | go func() {
108 | testFunc()
109 | wg.Done()
110 | }()
111 | }
112 | wg.Wait()
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/utils/obj_pool.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "sync"
4 |
5 | // ObjectPool with Type
6 | type ObjectPool[T any] struct {
7 | pool sync.Pool
8 | }
9 |
10 | func NewObjectPool[T any](creator func() T) *ObjectPool[T] {
11 | return &ObjectPool[T]{
12 | pool: sync.Pool{
13 | New: func() any { return creator() },
14 | },
15 | }
16 | }
17 |
18 | func (p *ObjectPool[T]) Get() T {
19 | return p.pool.Get().(T)
20 | }
21 |
22 | func (p *ObjectPool[T]) Put(x T) {
23 | p.pool.Put(x)
24 | }
25 |
--------------------------------------------------------------------------------
/utils/pprof.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "runtime/pprof"
6 | )
7 |
8 | type ProfileType int
9 |
10 | type PprofUtils struct {
11 | f *os.File
12 | profile ProfileType
13 | }
14 |
15 | func NewPprofUtils(profile ProfileType, output string) *PprofUtils {
16 | p := &PprofUtils{}
17 | f, err := os.Create(output)
18 | if err != nil {
19 | panic(err)
20 | }
21 | p.f = f
22 | p.profile = profile
23 |
24 | return p
25 | }
26 |
27 | const (
28 | CPU ProfileType = iota
29 | HEAP
30 | )
31 |
32 | func (p *PprofUtils) StartProfile() {
33 | switch p.profile {
34 | case CPU:
35 | if err := pprof.StartCPUProfile(p.f); err != nil {
36 | panic(err)
37 | }
38 | case HEAP:
39 | if err := pprof.WriteHeapProfile(p.f); err != nil {
40 | panic(err)
41 | }
42 | default:
43 | panic("unsupported profile type")
44 | }
45 |
46 | }
47 |
48 | func (p *PprofUtils) StopProfile() {
49 | defer p.f.Close()
50 |
51 | switch p.profile {
52 | case CPU:
53 | pprof.StopCPUProfile()
54 | case HEAP:
55 | default:
56 | panic("unsupported profile type")
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/utils/pprof_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 | )
8 |
9 | func TestCPUProfile(t *testing.T) {
10 | tmpFile, err := os.CreateTemp("", "cpu_profile.*.prof")
11 | if err != nil {
12 | t.Fatalf("Failed to create temp file: %v", err)
13 | }
14 | defer os.Remove(tmpFile.Name())
15 | tmpFile.Close()
16 |
17 | profiler := NewPprofUtils(CPU, tmpFile.Name())
18 | defer profiler.StopProfile()
19 |
20 | profiler.StartProfile()
21 |
22 | doCPUWork()
23 | }
24 |
25 | func TestHeapProfile(t *testing.T) {
26 | tmpFile, err := ioutil.TempFile("", "heap_profile.*.prof")
27 | if err != nil {
28 | t.Fatalf("Failed to create temp file: %v", err)
29 | }
30 | defer os.Remove(tmpFile.Name())
31 | tmpFile.Close()
32 |
33 | profiler := NewPprofUtils(HEAP, tmpFile.Name())
34 | defer profiler.StopProfile()
35 | profiler.StartProfile()
36 |
37 | doMemoryWork()
38 | }
39 |
40 | func TestInvalidProfileType(t *testing.T) {
41 | defer func() {
42 | if r := recover(); r == nil {
43 | t.Error("Expected panic for invalid profile type, but got none")
44 | }
45 | }()
46 |
47 | p := NewPprofUtils(ProfileType(99), "invalid.prof")
48 | p.StartProfile()
49 | defer p.StopProfile()
50 | }
51 |
52 | func TestFileCreateError(t *testing.T) {
53 | defer func() {
54 | if r := recover(); r == nil {
55 | t.Error("Expected panic when file creation fails, but got none")
56 | }
57 | }()
58 |
59 | _ = NewPprofUtils(CPU, "/nonexistent/cpu.prof")
60 | }
61 |
62 | func doCPUWork() {
63 | for i := 0; i < 1e6; i++ {
64 | _ = i * i
65 | }
66 | }
67 |
68 | func doMemoryWork() {
69 | data := make([]byte, 10<<20)
70 | _ = data
71 | }
72 |
--------------------------------------------------------------------------------
/utils/queue.go:
--------------------------------------------------------------------------------
1 | // NOTE: CODE BASE IS COPIED FROM https://github.com/eapache/queue/blob/main/v2/queue.go, modified to make it thread safe
2 |
3 | package utils
4 |
5 | import (
6 | "sync"
7 | )
8 |
9 | // minQueueLen is smallest capacity that queue may have.
10 | // Must be power of 2 for bitwise modulus: x % n == x & (n - 1).
11 | const minQueueLen = 16
12 |
13 | // Queue represents a single instance of the queue data structure.
14 | type Queue[V any] struct {
15 | buf []*V
16 | head, tail, count int
17 | rw *sync.RWMutex
18 | tsafe bool
19 | }
20 |
21 | // New constructs and returns a new Queue.
22 | func NewQueue[V any](threadSafe bool) *Queue[V] {
23 | return &Queue[V]{
24 | buf: make([]*V, minQueueLen),
25 | rw: &sync.RWMutex{},
26 | tsafe: threadSafe,
27 | }
28 | }
29 |
30 | // Length returns the number of elements currently stored in the queue.
31 | func (q *Queue[V]) Len() int {
32 | if q.tsafe {
33 | q.rw.RLock()
34 | defer q.rw.RUnlock()
35 | }
36 |
37 | return q.count
38 | }
39 |
40 | // resizes the queue to fit exactly twice its current contents
41 | // this can result in shrinking if the queue is less than half-full
42 | func (q *Queue[V]) resize() {
43 | newBuf := make([]*V, q.count<<1)
44 |
45 | if q.tail > q.head {
46 | copy(newBuf, q.buf[q.head:q.tail])
47 | } else {
48 | n := copy(newBuf, q.buf[q.head:])
49 | copy(newBuf[n:], q.buf[:q.tail])
50 | }
51 |
52 | q.head = 0
53 | q.tail = q.count
54 | q.buf = newBuf
55 | }
56 |
57 | // Add puts an element on the end of the queue.
58 | func (q *Queue[V]) Put(elem V) {
59 | if q.tsafe {
60 | q.rw.Lock()
61 | defer q.rw.Unlock()
62 | }
63 |
64 | if q.count == len(q.buf) {
65 | q.resize()
66 | }
67 |
68 | q.buf[q.tail] = &elem
69 | // bitwise modulus
70 | q.tail = (q.tail + 1) & (len(q.buf) - 1)
71 | q.count++
72 | }
73 |
74 | // Top returns the element at the head of the queue. This call panics
75 | // if the queue is empty.
76 | func (q *Queue[V]) Top() V {
77 | if q.tsafe {
78 | q.rw.RLock()
79 | defer q.rw.RUnlock()
80 | }
81 |
82 | if q.count <= 0 {
83 | panic("queue: Peek() called on empty queue")
84 | }
85 | return *(q.buf[q.head])
86 | }
87 |
88 | // Get returns the element at index i in the queue. If the index is
89 | // invalid, the call will panic. This method accepts both positive and
90 | // negative index values. Index 0 refers to the first element, and
91 | // index -1 refers to the last.
92 | func (q *Queue[V]) Get(i int) V {
93 | if q.tsafe {
94 | q.rw.RLock()
95 | defer q.rw.RUnlock()
96 | }
97 |
98 | // If indexing backwards, convert to positive index.
99 | if i < 0 {
100 | i += q.count
101 | }
102 | if i < 0 || i >= q.count {
103 | panic("queue: Get() called with index out of range")
104 | }
105 | // bitwise modulus
106 | return *(q.buf[(q.head+i)&(len(q.buf)-1)])
107 | }
108 |
109 | // Remove removes and returns the element from the front of the queue. If the
110 | // queue is empty, the call will panic.
111 | func (q *Queue[V]) Pop() V {
112 | if q.tsafe {
113 | q.rw.Lock()
114 | defer q.rw.Unlock()
115 | }
116 |
117 | if q.count <= 0 {
118 | panic("queue: Remove() called on empty queue")
119 | }
120 | ret := q.buf[q.head]
121 | q.buf[q.head] = nil
122 | // bitwise modulus
123 | q.head = (q.head + 1) & (len(q.buf) - 1)
124 | q.count--
125 | // Resize down if buffer 1/4 full.
126 | if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
127 | q.resize()
128 | }
129 | return *ret
130 | }
131 |
132 | // Remove removes and returns the element from the front of the queue. If the
133 | // queue is empty, the call will panic.
134 | func (q *Queue[V]) TryPop() (V, bool) {
135 | if q.tsafe {
136 | q.rw.Lock()
137 | defer q.rw.Unlock()
138 | }
139 |
140 | if q.count <= 0 {
141 | var tmp V
142 | return tmp, false
143 | }
144 | ret := q.buf[q.head]
145 | q.buf[q.head] = nil
146 | // bitwise modulus
147 | q.head = (q.head + 1) & (len(q.buf) - 1)
148 | q.count--
149 | // Resize down if buffer 1/4 full.
150 | if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
151 | q.resize()
152 | }
153 |
154 | return *ret, true
155 | }
156 |
--------------------------------------------------------------------------------
/utils/queue_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | NOTE: CODE BASE IS COPIED FROM https://github.com/eapache/queue/blob/main/v2/queue.go, modified to make it thread safe
3 |
4 | Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki.
5 | Using this instead of other, simpler, queue implementations (slice+append or linked list) provides
6 | substantial memory and time benefits, and fewer GC pauses.
7 |
8 | The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe.
9 | */
10 |
11 | package utils
12 |
13 | import "testing"
14 |
15 | func TestQueueSimple(t *testing.T) {
16 | q := NewQueue[int](false)
17 |
18 | for i := 0; i < minQueueLen; i++ {
19 | q.Put(i)
20 | }
21 | for i := 0; i < minQueueLen; i++ {
22 | if q.Top() != i {
23 | t.Error("peek", i, "had value", q.Top())
24 | }
25 | x := q.Pop()
26 | if x != i {
27 | t.Error("remove", i, "had value", x)
28 | }
29 | }
30 | }
31 |
32 | func TestQueueWrapping(t *testing.T) {
33 | q := NewQueue[int](false)
34 |
35 | for i := 0; i < minQueueLen; i++ {
36 | q.Put(i)
37 | }
38 | for i := 0; i < 3; i++ {
39 | q.Pop()
40 | q.Put(minQueueLen + i)
41 | }
42 |
43 | for i := 0; i < minQueueLen; i++ {
44 | if q.Top() != i+3 {
45 | t.Error("peek", i, "had value", q.Top())
46 | }
47 | q.Pop()
48 | }
49 | }
50 |
51 | func TestQueueLen(t *testing.T) {
52 | q := NewQueue[int](false)
53 |
54 | if q.Len() != 0 {
55 | t.Error("empty queue length not 0")
56 | }
57 |
58 | for i := 0; i < 1000; i++ {
59 | q.Put(i)
60 | if q.Len() != i+1 {
61 | t.Error("adding: queue with", i, "elements has length", q.Len())
62 | }
63 | }
64 | for i := 0; i < 1000; i++ {
65 | q.Pop()
66 | if q.Len() != 1000-i-1 {
67 | t.Error("removing: queue with", 1000-i-i, "elements has length", q.Len())
68 | }
69 | }
70 | }
71 |
72 | func TestQueueGet(t *testing.T) {
73 | q := NewQueue[int](false)
74 |
75 | for i := 0; i < 1000; i++ {
76 | q.Put(i)
77 | for j := 0; j < q.Len(); j++ {
78 | if q.Get(j) != j {
79 | t.Errorf("index %d doesn't contain %d", j, j)
80 | }
81 | }
82 | }
83 | }
84 |
85 | func TestQueueGetNegative(t *testing.T) {
86 | q := NewQueue[int](false)
87 |
88 | for i := 0; i < 1000; i++ {
89 | q.Put(i)
90 | for j := 1; j <= q.Len(); j++ {
91 | if q.Get(-j) != q.Len()-j {
92 | t.Errorf("index %d doesn't contain %d", -j, q.Len()-j)
93 | }
94 | }
95 | }
96 | }
97 |
98 | func TestQueueGetOutOfRangePanics(t *testing.T) {
99 | q := NewQueue[int](false)
100 |
101 | q.Put(1)
102 | q.Put(2)
103 | q.Put(3)
104 |
105 | AssertPanics(t, "should panic when negative index", func() {
106 | q.Get(-4)
107 | })
108 |
109 | AssertPanics(t, "should panic when index greater than length", func() {
110 | q.Get(4)
111 | })
112 | }
113 |
114 | func TestQueuePeekOutOfRangePanics(t *testing.T) {
115 | q := NewQueue[int](false)
116 |
117 | AssertPanics(t, "should panic when peeking empty queue", func() {
118 | q.Top()
119 | })
120 |
121 | q.Put(1)
122 | q.Pop()
123 |
124 | AssertPanics(t, "should panic when peeking emptied queue", func() {
125 | q.Top()
126 | })
127 | }
128 |
129 | func TestQueuePopOutOfRangePanics(t *testing.T) {
130 | q := NewQueue[int](false)
131 |
132 | AssertPanics(t, "should panic when removing empty queue", func() {
133 | q.Pop()
134 | })
135 |
136 | q.Put(1)
137 | q.Pop()
138 |
139 | AssertPanics(t, "should panic when removing emptied queue", func() {
140 | q.Pop()
141 | })
142 | }
143 |
144 | // WARNING: Go's benchmark utility (go test -bench .) increases the number of
145 | // iterations until the benchmarks take a reasonable amount of time to run; memory usage
146 | // is *NOT* considered. On a fast CPU, these benchmarks can fill hundreds of GB of memory
147 | // (and then hang when they start to swap). You can manually control the number of iterations
148 | // with the `-benchtime` argument. Passing `-benchtime 1000000x` seems to be about right.
149 |
150 | func BenchmarkQueueSerial(b *testing.B) {
151 | q := NewQueue[int](false)
152 | for i := 0; i < b.N; i++ {
153 | q.Put(0)
154 | }
155 | for i := 0; i < b.N; i++ {
156 | q.Top()
157 | q.Pop()
158 | }
159 | }
160 |
161 | func BenchmarkQueueGet(b *testing.B) {
162 | q := NewQueue[int](false)
163 | for i := 0; i < b.N; i++ {
164 | q.Put(i)
165 | }
166 | b.ResetTimer()
167 | for i := 0; i < b.N; i++ {
168 | q.Get(i)
169 | }
170 | }
171 |
172 | func BenchmarkQueueTickTock(b *testing.B) {
173 | q := NewQueue[int](false)
174 | for i := 0; i < b.N; i++ {
175 | q.Put(0)
176 | q.Top()
177 | q.Pop()
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | "unsafe"
8 | )
9 |
10 | func UnsafeToString(b []byte) string {
11 | return unsafe.String(unsafe.SliceData(b), len(b))
12 | }
13 |
14 | func UnsafeToBytes(s string) []byte {
15 | if len(s) == 0 {
16 | return []byte{}
17 | }
18 |
19 | ptr := unsafe.StringData(s)
20 | return unsafe.Slice(ptr, len(s))
21 | }
22 |
23 | // NormalizeDuration normalize duration
24 | func NormalizeDuration(d time.Duration) string {
25 | ns := d.Nanoseconds()
26 | hours := int(d.Hours())
27 | minutes := int(d.Minutes()) % 60
28 | seconds := int(d.Seconds()) % 60
29 | milliseconds := int(d.Milliseconds()) % 1000
30 | microseconds := int(d.Microseconds()) % 1000
31 | nanoseconds := ns % int64(time.Microsecond)
32 |
33 | var parts []byte
34 |
35 | if hours > 0 {
36 | parts = append(parts, fmt.Sprintf("%dh", hours)...)
37 | }
38 | if minutes > 0 {
39 | parts = append(parts, fmt.Sprintf("%dm", minutes)...)
40 | }
41 | if seconds > 0 {
42 | parts = append(parts, fmt.Sprintf("%ds", seconds)...)
43 | }
44 | if milliseconds > 0 {
45 | parts = append(parts, fmt.Sprintf("%dms", milliseconds)...)
46 | }
47 | if microseconds > 0 {
48 | parts = append(parts, fmt.Sprintf("%dµs", microseconds)...)
49 | }
50 | if nanoseconds > 0 || len(parts) == 0 {
51 | parts = append(parts, fmt.Sprintf("%dns", nanoseconds)...)
52 | }
53 |
54 | return UnsafeToString(parts)
55 | }
56 |
57 | func AssertPanics(t *testing.T, name string, f func()) {
58 | defer func() {
59 | if r := recover(); r == nil {
60 | t.Errorf("%s: didn't panic as expected", name)
61 | }
62 | }()
63 |
64 | f()
65 | }
66 |
--------------------------------------------------------------------------------
/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestUnsafeToString(t *testing.T) {
10 | original := "Hello, World!"
11 | b := []byte(original)
12 | s := UnsafeToString(b)
13 |
14 | if s != original {
15 | t.Errorf("Expected %q but got %q", original, s)
16 | }
17 | }
18 |
19 | func TestUnsafeToBytes(t *testing.T) {
20 | original := "Hello, World!"
21 | b := UnsafeToBytes(original)
22 |
23 | if string(b) != original {
24 | t.Errorf("Expected %q but got %q", original, string(b))
25 | }
26 | }
27 |
28 | func TestPanic(t *testing.T) {
29 | f := func() {
30 | defer func() {
31 | if r := recover(); r != nil {
32 | fmt.Println("Recovered in causePanic:", r)
33 | }
34 | fmt.Println("1")
35 | }()
36 |
37 | fmt.Println("result")
38 | }
39 | f()
40 | }
41 |
42 | func TestNormalizeDuration(t *testing.T) {
43 | tests := []struct {
44 | input time.Duration
45 | expected string
46 | }{
47 | {time.Duration(0), "0ns"},
48 | {time.Second, "1s"},
49 | {time.Duration(2 * time.Second), "2s"},
50 | {time.Minute, "1m"},
51 | {time.Duration(61 * time.Second), "1m1s"},
52 | {time.Hour, "1h"},
53 | {time.Duration(3601 * time.Second), "1h1s"},
54 | {time.Duration(1*time.Hour + 30*time.Minute + 15*time.Second), "1h30m15s"},
55 | {time.Duration(1*time.Minute + 500*time.Millisecond), "1m500ms"},
56 | {time.Duration(500 * time.Microsecond), "500µs"},
57 | {time.Duration(1 * time.Nanosecond), "1ns"},
58 | }
59 | for _, test := range tests {
60 | t.Run(test.input.String(), func(t *testing.T) {
61 | result := NormalizeDuration(test.input)
62 | if result != test.expected {
63 | t.Errorf("NormalizeDuration(%v) = %v; want %v", test.input, result, test.expected)
64 | }
65 | })
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/visualizer.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "io"
5 | )
6 |
7 | var dot = dotVizer{}
8 |
9 | type Visualizer interface {
10 | // Visualize generate raw dag text in dot format and write to writer
11 | Visualize(tf *TaskFlow, writer io.Writer) error
12 | }
13 |
--------------------------------------------------------------------------------
/visualizer_dot.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "strings"
7 | )
8 |
9 | type dotVizer struct{}
10 |
11 | // DotGraph represents a graph in DOT format
12 | type DotGraph struct {
13 | name string
14 | isSubgraph bool
15 | nodes map[string]*DotNode
16 | edges []*DotEdge
17 | attributes map[string]string
18 | subgraphs []*DotGraph
19 | indent string
20 | }
21 |
22 | // DotNode represents a node in DOT format
23 | type DotNode struct {
24 | id string
25 | attributes map[string]string
26 | }
27 |
28 | // DotEdge represents an edge in DOT format
29 | type DotEdge struct {
30 | from *DotNode
31 | to *DotNode
32 | attributes map[string]string
33 | }
34 |
35 | func NewDotGraph(name string) *DotGraph {
36 | return &DotGraph{
37 | name: name,
38 | isSubgraph: false,
39 | nodes: make(map[string]*DotNode),
40 | edges: make([]*DotEdge, 0),
41 | attributes: make(map[string]string),
42 | subgraphs: make([]*DotGraph, 0),
43 | indent: "",
44 | }
45 | }
46 |
47 | func (g *DotGraph) CreateNode(name string) *DotNode {
48 | if node, exists := g.nodes[name]; exists {
49 | return node
50 | }
51 |
52 | node := &DotNode{
53 | id: name,
54 | attributes: make(map[string]string),
55 | }
56 | g.nodes[name] = node
57 | return node
58 | }
59 |
60 | func (g *DotGraph) CreateEdge(from, to *DotNode, label string) *DotEdge {
61 | edge := &DotEdge{
62 | from: from,
63 | to: to,
64 | attributes: make(map[string]string),
65 | }
66 | if label != "" {
67 | edge.attributes["label"] = label
68 | }
69 | g.edges = append(g.edges, edge)
70 | return edge
71 | }
72 |
73 | func (g *DotGraph) SubGraph(name string) *DotGraph {
74 | subgraph := &DotGraph{
75 | name: name,
76 | isSubgraph: true,
77 | nodes: make(map[string]*DotNode),
78 | edges: make([]*DotEdge, 0),
79 | attributes: make(map[string]string),
80 | subgraphs: make([]*DotGraph, 0),
81 | indent: g.indent + " ",
82 | }
83 | g.subgraphs = append(g.subgraphs, subgraph)
84 | return subgraph
85 | }
86 |
87 | func (g *DotGraph) String() string {
88 | var sb strings.Builder
89 |
90 | if g.isSubgraph {
91 | sb.WriteString(g.indent + "subgraph " + quote("cluster_"+g.name) + " {\n")
92 | } else {
93 | sb.WriteString(g.indent + "digraph " + quote(g.name) + " {\n")
94 | }
95 |
96 | for k, v := range g.attributes {
97 | sb.WriteString(g.indent + " " + k + "=" + quote(v) + ";\n")
98 | }
99 |
100 | for _, node := range g.nodes {
101 | sb.WriteString(node.Format(g.indent + " "))
102 | }
103 |
104 | for _, edge := range g.edges {
105 | sb.WriteString(edge.Format(g.indent + " "))
106 | }
107 |
108 | for _, subgraph := range g.subgraphs {
109 | sb.WriteString(subgraph.String())
110 | }
111 |
112 | sb.WriteString(g.indent + "}\n")
113 | return sb.String()
114 | }
115 |
116 | func (node *DotNode) Format(indent string) string {
117 | attrs := formatAttributes(node.attributes)
118 |
119 | if attrs == "" {
120 | return indent + quote(node.id) + ";\n"
121 | }
122 |
123 | return indent + quote(node.id) + " [" + attrs + "];\n"
124 | }
125 |
126 | func (edge *DotEdge) Format(indent string) string {
127 | from := edge.from.id
128 | to := edge.to.id
129 |
130 | attrs := formatAttributes(edge.attributes)
131 |
132 | if attrs == "" {
133 | return indent + quote(from) + " -> " + quote(to) + ";\n"
134 | }
135 |
136 | return indent + quote(from) + " -> " + quote(to) + " [" + attrs + "];\n"
137 | }
138 |
139 | func quote(s string) string {
140 | return "\"" + s + "\""
141 | }
142 |
143 | func formatAttributes(attrs map[string]string) string {
144 | if len(attrs) == 0 {
145 | return ""
146 | }
147 |
148 | result := make([]string, 0, len(attrs))
149 | for k, v := range attrs {
150 | result = append(result, k+"="+quote(v))
151 | }
152 | return strings.Join(result, ", ")
153 | }
154 |
155 | // visualizeG recursively visualizes the graph and its subgraphs in DOT format
156 | func (v *dotVizer) visualizeG(g *eGraph, parentGraph *DotGraph) error {
157 | graph := parentGraph
158 | graph.attributes["rankdir"] = "LR"
159 |
160 | nodeMap := make(map[string]*DotNode)
161 |
162 | for _, node := range g.nodes {
163 | color := "black"
164 | if node.priority == HIGH {
165 | color = "#f5427b"
166 | } else if node.priority == LOW {
167 | color = "purple"
168 | }
169 |
170 | switch p := node.ptr.(type) {
171 | case *Static:
172 | dotNode := graph.CreateNode(node.name)
173 | dotNode.attributes["color"] = color
174 | nodeMap[node.name] = dotNode
175 |
176 | case *Condition:
177 | dotNode := graph.CreateNode(node.name)
178 | dotNode.attributes["shape"] = "diamond"
179 | dotNode.attributes["color"] = "green"
180 | nodeMap[node.name] = dotNode
181 |
182 | case *Subflow:
183 | subgraph := graph.SubGraph(node.name)
184 | subgraph.attributes["label"] = node.name
185 | subgraph.attributes["style"] = "dashed"
186 | subgraph.attributes["rankdir"] = "LR"
187 | subgraph.attributes["bgcolor"] = "#F5F5F5"
188 | subgraph.attributes["fontcolor"] = color
189 |
190 | subgraphDot := subgraph.CreateNode(node.name)
191 | subgraphDot.attributes["shape"] = "point"
192 | subgraphDot.attributes["height"] = "0.05"
193 | subgraphDot.attributes["width"] = "0.05"
194 |
195 | nodeMap[node.name] = subgraphDot
196 |
197 | err := v.visualizeG(p.g, subgraph)
198 | if err != nil {
199 | errorNodeName := "unvisualized_subflow_" + p.g.name
200 | dotNode := graph.CreateNode(errorNodeName)
201 | dotNode.attributes["color"] = "#a10212"
202 | dotNode.attributes["comment"] = "cannot visualize due to instantiate panic or failed"
203 | nodeMap[node.name] = dotNode
204 | }
205 | }
206 | }
207 |
208 | for _, node := range g.nodes {
209 | for idx, deps := range node.successors {
210 | if from, ok := nodeMap[node.name]; ok {
211 | if to, ok := nodeMap[deps.name]; ok {
212 | label := ""
213 | style := "solid"
214 | if _, ok := node.ptr.(*Condition); ok {
215 | label = fmt.Sprintf("%d", idx)
216 | style = "dashed"
217 | }
218 |
219 | edge := graph.CreateEdge(from, to, label)
220 | if style != "solid" {
221 | edge.attributes["style"] = style
222 | }
223 | }
224 | }
225 | }
226 | }
227 |
228 | return nil
229 | }
230 |
231 | // Visualize generates raw dag text in dot format and writes to writer
232 | func (v *dotVizer) Visualize(tf *TaskFlow, writer io.Writer) error {
233 | graph := NewDotGraph(tf.graph.name)
234 | err := v.visualizeG(tf.graph, graph)
235 | if err != nil {
236 | return fmt.Errorf("visualize %v -> %w", tf.graph.name, err)
237 | }
238 |
239 | _, err = writer.Write([]byte(graph.String()))
240 | if err != nil {
241 | return fmt.Errorf("write dot output -> %w", err)
242 | }
243 |
244 | return nil
245 | }
246 |
--------------------------------------------------------------------------------
/visualizer_dot_test.go:
--------------------------------------------------------------------------------
1 | package gotaskflow
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestDotGraph_String(t *testing.T) {
10 | graph := NewDotGraph("test_graph")
11 | graph.attributes["rankdir"] = "LR"
12 |
13 | nodeA := graph.CreateNode("A")
14 | nodeA.attributes["color"] = "black"
15 |
16 | nodeB := graph.CreateNode("B")
17 | nodeB.attributes["shape"] = "diamond"
18 |
19 | edge := graph.CreateEdge(nodeA, nodeB, "edge_label")
20 | edge.attributes["style"] = "dashed"
21 |
22 | result := graph.String()
23 |
24 | expectedParts := []string{
25 | `digraph "test_graph" {`,
26 | `rankdir="LR";`,
27 | `"A" [color="black"];`,
28 | `"B" [shape="diamond"];`,
29 | `"A" -> "B"`,
30 | `}`,
31 | }
32 |
33 | for _, part := range expectedParts {
34 | if !strings.Contains(result, part) {
35 | t.Errorf("Expected DOT output to contain %q, but it didn't.\nActual output:\n%s", part, result)
36 | }
37 | }
38 | }
39 |
40 | func TestDotGraph_SubGraph(t *testing.T) {
41 | graph := NewDotGraph("main_graph")
42 |
43 | nodeA := graph.CreateNode("A")
44 | nodeB := graph.CreateNode("B")
45 | graph.CreateEdge(nodeA, nodeB, "")
46 |
47 | subgraph := graph.SubGraph("sub_graph")
48 | subgraph.attributes["style"] = "dashed"
49 |
50 | nodeC := subgraph.CreateNode("C")
51 | nodeD := subgraph.CreateNode("D")
52 | subgraph.CreateEdge(nodeC, nodeD, "")
53 |
54 | result := graph.String()
55 |
56 | expectedParts := []string{
57 | `digraph "main_graph" {`,
58 | `"A" -> "B";`,
59 | `subgraph "cluster_sub_graph" {`,
60 | `style="dashed";`,
61 | `"C";`,
62 | `"D";`,
63 | `"C" -> "D";`,
64 | `}`,
65 | }
66 |
67 | for _, part := range expectedParts {
68 | if !strings.Contains(result, part) {
69 | t.Errorf("Expected DOT output to contain %q, but it didn't.\nActual output:\n%s", part, result)
70 | }
71 | }
72 | }
73 |
74 | func TestDotVizer_Visualize(t *testing.T) {
75 | tf := NewTaskFlow("test_flow")
76 |
77 | taskA := tf.NewTask("A", func() {})
78 | taskB := tf.NewTask("B", func() {})
79 | taskC := tf.NewTask("C", func() {})
80 |
81 | taskA.Precede(taskB)
82 | taskC.Precede(taskB)
83 |
84 | var buf bytes.Buffer
85 |
86 | vizer := &dotVizer{}
87 | err := vizer.Visualize(tf, &buf)
88 |
89 | if err != nil {
90 | t.Fatalf("Visualize returned an error: %v", err)
91 | }
92 |
93 | result := buf.String()
94 |
95 | expectedParts := []string{
96 | `digraph "test_flow" {`,
97 | `rankdir="LR";`,
98 | `"A"`,
99 | `"B"`,
100 | `"C"`,
101 | `"A" -> "B"`,
102 | `"C" -> "B"`,
103 | }
104 |
105 | for _, part := range expectedParts {
106 | if !strings.Contains(result, part) {
107 | t.Errorf("Expected DOT output to contain %q, but it didn't.\nActual output:\n%s", part, result)
108 | }
109 | }
110 | }
111 |
112 | func TestDotVizer_VisualizeComplex(t *testing.T) {
113 | tf := NewTaskFlow("complex_flow")
114 |
115 | taskA := tf.NewTask("A", func() {})
116 | taskB := tf.NewTask("B", func() {})
117 |
118 | condTask := tf.NewCondition("cond", func() uint { return 0 })
119 |
120 | subTask := tf.NewSubflow("sub", func(sf *Subflow) {
121 | subA := sf.NewTask("subA", func() {})
122 | subB := sf.NewTask("subB", func() {})
123 | subA.Precede(subB)
124 | })
125 |
126 | taskA.Precede(taskB)
127 | taskB.Precede(condTask)
128 | condTask.Precede(subTask)
129 |
130 | var buf bytes.Buffer
131 |
132 | vizer := &dotVizer{}
133 | err := vizer.Visualize(tf, &buf)
134 |
135 | if err != nil {
136 | t.Fatalf("Visualize returned an error: %v", err)
137 | }
138 |
139 | result := buf.String()
140 |
141 | expectedParts := []string{
142 | `digraph "complex_flow" {`,
143 | `rankdir="LR";`,
144 | `"A"`,
145 | `"B"`,
146 | `"cond" `,
147 | `subgraph "cluster_sub" {`,
148 | `"sub"`,
149 | `"A" -> "B"`,
150 | `"B" -> "cond"`,
151 | `"cond" -> "sub"`,
152 | }
153 |
154 | for _, part := range expectedParts {
155 | if !strings.Contains(result, part) {
156 | t.Errorf("Expected DOT output to contain %q, but it didn't.\nActual output:\n%s", part, result)
157 | }
158 | }
159 | }
160 |
161 | func TestDotNode_Format(t *testing.T) {
162 | node := &DotNode{
163 | id: "test_node",
164 | attributes: make(map[string]string),
165 | }
166 |
167 | result := node.Format(" ")
168 | expected := ` "test_node";` + "\n"
169 | if result != expected {
170 | t.Errorf("Expected %q, got %q", expected, result)
171 | }
172 |
173 | node.attributes["color"] = "red"
174 | result = node.Format(" ")
175 | expected = ` "test_node" [color="red"];` + "\n"
176 | if result != expected {
177 | t.Errorf("Expected %q, got %q", expected, result)
178 | }
179 | }
180 |
181 | func TestDotEdge_Format(t *testing.T) {
182 | from := &DotNode{id: "from"}
183 | to := &DotNode{id: "to"}
184 |
185 | edge := &DotEdge{
186 | from: from,
187 | to: to,
188 | attributes: make(map[string]string),
189 | }
190 |
191 | result := edge.Format(" ")
192 | expected := ` "from" -> "to";` + "\n"
193 | if result != expected {
194 | t.Errorf("Expected %q, got %q", expected, result)
195 | }
196 |
197 | edge.attributes["style"] = "dashed"
198 | result = edge.Format(" ")
199 | expected = ` "from" -> "to" [style="dashed"];` + "\n"
200 | if result != expected {
201 | t.Errorf("Expected %q, got %q", expected, result)
202 | }
203 | }
204 |
205 | func TestFormatAttributes(t *testing.T) {
206 | attrs := make(map[string]string)
207 | result := formatAttributes(attrs)
208 | if result != "" {
209 | t.Errorf("Expected empty string for empty attributes, got %q", result)
210 | }
211 |
212 | attrs["color"] = "red"
213 | result = formatAttributes(attrs)
214 | expected := `color="red"`
215 | if result != expected {
216 | t.Errorf("Expected %q for single attribute, got %q", expected, result)
217 | }
218 |
219 | attrs["shape"] = "box"
220 | result = formatAttributes(attrs)
221 |
222 | option1 := `color="red", shape="box"`
223 | option2 := `shape="box", color="red"`
224 | if result != option1 && result != option2 {
225 | t.Errorf("Expected either %q or %q for multiple attributes, got %q", option1, option2, result)
226 | }
227 | }
228 |
229 | func TestQuote(t *testing.T) {
230 | testCases := []struct {
231 | input string
232 | expected string
233 | }{
234 | {"test", `"test"`},
235 | {"", `""`},
236 | {`"quoted"`, `""quoted""`},
237 | }
238 |
239 | for _, tc := range testCases {
240 | result := quote(tc.input)
241 | if result != tc.expected {
242 | t.Errorf("quote(%q) = %q, expected %q", tc.input, result, tc.expected)
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------