keel/server.go
2021-11-16 15:31:00 +01:00

201 lines
4.6 KiB
Go

package keel
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"github.com/foomo/keel/config"
"github.com/foomo/keel/log"
"github.com/foomo/keel/telemetry"
)
// Server struct
type Server struct {
services []Service
shutdownTimeout time.Duration
closers []interface{}
ctx context.Context
l *zap.Logger
c *viper.Viper
}
func NewServer(opts ...Option) *Server {
inst := &Server{
shutdownTimeout: 5 * time.Second,
ctx: context.Background(),
c: config.Config(),
l: log.Logger(),
}
for _, opt := range opts {
opt(inst)
}
return inst
}
// Logger returns server logger
func (s *Server) Logger() *zap.Logger {
return s.l
}
// Config returns server config
func (s *Server) Config() *viper.Viper {
return s.c
}
// Context returns server context
func (s *Server) Context() context.Context {
return s.ctx
}
// AddServices adds multiple service
func (s *Server) AddServices(services ...Service) {
for _, service := range services {
s.AddService(service)
}
}
// AddService add a single service
func (s *Server) AddService(service Service) {
for _, value := range s.services {
if value == service {
return
}
}
s.services = append(s.services, service)
}
// AddClosers adds an closer to be called on shutdown
func (s *Server) AddClosers(closers ...interface{}) {
for _, closer := range closers {
s.AddCloser(closer)
}
}
// AddCloser adds a closer to be called on shutdown
func (s *Server) AddCloser(closer interface{}) {
switch closer.(type) {
case Closer,
ErrorCloser,
CloserWithContext,
ErrorCloserWithContext,
Shutdowner,
ErrorShutdowner,
ShutdownerWithContext,
ErrorShutdownerWithContext,
Unsubscriber,
ErrorUnsubscriber,
UnsubscriberWithContext,
ErrorUnsubscriberWithContext:
s.closers = append(s.closers, closer)
default:
s.l.Warn("unable to add closer")
}
}
// Run runs the server
func (s *Server) Run() {
s.l.Info("starting server")
ctx, stop := signal.NotifyContext(s.ctx, os.Interrupt)
defer stop()
g, gctx := errgroup.WithContext(ctx)
for _, service := range s.services {
service := service
g.Go(func() error {
if err := service.Start(s.ctx); errors.Is(err, http.ErrServerClosed) {
log.WithError(s.l, err).Debug("server has closed")
} else if err != nil {
log.WithError(s.l, err).Error("failed to start service")
return err
}
return nil
})
// register started service
s.AddCloser(service)
}
// gracefully shutdown servers
g.Go(func() error {
<-gctx.Done()
s.l.Debug("gracefully stopping closers...")
timeoutCtx, timeoutCancel := context.WithTimeout(
context.Background(),
s.shutdownTimeout,
)
defer timeoutCancel()
// append internal closers
closers := append(s.closers, telemetry.Exporter(), telemetry.Controller()) //nolint:gocritic
for _, closer := range closers {
switch c := closer.(type) {
case Closer:
c.Close()
case ErrorCloser:
if err := c.Close(); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorCloser")
continue
}
case CloserWithContext:
c.Close(timeoutCtx)
case ErrorCloserWithContext:
if err := c.Close(timeoutCtx); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorCloserWithContext")
continue
}
case Shutdowner:
c.Shutdown()
case ErrorShutdowner:
if err := c.Shutdown(); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorShutdowner")
continue
}
case ShutdownerWithContext:
c.Shutdown(timeoutCtx)
case ErrorShutdownerWithContext:
if err := c.Shutdown(timeoutCtx); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorShutdownerWithContext")
continue
}
case Unsubscriber:
c.Unsubscribe()
case ErrorUnsubscriber:
if err := c.Unsubscribe(); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorUnsubscriber")
continue
}
case UnsubscriberWithContext:
c.Unsubscribe(timeoutCtx)
case ErrorUnsubscriberWithContext:
if err := c.Unsubscribe(timeoutCtx); err != nil {
log.WithError(s.l, err).Error("failed to gracefully stop ErrorUnsubscriberWithContext")
continue
}
}
s.l.Info("stopped registered closer", log.FName(fmt.Sprintf("%T", closer)))
}
return gctx.Err()
})
// wait for shutdown
if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
log.WithError(s.l, err).Error("service error")
}
s.l.Info("graceful shutdown complete")
}