From c691d422395cb72283512d8956a255db10b70b44 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Thu, 4 Apr 2019 22:56:53 +0300 Subject: [PATCH] v0.4.3 Added: - Hooks for lifecycle events Fixed: - Close errors channel - Small fixes --- README.md | 21 +++++++++++++ event_string.go | 29 +++++++++++++++++ events.go | 22 +++++++++++++ example/policies.go | 2 +- mixins.go | 2 +- rutina.go | 76 +++++++++++++++++++++++++++++---------------- 6 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 event_string.go create mode 100644 events.go diff --git a/README.md b/README.md index b3c5ceb..fa1a88f 100755 --- a/README.md +++ b/README.md @@ -96,6 +96,27 @@ err := <- r.Errors() Disabled by default. Use `r.With(rutina.WithErrChan())` to turn on. +## Events and hooks + +Rutina has own simple lifecycle events system. You can subscribe your hooks on any of this events: + +* `EventRoutineStart` - Fires when starts new routine +* `EventRoutineStop` - Fires when routine stopped with any result +* `EventRoutineComplete` - Fires when routine stopped without errors +* `EventRoutineFail` - Fires when routine stopped with error +* `EventAppStop` - Fires when all routines stopped with any result +* `EventAppComplete` - Fires when all routines stopped with no errors +* `EventAppFail` - Fires when all routines stopped with error + +Example: + +```go +r.RegisterHook(rutina.EventRoutineStart, func(ev rutina.Event, rid int) error { + log.Println("Started routine with ID", rid) + return nil +}) +``` + ## Mixins ### Usage diff --git a/event_string.go b/event_string.go new file mode 100644 index 0000000..05f1b7a --- /dev/null +++ b/event_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=Event"; DO NOT EDIT. + +package rutina + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[EventRoutineStart-0] + _ = x[EventRoutineStop-1] + _ = x[EventRoutineComplete-2] + _ = x[EventRoutineFail-3] + _ = x[EventAppStop-4] + _ = x[EventAppComplete-5] + _ = x[EventAppFail-6] +} + +const _Event_name = "EventRoutineStartEventRoutineStopEventRoutineCompleteEventRoutineFailEventAppStopEventAppCompleteEventAppFail" + +var _Event_index = [...]uint8{0, 17, 33, 53, 69, 81, 97, 109} + +func (i Event) String() string { + if i < 0 || i >= Event(len(_Event_index)-1) { + return "Event(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Event_name[_Event_index[i]:_Event_index[i+1]] +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..d435e62 --- /dev/null +++ b/events.go @@ -0,0 +1,22 @@ +//go:generate stringer -type=Event +package rutina + +// Event represents lifecycle events +type Event int + +const ( + EventRoutineStart Event = iota + EventRoutineStop + EventRoutineComplete + EventRoutineFail + EventAppStop + EventAppComplete + EventAppFail +) + +// Hook is function that calls when event fired +// Params: +// ev Event - fired event +// r *Rutina - pointer to rutina +// rid int - ID of routine if present, 0 - otherwise +type Hook func(ev Event, r *Rutina, rid int) error diff --git a/example/policies.go b/example/policies.go index 7a81359..3e14f5a 100644 --- a/example/policies.go +++ b/example/policies.go @@ -15,7 +15,7 @@ func main() { // New instance with builtin context r := rutina.New() - r = r.With(rutina.WithErrChan()) + r = r.With(rutina.WithErrChan(), rutina.WithStdLogger()) r.Go(func(ctx context.Context) error { <-time.After(1 * time.Second) diff --git a/mixins.go b/mixins.go index 1cc2c76..6688f36 100755 --- a/mixins.go +++ b/mixins.go @@ -39,7 +39,7 @@ func WithLogger(logger *log.Logger) *MixinLogger { // WithStdLogger adds standard logger to rutina func WithStdLogger() *MixinLogger { - return &MixinLogger{Logger: log.New(os.Stdout, "rutina", log.LstdFlags)} + return &MixinLogger{Logger: log.New(os.Stdout, "[rutina]", log.LstdFlags)} } func (o MixinLogger) apply(r *Rutina) { diff --git a/rutina.go b/rutina.go index 9531cbb..e4ef1a7 100755 --- a/rutina.go +++ b/rutina.go @@ -12,21 +12,23 @@ import ( //Rutina is routine manager type Rutina struct { - ctx context.Context // State of application (started/stopped) - Cancel func() // Cancel func that stops all routines - wg sync.WaitGroup // WaitGroup that wait all routines to complete - o sync.Once // Flag that prevents overwrite first error that shutdowns all routines - err error // First error that shutdowns all routines - logger *log.Logger // Optional 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 + ctx context.Context // State of application (started/stopped) + Cancel func() // Cancel func that stops all routines + wg sync.WaitGroup // WaitGroup that wait all routines to complete + onceErr sync.Once // Flag that prevents overwrite first error that shutdowns all routines + onceWait sync.Once // Flag that prevents wait already waited rutina + err error // First error that shutdowns all routines + logger *log.Logger // Optional 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 + hooks map[Event][]Hook // Lifecycle hooks } // New instance with builtin context func New(mixins ...Mixin) *Rutina { ctx, cancel := context.WithCancel(context.Background()) var counter uint64 - r := &Rutina{ctx: ctx, Cancel: cancel, counter: &counter, errCh: nil} + r := &Rutina{ctx: ctx, Cancel: cancel, counter: &counter, errCh: nil, hooks: map[Event][]Hook{}} return r.With(mixins...) } @@ -38,8 +40,16 @@ func (r *Rutina) With(mixins ...Mixin) *Rutina { return r } +func (r *Rutina) RegisterHook(ev Event, hook Hook) { + r.hooks[ev] = append(r.hooks[ev], hook) +} + // Go routine func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) { + // Check that context is not canceled yet + if r.ctx.Err() != nil { + return + } onFail := ShutdownIfFail for _, o := range opts { switch o { @@ -66,49 +76,39 @@ func (r *Rutina) Go(doer func(ctx context.Context) error, opts ...Options) { r.wg.Add(1) go func() { defer r.wg.Done() - // Check that context is not canceled yet - if r.ctx.Err() != nil { - return - } id := atomic.AddUint64(r.counter, 1) - r.log("starting #%d", id) + r.fire(EventRoutineStart, int(id)) if err := doer(r.ctx); err != nil { + r.fire(EventRoutineFail, int(id)) + r.fire(EventRoutineStop, int(id)) // errors history if r.errCh != nil { r.errCh <- err } // region routine failed - r.log("error at #%d : %v", id, err) switch onFail { case ShutdownIfFail: - r.log("stopping #%d", id) // Save error only if shutdown all routines - r.o.Do(func() { + r.onceErr.Do(func() { r.err = err }) r.Cancel() case RestartIfFail: - r.log("restarting #%d", id) r.Go(doer, opts...) - case DoNothingIfFail: - r.log("stopping #%d", id) } // endregion } else { + r.fire(EventRoutineComplete, int(id)) + r.fire(EventRoutineStop, int(id)) // region routine successfully done switch onDone { case ShutdownIfDone: - r.log("stopping #%d with shutdown", id) r.Cancel() case RestartIfDone: - r.log("restarting #%d", id) r.Go(doer, opts...) - case DoNothingIfDone: - r.log("stopping #%d", id) } // endregion } - }() } @@ -138,10 +138,34 @@ func (r *Rutina) ListenOsSignals(signals ...os.Signal) { // Wait all routines and returns first error or nil if all routines completes without errors func (r *Rutina) Wait() error { - r.wg.Wait() + r.onceWait.Do(func() { + r.wg.Wait() + r.fire(EventAppStop, 0) + if r.err == nil { + r.fire(EventAppComplete, 0) + } else { + r.fire(EventAppFail, 0) + } + if r.errCh != nil { + close(r.errCh) + } + }) return r.err } +func (r *Rutina) fire(ev Event, rid int) { + r.log("Event = %s Routine ID = %d", ev.String(), rid) + if hooks, ok := r.hooks[ev]; ok == true { + for _, h := range hooks { + if err := h(ev, r, rid); err != nil { + if r.errCh != nil { + r.errCh <- err + } + } + } + } +} + // Log if can func (r *Rutina) log(format string, args ...interface{}) { if r.logger != nil {