diff --git a/event.go b/event.go index f9bb121..d500d36 100644 --- a/event.go +++ b/event.go @@ -2,21 +2,33 @@ package vabastegi import "time" -// Event is what Vabastegi event look like. +// Event is what Vabastegi event looks like. type Event interface { event() // it's private to prevent outside implementation. } -// EventHandlers is list of EventHandler. -type EventHandlers []EventHandler +// eventManager responsible to manage the event system. +type eventManager struct { + handlers []EventHandler +} + +// newEventManager create a new instance of eventManager. +func newEventManager(handlers []EventHandler) *eventManager { + return &eventManager{handlers: handlers} +} // Publish passed event using event handlers. -func (e EventHandlers) Publish(event Event) { - for _, handler := range e { +func (e *eventManager) Publish(event Event) { + for _, handler := range e.handlers { handler.OnEvent(event) } } +// Register event handler. +func (e *eventManager) Register(handler EventHandler) { + e.handlers = append(e.handlers, handler) +} + // EventHandler used if you need to handle the events. type EventHandler interface { OnEvent(event Event) @@ -106,15 +118,12 @@ type OnApplicationShutdownExecuting struct { // ShutdownAt is the time shutdown happened. ShutdownAt time.Time - // Reason is the reason for shutdown the application. - Reason string + // Cause is the reason for shutdown the application. + Cause error } // OnApplicationShutdownExecuted is emitted after the application Shutdown has been executed. type OnApplicationShutdownExecuted struct { - // Reason is the reason for shutdown the application. - Reason string - // Runtime specifies how long it took to run this hook. Runtime time.Duration diff --git a/go.mod b/go.mod index 6a10f26..70ab4e1 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/mrsoftware/vabastegi go 1.18.0 -require github.com/mrsoftware/errors v0.1.0 +require github.com/mrsoftware/errors v0.2.0-alpha.3 diff --git a/go.sum b/go.sum index daa0986..ca7327c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/mrsoftware/errors v0.1.0 h1:5MSHsrsqlBMPbNzwobVt39IpgsVee7LuSe+n8aQWPsI= -github.com/mrsoftware/errors v0.1.0/go.mod h1:iHqx83gamUM9jhiV/rWZuVZe54NVqtqkIDnvZHywSM8= +github.com/mrsoftware/errors v0.2.0-alpha.3 h1:T6UTulgnvEwUF+naTksmd6bO2tHb3hrTKdMm0Wxc0SI= +github.com/mrsoftware/errors v0.2.0-alpha.3/go.mod h1:iHqx83gamUM9jhiV/rWZuVZe54NVqtqkIDnvZHywSM8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/lifecycle.go b/lifecycle.go new file mode 100644 index 0000000..433dcca --- /dev/null +++ b/lifecycle.go @@ -0,0 +1,140 @@ +package vabastegi + +import ( + "context" + "os/signal" + "reflect" + "runtime" + "strings" + "syscall" + "time" + + "github.com/mrsoftware/errors" +) + +// ShutdownListener is what you need to pass to OnShutdown method. +type ShutdownListener = func(ctx context.Context) error + +// lifecycle manage application lifecycle like running task or shutdown. +type lifecycle struct { + listeners []ShutdownListener + publisher *eventManager + ctx context.Context + cancel context.CancelCauseFunc + waitGroup *errors.WaitGroup +} + +// newLifecycle create a new instance of lifecycle. +func newLifecycle(ctx context.Context, publisher *eventManager) *lifecycle { + waitGroup := errors.NewWaitGroup(errors.WaitGroupWithContext(ctx), errors.WaitGroupWithStopOnError()) + + return &lifecycle{ + listeners: make([]ShutdownListener, 0), + publisher: publisher, + ctx: waitGroup.Context(), + cancel: waitGroup.Stop, + waitGroup: waitGroup, + } +} + +// Wait on shutdown or application finish. +func (l *lifecycle) Wait() error { + var err error + select { + case <-l.ctx.Done(): + err = l.ctx.Err() + case err = <-errors.WaitChanel(l.waitGroup): + l.Stop(err) + } + + return l.callShutdownListeners(l.ctx, err) +} + +// RegisterGracefulShutdown start listing on os signal and cancel the parent context on getting one. +func (l *lifecycle) RegisterGracefulShutdown() { + ctx, cancel := signal.NotifyContext(l.ctx, syscall.SIGINT, syscall.SIGTERM) + + l.ctx, l.cancel = ctx, cancelToCancelCause(cancel) +} + +// Stop the application. +func (l *lifecycle) Stop(cause error) { + l.cancel(cause) +} + +// GetContext of lifecycle. +func (l *lifecycle) GetContext() context.Context { + return l.ctx +} + +// do Shut down the application. +func (l *lifecycle) callShutdownListeners(ctx context.Context, cause error) error { + errList := errors.NewMultiError() + + startAt := time.Now() + + l.publisher.Publish(&OnApplicationShutdownExecuting{ + Cause: cause, + ShutdownAt: startAt, + }) + + defer func() { + l.publisher.Publish(&OnApplicationShutdownExecuted{ + Runtime: time.Now().Sub(startAt), + Err: errList.Err(), + }) + }() + + for _, fn := range l.listeners { + errList.Add(l.shutdown(ctx, fn)) + } + + return errList.Err() +} + +func (l *lifecycle) shutdown(ctx context.Context, callback ShutdownListener) (err error) { + startAt := time.Now() + + l.publisher.Publish(&OnShutdownExecuting{ + ProviderName: getProviderName(callback, 1), + CallerPath: getProviderName(callback, -1), + ShutdownAt: startAt, + }) + + defer func() { + l.publisher.Publish(&OnShutdownExecuted{ + ProviderName: getProviderName(callback, 1), + CallerPath: getProviderName(callback, -1), + Runtime: time.Now().Sub(startAt), + Err: err, + }) + }() + + return callback(ctx) +} + +// OnShutdown add callback to a list of listeners. +func (l *lifecycle) OnShutdown(callback ShutdownListener) { + l.listeners = append(l.listeners, callback) +} + +// RunTask in the background. +func (l *lifecycle) RunTask(ctx context.Context, fn func(ctx context.Context) error) { + l.waitGroup.DoWithContext(ctx, fn) +} + +func getProviderName(creator interface{}, index int) string { + reference := runtime.FuncForPC(reflect.ValueOf(creator).Pointer()).Name() + if index == -1 { + return reference + } + + parts := strings.Split(reference, ".") + + return parts[len(parts)-(1+index)] +} + +// cancelToCancelCause is just a wrapper to turn context.CancelFunc into context.CancelCauseFunc. +func cancelToCancelCause(cancelFunc context.CancelFunc) context.CancelCauseFunc { + return func(cause error) { cancelFunc() } +} diff --git a/logger.go b/logger.go index 9fb8be2..2461b6d 100644 --- a/logger.go +++ b/logger.go @@ -75,7 +75,7 @@ func (l *EventLogger) OnEvent(event Event) { case *OnShutdownExecuting: l.log(InfoLogLevel, "Shutting Down %s", e.ProviderName) case *OnApplicationShutdownExecuting: - l.log(InfoLogLevel, "Shutting Down Application: %s", e.Reason) + l.log(InfoLogLevel, "Shutting Down Application: %s", e.Cause) case *OnLog: l.log(e.Level, e.Message, e.Args...) } diff --git a/options.go b/options.go index cea9727..9983e27 100644 --- a/options.go +++ b/options.go @@ -1,10 +1,13 @@ package vabastegi +import "context" + // Options of Vabastegi. type Options struct { GracefulShutdown bool AppName string - EventHandlers EventHandlers + EventHandlers []EventHandler + Ctx context.Context } // Option of Vabastegi. @@ -30,3 +33,10 @@ func WithEventHandlers(handlers ...EventHandler) Option { options.EventHandlers = append(options.EventHandlers, handlers...) } } + +// WithContext used if you need to pass custom context. +func WithContext(ctx context.Context) Option { + return func(options *Options) { + options.Ctx = ctx + } +} diff --git a/vabastegi.go b/vabastegi.go index 0eb07d0..fb7ba92 100644 --- a/vabastegi.go +++ b/vabastegi.go @@ -2,15 +2,7 @@ package vabastegi import ( "context" - "os" - "os/signal" - "reflect" - "runtime" - "strings" - "sync" "time" - - "github.com/mrsoftware/errors" ) // Provider is a dependency provider for application. @@ -18,55 +10,51 @@ type Provider[T any] func(context.Context, *App[T]) error // App is the dependency injection manger. type App[T any] struct { - waitGroup sync.WaitGroup - errors *errors.MultiError - onShutdown []func(ctx context.Context) error - Hub T - options Options - backgroundTasksCount int - graceFullOnce sync.Once + Hub T + options Options + Events *eventManager + Lifecycle *lifecycle } // New instance of App Dependency management. func New[T any](options ...Option) *App[T] { - app := App[T]{ - errors: errors.NewMultiError(), - options: Options{EventHandlers: make(EventHandlers, 0)}, - } - app.UpdateOptions(options...) - - return &app -} + op := Options{Ctx: context.Background()} -// UpdateOptions is used if you want to change any options for App. -func (a *App[ـ]) UpdateOptions(options ...Option) { for _, option := range options { - option(&a.options) + option(&op) } - if a.options.GracefulShutdown { - a.registerGracefulShutdown() + eManager := newEventManager(op.EventHandlers) + life := newLifecycle(op.Ctx, eManager) + + if op.GracefulShutdown { + life.RegisterGracefulShutdown() } + + return &App[T]{options: op, Events: eManager, Lifecycle: life} } // Builds the dependency structure of your app. -func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) (err error) { +func (a *App[T]) Builds(providers ...Provider[T]) (err error) { + defer func() { + if err == nil { + return + } + + a.Lifecycle.Stop(err) + }() + startAt := time.Now() - a.options.EventHandlers.Publish(&OnBuildsExecuting{BuildAt: startAt}) + a.Events.Publish(&OnBuildsExecuting{BuildAt: startAt}) - defer func() { - a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{Runtime: time.Since(startAt), Err: err}) - }() + defer func() { a.Events.Publish(&OnApplicationShutdownExecuted{Runtime: time.Since(startAt), Err: err}) }() for _, provider := range providers { - err := a.Build(ctx, provider) - if err == nil { + if err = a.Build(a.Lifecycle.GetContext(), provider); err == nil { continue } - a.Shutdown(ctx, "Provider Failure") - return err } @@ -77,16 +65,16 @@ func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) (err erro func (a *App[T]) Build(ctx context.Context, provider Provider[T]) (err error) { startAt := time.Now() - a.options.EventHandlers.Publish(&OnBuildExecuting{ - ProviderName: a.getProviderName(provider, 0), - CallerPath: a.getProviderName(provider, -1), + a.Events.Publish(&OnBuildExecuting{ + ProviderName: getProviderName(provider, 0), + CallerPath: getProviderName(provider, -1), BuildAt: startAt, }) defer func() { - a.options.EventHandlers.Publish(&OnBuildExecuted{ - ProviderName: a.getProviderName(provider, 0), - CallerPath: a.getProviderName(provider, -1), + a.Events.Publish(&OnBuildExecuted{ + ProviderName: getProviderName(provider, 0), + CallerPath: getProviderName(provider, -1), Runtime: time.Now().Sub(startAt), Err: err, }) @@ -95,98 +83,12 @@ func (a *App[T]) Build(ctx context.Context, provider Provider[T]) (err error) { return provider(ctx, a) } -// RunTask in background. -func (a *App[ـ]) RunTask(fn func()) { - go fn() - - a.backgroundTasksCount++ - - a.waitGroup.Add(1) -} - -// Wait for background task to done or any shutdown signal. -func (a *App[ـ]) Wait() error { - a.waitGroup.Wait() - - return a.errors.Err() -} - -// Shutdown ths application. -func (a *App[ـ]) Shutdown(ctx context.Context, reason string) { - startAt := time.Now() - - a.options.EventHandlers.Publish(&OnApplicationShutdownExecuting{ - Reason: reason, - ShutdownAt: startAt, - }) - - defer func() { - a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{ - Reason: reason, - Runtime: time.Now().Sub(startAt), - Err: a.errors.Err(), - }) - }() - - for _, fn := range a.onShutdown { - a.errors.Add(a.shutdown(ctx, fn)) - } - - for i := 0; i < a.backgroundTasksCount; i++ { - a.waitGroup.Done() - } -} - -func (a *App[ـ]) shutdown(ctx context.Context, fn func(context.Context) error) (err error) { - startAt := time.Now() - - a.options.EventHandlers.Publish(&OnShutdownExecuting{ - ProviderName: a.getProviderName(fn, 1), - CallerPath: a.getProviderName(fn, -1), - ShutdownAt: startAt, - }) - - defer func() { - a.options.EventHandlers.Publish(&OnShutdownExecuted{ - ProviderName: a.getProviderName(fn, 1), - CallerPath: a.getProviderName(fn, -1), - Runtime: time.Now().Sub(startAt), - Err: err, - }) - }() - - return fn(ctx) -} - -// OnShutdown register any method for Shutdown method. -func (a *App[ـ]) OnShutdown(fn func(ctx context.Context) error) { - a.onShutdown = append(a.onShutdown, fn) -} - -func (a *App[ـ]) getProviderName(creator interface{}, index int) string { - reference := runtime.FuncForPC(reflect.ValueOf(creator).Pointer()).Name() - if index == -1 { - return reference - } - - parts := strings.Split(reference, ".") - - return parts[len(parts)-(1+index)] -} - // Log the message. func (a *App[ـ]) Log(level logLevel, message string, args ...interface{}) { - a.options.EventHandlers.Publish(&OnLog{LogAt: time.Now(), Level: level, Message: message, Args: args}) + a.Events.Publish(&OnLog{LogAt: time.Now(), Level: level, Message: message, Args: args}) } -func (a *App[ـ]) registerGracefulShutdown() { - a.graceFullOnce.Do(func() { - interruptChan := make(chan os.Signal, 1) - signal.Notify(interruptChan, os.Interrupt) - - go func() { - appSignal := <-interruptChan - a.Shutdown(context.Background(), appSignal.String()) - }() - }) +// Wait for the application lifecycle to finish. +func (a *App[_]) Wait() error { + return a.Lifecycle.Wait() }