├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .golangci.yml ├── LICENSE ├── README.md ├── go.mod ├── tunny.go ├── tunny_logo.png ├── tunny_test.go └── worker.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Jeffail 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.16.x 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - uses: actions/cache@v2 20 | with: 21 | path: | 22 | ~/go/pkg/mod 23 | ~/.cache/go-build 24 | ~/Library/Caches/go-build 25 | %LocalAppData%\go-build 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Tidy 31 | run: go mod tidy && git diff-index --quiet HEAD || { >&2 echo "Stale go.{mod,sum} detected. This can be fixed with 'go mod tidy'."; exit 1; } 32 | 33 | - name: Test 34 | run: go test -count 100 ./... 35 | 36 | golangci-lint: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v2 41 | 42 | - name: Lint 43 | uses: golangci/golangci-lint-action@v2 44 | with: 45 | version: latest 46 | args: --timeout 10m 47 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 30s 3 | 4 | issues: 5 | max-issues-per-linter: 0 6 | max-same-issues: 0 7 | 8 | linters-settings: 9 | gocritic: 10 | enabled-tags: 11 | - diagnostic 12 | - experimental 13 | - opinionated 14 | - performance 15 | - style 16 | 17 | linters: 18 | disable-all: true 19 | enable: 20 | # Default linters reported by golangci-lint help linters` in v1.39.0 21 | - deadcode 22 | - errcheck 23 | - gosimple 24 | - govet 25 | - gosimple 26 | - ineffassign 27 | - staticcheck 28 | - structcheck 29 | - stylecheck 30 | - typecheck 31 | - unused 32 | - varcheck 33 | # Extra linters: 34 | - gofmt 35 | - goimports 36 | - gocritic 37 | - revive 38 | - bodyclose 39 | - gosec 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ashley Jeffs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tunny](tunny_logo.png "Tunny") 2 | 3 | [![godoc for Jeffail/tunny][1]][2] 4 | [![goreportcard for Jeffail/tunny][3]][4] 5 | 6 | Tunny is a Golang library for spawning and managing a goroutine pool, allowing 7 | you to limit work coming from any number of goroutines with a synchronous API. 8 | 9 | A fixed goroutine pool is helpful when you have work coming from an arbitrary 10 | number of asynchronous sources, but a limited capacity for parallel processing. 11 | For example, when processing jobs from HTTP requests that are CPU heavy you can 12 | create a pool with a size that matches your CPU count. 13 | 14 | ## Install 15 | 16 | ``` sh 17 | go get github.com/Jeffail/tunny 18 | ``` 19 | 20 | Or, using dep: 21 | 22 | ``` sh 23 | dep ensure -add github.com/Jeffail/tunny 24 | ``` 25 | 26 | ## Use 27 | 28 | For most cases your heavy work can be expressed in a simple `func()`, where you 29 | can use `NewFunc`. Let's see how this looks using our HTTP requests to CPU count 30 | example: 31 | 32 | ``` go 33 | package main 34 | 35 | import ( 36 | "io/ioutil" 37 | "net/http" 38 | "runtime" 39 | 40 | "github.com/Jeffail/tunny" 41 | ) 42 | 43 | func main() { 44 | numCPUs := runtime.NumCPU() 45 | 46 | pool := tunny.NewFunc(numCPUs, func(payload interface{}) interface{} { 47 | var result []byte 48 | 49 | // TODO: Something CPU heavy with payload 50 | 51 | return result 52 | }) 53 | defer pool.Close() 54 | 55 | http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) { 56 | input, err := ioutil.ReadAll(r.Body) 57 | if err != nil { 58 | http.Error(w, "Internal error", http.StatusInternalServerError) 59 | } 60 | defer r.Body.Close() 61 | 62 | // Funnel this work into our pool. This call is synchronous and will 63 | // block until the job is completed. 64 | result := pool.Process(input) 65 | 66 | w.Write(result.([]byte)) 67 | }) 68 | 69 | http.ListenAndServe(":8080", nil) 70 | } 71 | ``` 72 | 73 | Tunny also supports timeouts. You can replace the `Process` call above to the 74 | following: 75 | 76 | ``` go 77 | result, err := pool.ProcessTimed(input, time.Second*5) 78 | if err == tunny.ErrJobTimedOut { 79 | http.Error(w, "Request timed out", http.StatusRequestTimeout) 80 | } 81 | ``` 82 | 83 | You can also use the context from the request (or any other context) to handle timeouts and deadlines. Simply replace the `Process` call to the following: 84 | 85 | ``` go 86 | result, err := pool.ProcessCtx(r.Context(), input) 87 | if err == context.DeadlineExceeded { 88 | http.Error(w, "Request timed out", http.StatusRequestTimeout) 89 | } 90 | ``` 91 | 92 | ## Changing Pool Size 93 | 94 | The size of a Tunny pool can be changed at any time with `SetSize(int)`: 95 | 96 | ``` go 97 | pool.SetSize(10) // 10 goroutines 98 | pool.SetSize(100) // 100 goroutines 99 | ``` 100 | 101 | This is safe to perform from any goroutine even if others are still processing. 102 | 103 | ## Goroutines With State 104 | 105 | Sometimes each goroutine within a Tunny pool will require its own managed state. 106 | In this case you should implement [`tunny.Worker`][tunny-worker], which includes 107 | calls for terminating, interrupting (in case a job times out and is no longer 108 | needed) and blocking the next job allocation until a condition is met. 109 | 110 | When creating a pool using `Worker` types you will need to provide a constructor 111 | function for spawning your custom implementation: 112 | 113 | ``` go 114 | pool := tunny.New(poolSize, func() Worker { 115 | // TODO: Any per-goroutine state allocation here. 116 | return newCustomWorker() 117 | }) 118 | ``` 119 | 120 | This allows Tunny to create and destroy `Worker` types cleanly when the pool 121 | size is changed. 122 | 123 | ## Ordering 124 | 125 | Backlogged jobs are not guaranteed to be processed in order. Due to the current 126 | implementation of channels and select blocks a stack of backlogged jobs will be 127 | processed as a FIFO queue. However, this behaviour is not part of the spec and 128 | should not be relied upon. 129 | 130 | [1]: https://godoc.org/github.com/Jeffail/tunny?status.svg 131 | [2]: http://godoc.org/github.com/Jeffail/tunny 132 | [3]: https://goreportcard.com/badge/github.com/Jeffail/tunny 133 | [4]: https://goreportcard.com/report/Jeffail/tunny 134 | [tunny-worker]: https://godoc.org/github.com/Jeffail/tunny#Worker 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Jeffail/tunny 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /tunny.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Ashley Jeffs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tunny 22 | 23 | import ( 24 | "context" 25 | "errors" 26 | "sync" 27 | "sync/atomic" 28 | "time" 29 | ) 30 | 31 | //------------------------------------------------------------------------------ 32 | 33 | // Errors that are used throughout the Tunny API. 34 | var ( 35 | ErrPoolNotRunning = errors.New("the pool is not running") 36 | ErrJobNotFunc = errors.New("generic worker not given a func()") 37 | ErrWorkerClosed = errors.New("worker was closed") 38 | ErrJobTimedOut = errors.New("job request timed out") 39 | ) 40 | 41 | // Worker is an interface representing a Tunny working agent. It will be used to 42 | // block a calling goroutine until ready to process a job, process that job 43 | // synchronously, interrupt its own process call when jobs are abandoned, and 44 | // clean up its resources when being removed from the pool. 45 | // 46 | // Each of these duties are implemented as a single method and can be averted 47 | // when not needed by simply implementing an empty func. 48 | type Worker interface { 49 | // Process will synchronously perform a job and return the result. 50 | Process(interface{}) interface{} 51 | 52 | // BlockUntilReady is called before each job is processed and must block the 53 | // calling goroutine until the Worker is ready to process the next job. 54 | BlockUntilReady() 55 | 56 | // Interrupt is called when a job is cancelled. The worker is responsible 57 | // for unblocking the Process implementation. 58 | Interrupt() 59 | 60 | // Terminate is called when a Worker is removed from the processing pool 61 | // and is responsible for cleaning up any held resources. 62 | Terminate() 63 | } 64 | 65 | //------------------------------------------------------------------------------ 66 | 67 | // closureWorker is a minimal Worker implementation that simply wraps a 68 | // func(interface{}) interface{} 69 | type closureWorker struct { 70 | processor func(interface{}) interface{} 71 | } 72 | 73 | func (w *closureWorker) Process(payload interface{}) interface{} { 74 | return w.processor(payload) 75 | } 76 | 77 | func (w *closureWorker) BlockUntilReady() {} 78 | func (w *closureWorker) Interrupt() {} 79 | func (w *closureWorker) Terminate() {} 80 | 81 | //------------------------------------------------------------------------------ 82 | 83 | // callbackWorker is a minimal Worker implementation that attempts to cast 84 | // each job into func() and either calls it if successful or returns 85 | // ErrJobNotFunc. 86 | type callbackWorker struct{} 87 | 88 | func (w *callbackWorker) Process(payload interface{}) interface{} { 89 | f, ok := payload.(func()) 90 | if !ok { 91 | return ErrJobNotFunc 92 | } 93 | f() 94 | return nil 95 | } 96 | 97 | func (w *callbackWorker) BlockUntilReady() {} 98 | func (w *callbackWorker) Interrupt() {} 99 | func (w *callbackWorker) Terminate() {} 100 | 101 | //------------------------------------------------------------------------------ 102 | 103 | // Pool is a struct that manages a collection of workers, each with their own 104 | // goroutine. The Pool can initialize, expand, compress and close the workers, 105 | // as well as processing jobs with the workers synchronously. 106 | type Pool struct { 107 | queuedJobs int64 108 | 109 | ctor func() Worker 110 | workers []*workerWrapper 111 | reqChan chan workRequest 112 | 113 | workerMut sync.Mutex 114 | } 115 | 116 | // New creates a new Pool of workers that starts with n workers. You must 117 | // provide a constructor function that creates new Worker types and when you 118 | // change the size of the pool the constructor will be called to create each new 119 | // Worker. 120 | func New(n int, ctor func() Worker) *Pool { 121 | p := &Pool{ 122 | ctor: ctor, 123 | reqChan: make(chan workRequest), 124 | } 125 | p.SetSize(n) 126 | 127 | return p 128 | } 129 | 130 | // NewFunc creates a new Pool of workers where each worker will process using 131 | // the provided func. 132 | func NewFunc(n int, f func(interface{}) interface{}) *Pool { 133 | return New(n, func() Worker { 134 | return &closureWorker{ 135 | processor: f, 136 | } 137 | }) 138 | } 139 | 140 | // NewCallback creates a new Pool of workers where workers cast the job payload 141 | // into a func() and runs it, or returns ErrNotFunc if the cast failed. 142 | func NewCallback(n int) *Pool { 143 | return New(n, func() Worker { 144 | return &callbackWorker{} 145 | }) 146 | } 147 | 148 | //------------------------------------------------------------------------------ 149 | 150 | // Process will use the Pool to process a payload and synchronously return the 151 | // result. Process can be called safely by any goroutines, but will panic if the 152 | // Pool has been stopped. 153 | func (p *Pool) Process(payload interface{}) interface{} { 154 | atomic.AddInt64(&p.queuedJobs, 1) 155 | 156 | request, open := <-p.reqChan 157 | if !open { 158 | panic(ErrPoolNotRunning) 159 | } 160 | 161 | request.jobChan <- payload 162 | 163 | payload, open = <-request.retChan 164 | if !open { 165 | panic(ErrWorkerClosed) 166 | } 167 | 168 | atomic.AddInt64(&p.queuedJobs, -1) 169 | return payload 170 | } 171 | 172 | // ProcessTimed will use the Pool to process a payload and synchronously return 173 | // the result. If the timeout occurs before the job has finished the worker will 174 | // be interrupted and ErrJobTimedOut will be returned. ProcessTimed can be 175 | // called safely by any goroutines. 176 | func (p *Pool) ProcessTimed( 177 | payload interface{}, 178 | timeout time.Duration, 179 | ) (interface{}, error) { 180 | atomic.AddInt64(&p.queuedJobs, 1) 181 | defer atomic.AddInt64(&p.queuedJobs, -1) 182 | 183 | tout := time.NewTimer(timeout) 184 | 185 | var request workRequest 186 | var open bool 187 | 188 | select { 189 | case request, open = <-p.reqChan: 190 | if !open { 191 | return nil, ErrPoolNotRunning 192 | } 193 | case <-tout.C: 194 | return nil, ErrJobTimedOut 195 | } 196 | 197 | select { 198 | case request.jobChan <- payload: 199 | case <-tout.C: 200 | request.interruptFunc() 201 | return nil, ErrJobTimedOut 202 | } 203 | 204 | select { 205 | case payload, open = <-request.retChan: 206 | if !open { 207 | return nil, ErrWorkerClosed 208 | } 209 | case <-tout.C: 210 | request.interruptFunc() 211 | return nil, ErrJobTimedOut 212 | } 213 | 214 | tout.Stop() 215 | return payload, nil 216 | } 217 | 218 | // ProcessCtx will use the Pool to process a payload and synchronously return 219 | // the result. If the context cancels before the job has finished the worker will 220 | // be interrupted and ErrJobTimedOut will be returned. ProcessCtx can be 221 | // called safely by any goroutines. 222 | func (p *Pool) ProcessCtx(ctx context.Context, payload interface{}) (interface{}, error) { 223 | atomic.AddInt64(&p.queuedJobs, 1) 224 | defer atomic.AddInt64(&p.queuedJobs, -1) 225 | 226 | var request workRequest 227 | var open bool 228 | 229 | select { 230 | case request, open = <-p.reqChan: 231 | if !open { 232 | return nil, ErrPoolNotRunning 233 | } 234 | case <-ctx.Done(): 235 | return nil, ctx.Err() 236 | } 237 | 238 | select { 239 | case request.jobChan <- payload: 240 | case <-ctx.Done(): 241 | request.interruptFunc() 242 | return nil, ctx.Err() 243 | } 244 | 245 | select { 246 | case payload, open = <-request.retChan: 247 | if !open { 248 | return nil, ErrWorkerClosed 249 | } 250 | case <-ctx.Done(): 251 | request.interruptFunc() 252 | return nil, ctx.Err() 253 | } 254 | 255 | return payload, nil 256 | } 257 | 258 | // QueueLength returns the current count of pending queued jobs. 259 | func (p *Pool) QueueLength() int64 { 260 | return atomic.LoadInt64(&p.queuedJobs) 261 | } 262 | 263 | // SetSize changes the total number of workers in the Pool. This can be called 264 | // by any goroutine at any time unless the Pool has been stopped, in which case 265 | // a panic will occur. 266 | func (p *Pool) SetSize(n int) { 267 | p.workerMut.Lock() 268 | defer p.workerMut.Unlock() 269 | 270 | lWorkers := len(p.workers) 271 | if lWorkers == n { 272 | return 273 | } 274 | 275 | // Add extra workers if N > len(workers) 276 | for i := lWorkers; i < n; i++ { 277 | p.workers = append(p.workers, newWorkerWrapper(p.reqChan, p.ctor())) 278 | } 279 | 280 | // Asynchronously stop all workers > N 281 | for i := n; i < lWorkers; i++ { 282 | p.workers[i].stop() 283 | } 284 | 285 | // Synchronously wait for all workers > N to stop 286 | for i := n; i < lWorkers; i++ { 287 | p.workers[i].join() 288 | p.workers[i] = nil 289 | } 290 | 291 | // Remove stopped workers from slice 292 | p.workers = p.workers[:n] 293 | } 294 | 295 | // GetSize returns the current size of the pool. 296 | func (p *Pool) GetSize() int { 297 | p.workerMut.Lock() 298 | defer p.workerMut.Unlock() 299 | 300 | return len(p.workers) 301 | } 302 | 303 | // Close will terminate all workers and close the job channel of this Pool. 304 | func (p *Pool) Close() { 305 | p.SetSize(0) 306 | close(p.reqChan) 307 | } 308 | 309 | //------------------------------------------------------------------------------ 310 | -------------------------------------------------------------------------------- /tunny_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeffail/tunny/a274c3ce48a6dc4f7b5fb5b8eaefbbd9e23574b9/tunny_logo.png -------------------------------------------------------------------------------- /tunny_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Ashley Jeffs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tunny 22 | 23 | import ( 24 | "context" 25 | "sync" 26 | "sync/atomic" 27 | "testing" 28 | "time" 29 | ) 30 | 31 | //------------------------------------------------------------------------------ 32 | 33 | func TestPoolSizeAdjustment(t *testing.T) { 34 | pool := NewFunc(10, func(interface{}) interface{} { return "foo" }) 35 | if exp, act := 10, len(pool.workers); exp != act { 36 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 37 | } 38 | 39 | pool.SetSize(10) 40 | if exp, act := 10, pool.GetSize(); exp != act { 41 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 42 | } 43 | 44 | pool.SetSize(9) 45 | if exp, act := 9, pool.GetSize(); exp != act { 46 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 47 | } 48 | 49 | pool.SetSize(10) 50 | if exp, act := 10, pool.GetSize(); exp != act { 51 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 52 | } 53 | 54 | pool.SetSize(0) 55 | if exp, act := 0, pool.GetSize(); exp != act { 56 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 57 | } 58 | 59 | pool.SetSize(10) 60 | if exp, act := 10, pool.GetSize(); exp != act { 61 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 62 | } 63 | 64 | // Finally, make sure we still have actual active workers. 65 | if exp, act := "foo", pool.Process(0).(string); exp != act { 66 | t.Errorf("Wrong result: %v != %v", act, exp) 67 | } 68 | 69 | pool.Close() 70 | if exp, act := 0, pool.GetSize(); exp != act { 71 | t.Errorf("Wrong size of pool: %v != %v", act, exp) 72 | } 73 | } 74 | 75 | //------------------------------------------------------------------------------ 76 | 77 | func TestFuncJob(t *testing.T) { 78 | pool := NewFunc(10, func(in interface{}) interface{} { 79 | intVal := in.(int) 80 | return intVal * 2 81 | }) 82 | defer pool.Close() 83 | 84 | for i := 0; i < 10; i++ { 85 | ret := pool.Process(10) 86 | if exp, act := 20, ret.(int); exp != act { 87 | t.Errorf("Wrong result: %v != %v", act, exp) 88 | } 89 | } 90 | } 91 | 92 | func TestFuncJobTimed(t *testing.T) { 93 | pool := NewFunc(10, func(in interface{}) interface{} { 94 | intVal := in.(int) 95 | return intVal * 2 96 | }) 97 | defer pool.Close() 98 | 99 | for i := 0; i < 10; i++ { 100 | ret, err := pool.ProcessTimed(10, time.Millisecond) 101 | if err != nil { 102 | t.Fatalf("Failed to process: %v", err) 103 | } 104 | if exp, act := 20, ret.(int); exp != act { 105 | t.Errorf("Wrong result: %v != %v", act, exp) 106 | } 107 | } 108 | } 109 | 110 | func TestFuncJobCtx(t *testing.T) { 111 | t.Run("Completes when ctx not canceled", func(t *testing.T) { 112 | pool := NewFunc(10, func(in interface{}) interface{} { 113 | intVal := in.(int) 114 | return intVal * 2 115 | }) 116 | defer pool.Close() 117 | 118 | for i := 0; i < 10; i++ { 119 | ret, err := pool.ProcessCtx(context.Background(), 10) 120 | if err != nil { 121 | t.Fatalf("Failed to process: %v", err) 122 | } 123 | if exp, act := 20, ret.(int); exp != act { 124 | t.Errorf("Wrong result: %v != %v", act, exp) 125 | } 126 | } 127 | }) 128 | 129 | t.Run("Returns err when ctx canceled", func(t *testing.T) { 130 | pool := NewFunc(1, func(in interface{}) interface{} { 131 | intVal := in.(int) 132 | <-time.After(time.Millisecond) 133 | return intVal * 2 134 | }) 135 | defer pool.Close() 136 | 137 | ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) 138 | defer cancel() 139 | _, act := pool.ProcessCtx(ctx, 10) 140 | if exp := context.DeadlineExceeded; exp != act { 141 | t.Errorf("Wrong error returned: %v != %v", act, exp) 142 | } 143 | }) 144 | } 145 | 146 | func TestCallbackJob(t *testing.T) { 147 | pool := NewCallback(10) 148 | defer pool.Close() 149 | 150 | var counter int32 151 | for i := 0; i < 10; i++ { 152 | ret := pool.Process(func() { 153 | atomic.AddInt32(&counter, 1) 154 | }) 155 | if ret != nil { 156 | t.Errorf("Non-nil callback response: %v", ret) 157 | } 158 | } 159 | 160 | ret := pool.Process("foo") 161 | if exp, act := ErrJobNotFunc, ret; exp != act { 162 | t.Errorf("Wrong result from non-func: %v != %v", act, exp) 163 | } 164 | 165 | if exp, act := int32(10), counter; exp != act { 166 | t.Errorf("Wrong result: %v != %v", act, exp) 167 | } 168 | } 169 | 170 | func TestTimeout(t *testing.T) { 171 | pool := NewFunc(1, func(in interface{}) interface{} { 172 | intVal := in.(int) 173 | <-time.After(time.Millisecond) 174 | return intVal * 2 175 | }) 176 | defer pool.Close() 177 | 178 | _, act := pool.ProcessTimed(10, time.Duration(1)) 179 | if exp := ErrJobTimedOut; exp != act { 180 | t.Errorf("Wrong error returned: %v != %v", act, exp) 181 | } 182 | } 183 | 184 | func TestTimedJobsAfterClose(t *testing.T) { 185 | pool := NewFunc(1, func(in interface{}) interface{} { 186 | return 1 187 | }) 188 | pool.Close() 189 | 190 | _, act := pool.ProcessTimed(10, time.Duration(10*time.Millisecond)) 191 | if exp := ErrPoolNotRunning; exp != act { 192 | t.Errorf("Wrong error returned: %v != %v", act, exp) 193 | } 194 | } 195 | 196 | func TestJobsAfterClose(t *testing.T) { 197 | pool := NewFunc(1, func(in interface{}) interface{} { 198 | return 1 199 | }) 200 | pool.Close() 201 | 202 | defer func() { 203 | if r := recover(); r == nil { 204 | t.Errorf("Process after Stop() did not panic") 205 | } 206 | }() 207 | 208 | pool.Process(10) 209 | } 210 | 211 | func TestParallelJobs(t *testing.T) { 212 | nWorkers := 10 213 | 214 | jobGroup := sync.WaitGroup{} 215 | testGroup := sync.WaitGroup{} 216 | 217 | pool := NewFunc(nWorkers, func(in interface{}) interface{} { 218 | jobGroup.Done() 219 | jobGroup.Wait() 220 | 221 | intVal := in.(int) 222 | return intVal * 2 223 | }) 224 | defer pool.Close() 225 | 226 | for j := 0; j < 1; j++ { 227 | jobGroup.Add(nWorkers) 228 | testGroup.Add(nWorkers) 229 | 230 | for i := 0; i < nWorkers; i++ { 231 | go func() { 232 | ret := pool.Process(10) 233 | if exp, act := 20, ret.(int); exp != act { 234 | t.Errorf("Wrong result: %v != %v", act, exp) 235 | } 236 | testGroup.Done() 237 | }() 238 | } 239 | 240 | testGroup.Wait() 241 | } 242 | } 243 | 244 | //------------------------------------------------------------------------------ 245 | 246 | type mockWorker struct { 247 | blockProcChan chan struct{} 248 | blockReadyChan chan struct{} 249 | interruptChan chan struct{} 250 | terminated bool 251 | } 252 | 253 | func (m *mockWorker) Process(in interface{}) interface{} { 254 | select { 255 | case <-m.blockProcChan: 256 | case <-m.interruptChan: 257 | } 258 | return in 259 | } 260 | 261 | func (m *mockWorker) BlockUntilReady() { 262 | <-m.blockReadyChan 263 | } 264 | 265 | func (m *mockWorker) Interrupt() { 266 | m.interruptChan <- struct{}{} 267 | } 268 | 269 | func (m *mockWorker) Terminate() { 270 | m.terminated = true 271 | } 272 | 273 | func TestCustomWorker(t *testing.T) { 274 | pool := New(1, func() Worker { 275 | return &mockWorker{ 276 | blockProcChan: make(chan struct{}), 277 | blockReadyChan: make(chan struct{}), 278 | interruptChan: make(chan struct{}), 279 | } 280 | }) 281 | 282 | worker1, ok := pool.workers[0].worker.(*mockWorker) 283 | if !ok { 284 | t.Fatal("Wrong type of worker in pool") 285 | } 286 | 287 | if worker1.terminated { 288 | t.Fatal("Worker started off terminated") 289 | } 290 | 291 | _, err := pool.ProcessTimed(10, time.Millisecond) 292 | if exp, act := ErrJobTimedOut, err; exp != act { 293 | t.Errorf("Wrong error: %v != %v", act, exp) 294 | } 295 | 296 | close(worker1.blockReadyChan) 297 | _, err = pool.ProcessTimed(10, time.Millisecond) 298 | if exp, act := ErrJobTimedOut, err; exp != act { 299 | t.Errorf("Wrong error: %v != %v", act, exp) 300 | } 301 | 302 | close(worker1.blockProcChan) 303 | if exp, act := 10, pool.Process(10).(int); exp != act { 304 | t.Errorf("Wrong result: %v != %v", act, exp) 305 | } 306 | 307 | pool.Close() 308 | if !worker1.terminated { 309 | t.Fatal("Worker was not terminated") 310 | } 311 | } 312 | 313 | //------------------------------------------------------------------------------ 314 | 315 | func BenchmarkFuncJob(b *testing.B) { 316 | pool := NewFunc(10, func(in interface{}) interface{} { 317 | intVal := in.(int) 318 | return intVal * 2 319 | }) 320 | defer pool.Close() 321 | 322 | b.ResetTimer() 323 | 324 | for i := 0; i < b.N; i++ { 325 | ret := pool.Process(10) 326 | if exp, act := 20, ret.(int); exp != act { 327 | b.Errorf("Wrong result: %v != %v", act, exp) 328 | } 329 | } 330 | } 331 | 332 | func BenchmarkFuncTimedJob(b *testing.B) { 333 | pool := NewFunc(10, func(in interface{}) interface{} { 334 | intVal := in.(int) 335 | return intVal * 2 336 | }) 337 | defer pool.Close() 338 | 339 | b.ResetTimer() 340 | 341 | for i := 0; i < b.N; i++ { 342 | ret, err := pool.ProcessTimed(10, time.Second) 343 | if err != nil { 344 | b.Error(err) 345 | } 346 | if exp, act := 20, ret.(int); exp != act { 347 | b.Errorf("Wrong result: %v != %v", act, exp) 348 | } 349 | } 350 | } 351 | 352 | //------------------------------------------------------------------------------ 353 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Ashley Jeffs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tunny 22 | 23 | //------------------------------------------------------------------------------ 24 | 25 | // workRequest is a struct containing context representing a workers intention 26 | // to receive a work payload. 27 | type workRequest struct { 28 | // jobChan is used to send the payload to this worker. 29 | jobChan chan<- interface{} 30 | 31 | // retChan is used to read the result from this worker. 32 | retChan <-chan interface{} 33 | 34 | // interruptFunc can be called to cancel a running job. When called it is no 35 | // longer necessary to read from retChan. 36 | interruptFunc func() 37 | } 38 | 39 | //------------------------------------------------------------------------------ 40 | 41 | // workerWrapper takes a Worker implementation and wraps it within a goroutine 42 | // and channel arrangement. The workerWrapper is responsible for managing the 43 | // lifetime of both the Worker and the goroutine. 44 | type workerWrapper struct { 45 | worker Worker 46 | interruptChan chan struct{} 47 | 48 | // reqChan is NOT owned by this type, it is used to send requests for work. 49 | reqChan chan<- workRequest 50 | 51 | // closeChan can be closed in order to cleanly shutdown this worker. 52 | closeChan chan struct{} 53 | 54 | // closedChan is closed by the run() goroutine when it exits. 55 | closedChan chan struct{} 56 | } 57 | 58 | func newWorkerWrapper( 59 | reqChan chan<- workRequest, 60 | worker Worker, 61 | ) *workerWrapper { 62 | w := workerWrapper{ 63 | worker: worker, 64 | interruptChan: make(chan struct{}), 65 | reqChan: reqChan, 66 | closeChan: make(chan struct{}), 67 | closedChan: make(chan struct{}), 68 | } 69 | 70 | go w.run() 71 | 72 | return &w 73 | } 74 | 75 | //------------------------------------------------------------------------------ 76 | 77 | func (w *workerWrapper) interrupt() { 78 | close(w.interruptChan) 79 | w.worker.Interrupt() 80 | } 81 | 82 | func (w *workerWrapper) run() { 83 | jobChan, retChan := make(chan interface{}), make(chan interface{}) 84 | defer func() { 85 | w.worker.Terminate() 86 | close(retChan) 87 | close(w.closedChan) 88 | }() 89 | 90 | for { 91 | // NOTE: Blocking here will prevent the worker from closing down. 92 | w.worker.BlockUntilReady() 93 | select { 94 | case w.reqChan <- workRequest{ 95 | jobChan: jobChan, 96 | retChan: retChan, 97 | interruptFunc: w.interrupt, 98 | }: 99 | select { 100 | case payload := <-jobChan: 101 | result := w.worker.Process(payload) 102 | select { 103 | case retChan <- result: 104 | case <-w.interruptChan: 105 | w.interruptChan = make(chan struct{}) 106 | } 107 | case <-w.interruptChan: 108 | w.interruptChan = make(chan struct{}) 109 | } 110 | case <-w.closeChan: 111 | return 112 | } 113 | } 114 | } 115 | 116 | //------------------------------------------------------------------------------ 117 | 118 | func (w *workerWrapper) stop() { 119 | close(w.closeChan) 120 | } 121 | 122 | func (w *workerWrapper) join() { 123 | <-w.closedChan 124 | } 125 | 126 | //------------------------------------------------------------------------------ 127 | --------------------------------------------------------------------------------