├── .travis.yml ├── examples ├── minimalistic │ └── main.go ├── multiple_parameters │ └── main.go ├── exit_notification │ └── main.go └── signals │ └── main.go ├── LICENSE ├── runner.go ├── README.md └── runner_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7.x 4 | - 1.8.x 5 | - 1.9.x -------------------------------------------------------------------------------- /examples/minimalistic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/raksly/runner" 8 | ) 9 | 10 | func runTCP() { 11 | fmt.Println("Entering runTCP") 12 | time.Sleep(time.Second) 13 | fmt.Println("Exiting runTCP") 14 | } 15 | 16 | func runHTTP() { 17 | fmt.Println("Entering runTCP") 18 | time.Sleep(time.Second) 19 | fmt.Println("Exiting runTCP") 20 | } 21 | 22 | func main() { 23 | var r runner.Runner 24 | 25 | r.Run(runHTTP) 26 | r.Run(runTCP) 27 | 28 | r.Wait() 29 | } 30 | -------------------------------------------------------------------------------- /examples/multiple_parameters/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "syscall" 7 | 8 | "github.com/raksly/runner" 9 | ) 10 | 11 | func runSomething(ctx context.Context, a, b, c int) { 12 | fmt.Println("Entering runSomething", a, b, c) 13 | <-ctx.Done() 14 | fmt.Println("Exiting runSomething") 15 | } 16 | 17 | func main() { 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | r := runner.Runner{Ctx: ctx} 20 | 21 | select { 22 | case <-r.Run(func() { runSomething(ctx, 1, 2, 3) }): 23 | fmt.Println("Exited runSomething") 24 | case sig := <-r.RunSigs(syscall.SIGINT, syscall.SIGTERM): 25 | fmt.Println("Received signal", sig) 26 | } 27 | 28 | cancel() 29 | r.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /examples/exit_notification/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/raksly/runner" 9 | ) 10 | 11 | func runTCP(ctx context.Context) { 12 | fmt.Println("Entering runTCP") 13 | time.Sleep(time.Second) 14 | fmt.Println("Exiting runTCP") 15 | } 16 | 17 | func runHTTP(ctx context.Context) { 18 | fmt.Println("Entering runHTTP") 19 | <-ctx.Done() 20 | fmt.Println("Exiting runHTTP") 21 | } 22 | 23 | func main() { 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | r := runner.Runner{Ctx: ctx} 26 | 27 | select { 28 | case <-r.RunContext(runHTTP): 29 | fmt.Println("Exited runHTTP") 30 | case <-r.RunContext(runTCP): 31 | fmt.Println("Exited runTCP") 32 | } 33 | 34 | cancel() 35 | r.Wait() 36 | } 37 | -------------------------------------------------------------------------------- /examples/signals/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "syscall" 7 | 8 | "github.com/raksly/runner" 9 | ) 10 | 11 | func runTCP(ctx context.Context) { 12 | fmt.Println("Entering runTCP") 13 | <-ctx.Done() 14 | fmt.Println("Exiting runTCP") 15 | } 16 | 17 | func runHTTP(ctx context.Context) { 18 | fmt.Println("Entering runHTTP") 19 | <-ctx.Done() 20 | fmt.Println("Exiting runHTTP") 21 | } 22 | 23 | func main() { 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | r := runner.Runner{Ctx: ctx} 26 | 27 | select { 28 | case <-r.RunContext(runHTTP): 29 | fmt.Println("Exited runHTTP") 30 | case <-r.RunContext(runTCP): 31 | fmt.Println("Exited runTCP") 32 | case sig := <-r.RunSigs(syscall.SIGINT, syscall.SIGTERM): 33 | fmt.Println("Received signal", sig) 34 | } 35 | 36 | cancel() 37 | r.Wait() 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 raksly at protonmail dot com 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 all 11 | 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 THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package runner helps you to not reinvent the wheel on certain type of applications. 4 | Often you begin your application by starting multiple goroutines to do 5 | separate work. Imagine a server accepting http, tcp, and other protocols. 6 | You probably also create WaitGroups and/or channels to sync these goroutines. 7 | Runner does all that for you. 8 | 9 | For examples please view https://github.com/raksly/runner#examples 10 | 11 | */ 12 | package runner 13 | 14 | import ( 15 | "context" 16 | "os" 17 | "os/signal" 18 | "sync" 19 | ) 20 | 21 | // New creates a new Runner 22 | // Deprecated: Use runner.Runner{} 23 | func New(ctx context.Context) Runner { 24 | return Runner{Ctx: ctx} 25 | } 26 | 27 | // Runner runs functions in goroutines with `Run`, `RunContext` and/or 28 | // `RunOtherContext`, returning a channel that is closed when the goroutine 29 | // exits. 30 | // You may wait on all goroutines to finish using `Wait`. 31 | type Runner struct { 32 | // Ctx will be passed to functions called by `RunContext` 33 | Ctx context.Context 34 | wg sync.WaitGroup 35 | } 36 | 37 | // Run runs a function of type func() in a new goroutine. 38 | // The returned channel is closed when f returns 39 | func (r *Runner) Run(f func()) <-chan struct{} { 40 | done := make(chan struct{}) 41 | r.wg.Add(1) 42 | go func() { 43 | defer func() { 44 | r.wg.Done() 45 | close(done) 46 | }() 47 | f() 48 | }() 49 | return done 50 | } 51 | 52 | // RunContext is like `Run`, but passes the context given to `New`. 53 | func (r *Runner) RunContext(f func(context.Context)) <-chan struct{} { 54 | return r.RunOtherContext(r.Ctx, f) 55 | } 56 | 57 | // RunOtherContext is like `RunContext`, except you may specify which 58 | // context to be passed to f. 59 | func (r *Runner) RunOtherContext(ctx context.Context, f func(context.Context)) <-chan struct{} { 60 | return r.Run(func() { f(ctx) }) 61 | } 62 | 63 | // RunSigs is a convenience method to work with OS signals. 64 | // Unlike the other `Run*` functions, the channel returned 65 | // is not closed, but reads the received signal. 66 | func (r *Runner) RunSigs(sigs ...os.Signal) <-chan os.Signal { 67 | sig := make(chan os.Signal, 1) 68 | signal.Notify(sig, sigs...) 69 | return sig 70 | } 71 | 72 | // Wait waits for all goroutines started by this runner. 73 | func (r *Runner) Wait() { 74 | r.wg.Wait() 75 | } 76 | 77 | // Context returns the context given to `New`. 78 | // Deprecated: Access Runner.Ctx directly. 79 | func (r *Runner) Context() context.Context { 80 | return r.Ctx 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/raksly/runner.svg?branch=master)](https://travis-ci.org/raksly/runner) 2 | 3 | Runner helps you to not reinvent the wheel on certain type of applications. 4 | Often you begin your application by starting multiple goroutines to do 5 | separate work. Imagine a server accepting http, tcp, and other protocols. 6 | You probably also create WaitGroups and/or channels to sync these goroutines. 7 | Runner does all that for you. 8 | ## API 9 | The API is promised to never break existing projects using this library. 10 | Documentation available on [GoDoc](https://godoc.org/github.com/raksly/runner). 11 | ## Examples 12 | ### Minimalistic 13 | ```golang 14 | var r runner.Runner 15 | 16 | r.Run(runHTTP) 17 | r.Run(runTCP) 18 | 19 | r.Wait() 20 | ``` 21 | `runHTTP` and `runTCP` are both of type `func()`. `r.Run` will run both functions in separate goroutines, and `r.Wait()` waits until both functions exit. 22 | ### Exit notification 23 | When `runTCP` exits, it might be because the application is supposed to exit alltogether, or there was an irrecoverable error. In that case, you might want HTTP to exit aswell. `Run*` methods return a channel which is closed when its running function returns. 24 | ```golang 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | r := runner.Runner{Ctx: ctx} 27 | 28 | select { 29 | case <-r.RunContext(runHTTP): 30 | case <-r.RunContext(runTCP): 31 | } 32 | 33 | cancel() 34 | r.Wait() 35 | ``` 36 | Both `runHTTP` and `runTCP` are now of type `func(context.Context)` and 37 | are given the context `Runner.Ctx`. If either `runHTTP` or `runTCP` returns, `select` will break, the context will be cancelled, making the other function exit aswell in due time, and `r.Wait()` waits for that. 38 | ### Signals 39 | `Runner` contains a convenience method to work with OS signals 40 | ```golang 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | r := runner.Runner{Ctx: ctx} 43 | 44 | select { 45 | case <-r.RunContext(runHTTP): 46 | case <-r.RunContext(runTCP): 47 | case <-r.RunSigs(syscall.SIGINT, syscall.SIGTERM): 48 | } 49 | 50 | cancel() 51 | r.Wait() 52 | ``` 53 | If either `runHTTP` or `runTCP` returns, or `SIGINT`/`SIGTERM` is received, 54 | the context will be cancelled and we wait for everything to clean up. 55 | ### Multiple parameters 56 | You might want to pass more than just a context to your function. To archive 57 | this, you may use closures 58 | ```golang 59 | ctx, cancel := context.WithCancel(context.Background()) 60 | r := runner.Runner{Ctx: ctx} 61 | 62 | select { 63 | case <-r.Run(func() { runSomething(ctx, 1, 2, 3) }): 64 | case <-r.RunSigs(syscall.SIGINT, syscall.SIGTERM): 65 | } 66 | 67 | cancel() 68 | r.Wait() 69 | ``` 70 | ## License 71 | MIT -------------------------------------------------------------------------------- /runner_test.go: -------------------------------------------------------------------------------- 1 | package runner_test 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "syscall" 7 | "testing" 8 | "time" 9 | 10 | "github.com/raksly/runner" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func run() { 16 | time.Sleep(time.Second) 17 | } 18 | 19 | func runContext(ctx context.Context) { 20 | <-ctx.Done() 21 | } 22 | 23 | func assertChan(assert *assert.Assertions, c interface{}, expectClosed bool) { 24 | if expectClosed { 25 | _, ok := reflect.ValueOf(c).Recv() 26 | assert.False(ok) 27 | } else { 28 | chosen, _, _ := reflect.Select([]reflect.SelectCase{ 29 | reflect.SelectCase{Dir: reflect.SelectDefault}, 30 | reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c)}, 31 | }) 32 | assert.Equal(0, chosen) 33 | } 34 | } 35 | 36 | func Test2(t *testing.T) { 37 | assert := assert.New(t) 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | r := runner.Runner{Ctx: ctx} 41 | 42 | c1 := r.Run(run) 43 | c2 := r.RunContext(runContext) 44 | c3 := r.RunOtherContext(ctx, runContext) 45 | c4 := r.RunSigs(syscall.SIGINT) 46 | 47 | select { 48 | case <-c1: 49 | case <-c2: 50 | assert.Fail("should not run") 51 | case <-c3: 52 | assert.Fail("should not run") 53 | case <-c4: 54 | assert.Fail("should not run") 55 | } 56 | 57 | assertChan(assert, c1, true) 58 | assertChan(assert, c2, false) 59 | assertChan(assert, c3, false) 60 | assertChan(assert, c4, false) 61 | 62 | cancel() 63 | r.Wait() 64 | 65 | assertChan(assert, c2, true) 66 | assertChan(assert, c3, true) 67 | assertChan(assert, c4, false) 68 | 69 | var r2 runner.Runner 70 | 71 | r2.Run(func() {}) 72 | r2.RunContext(func(ctx context.Context) { 73 | assert.Nil(ctx) 74 | }) 75 | 76 | r2.Wait() 77 | } 78 | 79 | // Keeping Test1 to ensure the old api still works 80 | func Test1(t *testing.T) { 81 | assert := assert.New(t) 82 | 83 | ctx, cancel := context.WithCancel(context.Background()) 84 | runner := runner.New(ctx) 85 | 86 | assert.Equal(ctx, runner.Context()) 87 | 88 | c1 := runner.Run(run) 89 | c2 := runner.RunContext(runContext) 90 | c3 := runner.RunOtherContext(ctx, runContext) 91 | c4 := runner.RunSigs(syscall.SIGINT) 92 | 93 | select { 94 | case <-c1: 95 | case <-c2: 96 | assert.Fail("should not run") 97 | case <-c3: 98 | assert.Fail("should not run") 99 | case <-c4: 100 | assert.Fail("should not run") 101 | } 102 | 103 | assertChan(assert, c1, true) 104 | assertChan(assert, c2, false) 105 | assertChan(assert, c3, false) 106 | assertChan(assert, c4, false) 107 | 108 | cancel() 109 | runner.Wait() 110 | 111 | assertChan(assert, c2, true) 112 | assertChan(assert, c3, true) 113 | assertChan(assert, c4, false) 114 | } 115 | --------------------------------------------------------------------------------