Auto listen OS signal (with mixin)
Policies: ...Fail -> ...Error
This commit is contained in:
Alexander Kiryukhin 2019-06-29 01:19:21 +03:00
parent 741cf397a9
commit 01eeeaf5e1
7 changed files with 76 additions and 40 deletions

View file

@ -7,7 +7,7 @@ Package Rutina (russian "рутина" - ordinary boring everyday work) is routi
It seems like https://godoc.org/golang.org/x/sync/errgroup with some different: It seems like https://godoc.org/golang.org/x/sync/errgroup with some different:
1) propagates context to every routines. So routine can check if context stopped (`ctx.Done()`). 1) propagates context to every routines. So routine can check if context stopped (`ctx.Done()`).
2) has flexible run/stop policy. i.e. one routine restarts when it fails (useful on daemons) but if fails another - all routines will be cancelled 2) has flexible run/stop policy. i.e. one routine restarts when it errors (useful on daemons) but if errors another - all routines will be cancelled
3) already has optional signal handler `ListenOsSignals()` 3) already has optional signal handler `ListenOsSignals()`
## When it need? ## When it need?
@ -42,16 +42,16 @@ r.Go(func (ctx context.Context) error {
Available options of run policy: Available options of run policy:
* `ShutdownIfFail` - Shutdown all routines if this routine fails * `ShutdownIfError` - Shutdown all routines if this routine returns error
* `RestartIfFail` - Restart this routine if it fail * `RestartIfError` - Restart this routine if this routine returns error
* `DoNothingIfFail` - Do nothing just stop this routine if it fail * `DoNothingIfError` - Do nothing just stop this routine if this routine returns error
* `ShutdownIfDone` - Shutdown all routines if this routine done without errors * `ShutdownIfDone` - Shutdown all routines if this routine done without errors
* `RestartIfDone` - Restart if this routine done without errors * `RestartIfDone` - Restart if this routine done without errors
* `DoNothingIfDone` - Do nothing if this routine done without errors * `DoNothingIfDone` - Do nothing if this routine done without errors
Default policy: Default policy:
`ShutdownIfFail` && `ShutdownIfDone` `ShutdownIfError` && `ShutdownIfDone`
(just like [errgroup](https://godoc.org/golang.org/x/sync/errgroup)) (just like [errgroup](https://godoc.org/golang.org/x/sync/errgroup))
@ -61,17 +61,17 @@ Default policy:
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
// If this routine produce no error - it just restarts // If this routine produce no error - it just restarts
// If it returns error - all other routines will shutdown (because context cancels) // If it returns error - all other routines will shutdown (because context cancels)
}, rutina.RestartIfDone, rutina.ShutdownIfFail) }, rutina.RestartIfDone, rutina.ShutdownIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
// If this routine produce no error - it just completes // If this routine produce no error - it just completes
// If it returns error - all other routines will shutdown (because context cancels) // If it returns error - all other routines will shutdown (because context cancels)
}, rutina.DoNothingIfDone, rutina.ShutdownIfFail) }, rutina.DoNothingIfDone, rutina.ShutdownIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
// If this routine produce no error - all other routines will shutdown (because context cancels) // If this routine produce no error - all other routines will shutdown (because context cancels)
// If it returns error - it will be restarted // If it returns error - it will be restarted
}, rutina.RestartIfFail) }, rutina.RestartIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
// If this routine stopped by any case - all other routines will shutdown (because context cancels) // If this routine stopped by any case - all other routines will shutdown (because context cancels)
@ -103,10 +103,10 @@ Rutina has own simple lifecycle events:
* `EventRoutineStart` - Fires when starts new routine * `EventRoutineStart` - Fires when starts new routine
* `EventRoutineStop` - Fires when routine stopped with any result * `EventRoutineStop` - Fires when routine stopped with any result
* `EventRoutineComplete` - Fires when routine stopped without errors * `EventRoutineComplete` - Fires when routine stopped without errors
* `EventRoutineFail` - Fires when routine stopped with error * `EventRoutineError` - Fires when routine stopped with error
* `EventAppStop` - Fires when all routines stopped with any result * `EventAppStop` - Fires when all routines stopped with any result
* `EventAppComplete` - Fires when all routines stopped with no errors * `EventAppComplete` - Fires when all routines stopped with no errors
* `EventAppFail` - Fires when all routines stopped with error * `EventAppError` - Fires when all routines stopped with error
## Mixins ## Mixins
@ -151,6 +151,22 @@ err := <- r.Errors()
Turn on errors channel Turn on errors channel
### Lifecycle listener
```go
r = r.With(rutina.WithLifecycleListener(func (event rutina.Event, rid int) { ... }))
```
Simple lifecycle listener
### Auto listen OS signals
```go
r = r.With(rutina.WithListenOsSignals())
```
Automatically listen OS signals. There is no `r.ListenOsSignals()` needed.
## Example ## Example
HTTP server with graceful shutdown [`example/http_server.go`](https://github.com/NeonXP/rutina/blob/master/example/http_server.go) HTTP server with graceful shutdown [`example/http_server.go`](https://github.com/NeonXP/rutina/blob/master/example/http_server.go)

View file

@ -11,15 +11,15 @@ func _() {
_ = x[EventRoutineStart-0] _ = x[EventRoutineStart-0]
_ = x[EventRoutineStop-1] _ = x[EventRoutineStop-1]
_ = x[EventRoutineComplete-2] _ = x[EventRoutineComplete-2]
_ = x[EventRoutineFail-3] _ = x[EventRoutineError-3]
_ = x[EventAppStop-4] _ = x[EventAppStop-4]
_ = x[EventAppComplete-5] _ = x[EventAppComplete-5]
_ = x[EventAppFail-6] _ = x[EventAppError-6]
} }
const _Event_name = "EventRoutineStartEventRoutineStopEventRoutineCompleteEventRoutineFailEventAppStopEventAppCompleteEventAppFail" const _Event_name = "EventRoutineStartEventRoutineStopEventRoutineCompleteEventRoutineErrorEventAppStopEventAppCompleteEventAppError"
var _Event_index = [...]uint8{0, 17, 33, 53, 69, 81, 97, 109} var _Event_index = [...]uint8{0, 17, 33, 53, 70, 82, 98, 111}
func (i Event) String() string { func (i Event) String() string {
if i < 0 || i >= Event(len(_Event_index)-1) { if i < 0 || i >= Event(len(_Event_index)-1) {

View file

@ -8,10 +8,10 @@ const (
EventRoutineStart Event = iota EventRoutineStart Event = iota
EventRoutineStop EventRoutineStop
EventRoutineComplete EventRoutineComplete
EventRoutineFail EventRoutineError
EventAppStop EventAppStop
EventAppComplete EventAppComplete
EventAppFail EventAppError
) )
// Hook is function that calls when event fired // Hook is function that calls when event fired

View file

@ -21,31 +21,31 @@ func main() {
<-time.After(1 * time.Second) <-time.After(1 * time.Second)
log.Println("Do something 1 second without errors and restart") log.Println("Do something 1 second without errors and restart")
return nil return nil
}, rutina.RestartIfDone, rutina.ShutdownIfFail) }, rutina.RestartIfDone, rutina.ShutdownIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
<-time.After(2 * time.Second) <-time.After(2 * time.Second)
log.Println("Do something 2 seconds without errors and do nothing") log.Println("Do something 2 seconds without errors and do nothing")
return nil return nil
}, rutina.DoNothingIfDone, rutina.ShutdownIfFail) }, rutina.DoNothingIfDone, rutina.ShutdownIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
<-time.After(3 * time.Second) <-time.After(3 * time.Second)
log.Println("Do something 3 seconds with error and restart") log.Println("Do something 3 seconds with error and restart")
return errors.New("Error #1!") return errors.New("Error #1!")
}, rutina.RestartIfFail) }, rutina.RestartIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
<-time.After(4 * time.Second) <-time.After(4 * time.Second)
log.Println("Do something 4 seconds with error and do nothing") log.Println("Do something 4 seconds with error and do nothing")
return errors.New("Error #2!") return errors.New("Error #2!")
}, rutina.DoNothingIfFail) }, rutina.DoNothingIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
<-time.After(10 * time.Second) <-time.After(10 * time.Second)
log.Println("Do something 10 seconds with error and close context") log.Println("Do something 10 seconds with error and close context")
return errors.New("Successfully shutdown at proper place") return errors.New("Successfully shutdown at proper place")
}, rutina.ShutdownIfFail) }, rutina.ShutdownIfError)
r.Go(func(ctx context.Context) error { r.Go(func(ctx context.Context) error {
for { for {

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"log" "log"
"os" "os"
"syscall"
) )
// Mixin interface // Mixin interface
@ -72,3 +73,18 @@ func (l LifecycleMixin) apply(r *Rutina) {
func WithLifecycleListener(listener LifecycleListener) *LifecycleMixin { func WithLifecycleListener(listener LifecycleListener) *LifecycleMixin {
return &LifecycleMixin{Listener: listener} return &LifecycleMixin{Listener: listener}
} }
type ListenOsSignalsMixin struct {
Signals []os.Signal
}
func (l ListenOsSignalsMixin) apply(r *Rutina) {
r.autoListenSignals = l.Signals
}
func WithListenOsSignals(signals ...os.Signal) *ListenOsSignalsMixin {
if len(signals) == 0 {
signals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
}
return &ListenOsSignalsMixin{Signals: signals}
}

View file

@ -4,9 +4,9 @@ package rutina
type Options int type Options int
const ( const (
ShutdownIfFail Options = iota // Shutdown all routines if fail ShutdownIfError Options = iota // Shutdown all routines if fail
RestartIfFail // Restart this routine if fail RestartIfError // Restart this routine if fail
DoNothingIfFail // Do nothing on fail DoNothingIfError // Do nothing on fail
ShutdownIfDone // Shutdown all routines if this routine done without errors ShutdownIfDone // Shutdown all routines if this routine done without errors
RestartIfDone // Restart if this routine done without errors RestartIfDone // Restart if this routine done without errors
DoNothingIfDone // Do nothing if this routine done without errors DoNothingIfDone // Do nothing if this routine done without errors

View file

@ -20,8 +20,9 @@ type Rutina struct {
err error // First error that shutdowns all routines err error // First error that shutdowns all routines
logger *log.Logger // Optional logger logger *log.Logger // Optional logger
counter *uint64 // Optional counter that names routines with increment ids for debug purposes at logger counter *uint64 // Optional counter that names routines with increment ids for debug purposes at logger
errCh chan error // Optional channel for errors when RestartIfFail and DoNothingIfFail errCh chan error // Optional channel for errors when RestartIfError and DoNothingIfError
lifecycleListener LifecycleListener // Optional listener for events lifecycleListener LifecycleListener // Optional listener for events
autoListenSignals []os.Signal // Optional listening os signals, default disabled
} }
// New instance with builtin context // New instance with builtin context
@ -37,6 +38,9 @@ func (r *Rutina) With(mixins ...Mixin) *Rutina {
for _, m := range mixins { for _, m := range mixins {
m.apply(r) m.apply(r)
} }
if r.autoListenSignals != nil {
r.ListenOsSignals(r.autoListenSignals...)
}
return r return r
} }
@ -46,15 +50,15 @@ func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) {
if r.ctx.Err() != nil { if r.ctx.Err() != nil {
return return
} }
onFail := ShutdownIfFail onFail := ShutdownIfError
for _, o := range opts { for _, o := range opts {
switch o { switch o {
case ShutdownIfFail: case ShutdownIfError:
onFail = ShutdownIfFail onFail = ShutdownIfError
case RestartIfFail: case RestartIfError:
onFail = RestartIfFail onFail = RestartIfError
case DoNothingIfFail: case DoNothingIfError:
onFail = DoNothingIfFail onFail = DoNothingIfError
} }
} }
onDone := ShutdownIfDone onDone := ShutdownIfDone
@ -75,7 +79,7 @@ func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) {
id := atomic.AddUint64(r.counter, 1) id := atomic.AddUint64(r.counter, 1)
r.lifecycleEvent(EventRoutineStart, int(id)) r.lifecycleEvent(EventRoutineStart, int(id))
if err := doer(r.ctx); err != nil { if err := doer(r.ctx); err != nil {
r.lifecycleEvent(EventRoutineFail, int(id)) r.lifecycleEvent(EventRoutineError, int(id))
r.lifecycleEvent(EventRoutineStop, int(id)) r.lifecycleEvent(EventRoutineStop, int(id))
// errors history // errors history
if r.errCh != nil { if r.errCh != nil {
@ -83,13 +87,13 @@ func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) {
} }
// region routine failed // region routine failed
switch onFail { switch onFail {
case ShutdownIfFail: case ShutdownIfError:
// Save error only if shutdown all routines // Save error only if shutdown all routines
r.onceErr.Do(func() { r.onceErr.Do(func() {
r.err = err r.err = err
}) })
r.Cancel() r.Cancel()
case RestartIfFail: case RestartIfError:
r.Go(doer, opts...) r.Go(doer, opts...)
} }
// endregion // endregion
@ -108,7 +112,7 @@ func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) {
}() }()
} }
// Errors returns chan for all errors, event if DoNothingIfFail or RestartIfFail set. // Errors returns chan for all errors, event if DoNothingIfError or RestartIfError set.
// By default it nil. Use MixinErrChan to turn it on // By default it nil. Use MixinErrChan to turn it on
func (r *Rutina) Errors() <-chan error { func (r *Rutina) Errors() <-chan error {
return r.errCh return r.errCh
@ -140,7 +144,7 @@ func (r *Rutina) Wait() error {
if r.err == nil { if r.err == nil {
r.lifecycleEvent(EventAppComplete, 0) r.lifecycleEvent(EventAppComplete, 0)
} else { } else {
r.lifecycleEvent(EventAppFail, 0) r.lifecycleEvent(EventAppError, 0)
} }
if r.errCh != nil { if r.errCh != nil {
close(r.errCh) close(r.errCh)