Files
QSfera/opencloud/pkg/runtime/service/service.go
T
Pascal BleserandRalf Haferkamp 1827e143cd enhancement(runtime): improve logging in supervised crashes
When events occur in the supervisor, log them as FATAL when appropriate
(panic and termination when not restarting) instead of always using
INFO, and include more information such as the service name and the
current failures, etc..., rather than the generic message that was used
so far.
2025-11-10 10:39:09 +01:00

641 lines
24 KiB
Go

package service
import (
"context"
"errors"
"net"
"net/http"
"net/rpc"
"sort"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff"
"github.com/mohae/deepcopy"
"github.com/olekukonko/tablewriter"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/runner"
ogrpc "github.com/opencloud-eu/opencloud/pkg/service/grpc"
"github.com/opencloud-eu/opencloud/pkg/shared"
activitylog "github.com/opencloud-eu/opencloud/services/activitylog/pkg/command"
antivirus "github.com/opencloud-eu/opencloud/services/antivirus/pkg/command"
appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/command"
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command"
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command"
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command"
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command"
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command"
authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/command"
clientlog "github.com/opencloud-eu/opencloud/services/clientlog/pkg/command"
collaboration "github.com/opencloud-eu/opencloud/services/collaboration/pkg/command"
eventhistory "github.com/opencloud-eu/opencloud/services/eventhistory/pkg/command"
frontend "github.com/opencloud-eu/opencloud/services/frontend/pkg/command"
gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command"
graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command"
groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command"
idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command"
idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command"
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command"
nats "github.com/opencloud-eu/opencloud/services/nats/pkg/command"
notifications "github.com/opencloud-eu/opencloud/services/notifications/pkg/command"
ocdav "github.com/opencloud-eu/opencloud/services/ocdav/pkg/command"
ocm "github.com/opencloud-eu/opencloud/services/ocm/pkg/command"
ocs "github.com/opencloud-eu/opencloud/services/ocs/pkg/command"
policies "github.com/opencloud-eu/opencloud/services/policies/pkg/command"
postprocessing "github.com/opencloud-eu/opencloud/services/postprocessing/pkg/command"
proxy "github.com/opencloud-eu/opencloud/services/proxy/pkg/command"
search "github.com/opencloud-eu/opencloud/services/search/pkg/command"
settings "github.com/opencloud-eu/opencloud/services/settings/pkg/command"
sharing "github.com/opencloud-eu/opencloud/services/sharing/pkg/command"
sse "github.com/opencloud-eu/opencloud/services/sse/pkg/command"
storagepublic "github.com/opencloud-eu/opencloud/services/storage-publiclink/pkg/command"
storageshares "github.com/opencloud-eu/opencloud/services/storage-shares/pkg/command"
storageSystem "github.com/opencloud-eu/opencloud/services/storage-system/pkg/command"
storageusers "github.com/opencloud-eu/opencloud/services/storage-users/pkg/command"
thumbnails "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/command"
userlog "github.com/opencloud-eu/opencloud/services/userlog/pkg/command"
users "github.com/opencloud-eu/opencloud/services/users/pkg/command"
web "github.com/opencloud-eu/opencloud/services/web/pkg/command"
webdav "github.com/opencloud-eu/opencloud/services/webdav/pkg/command"
webfinger "github.com/opencloud-eu/opencloud/services/webfinger/pkg/command"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/thejerf/suture/v4"
)
var (
// runset keeps track of which services to start supervised.
runset map[string]struct{}
// wait funcs run after the service group has been started.
_waitFuncs = []func(*occfg.Config) error{pingNats, pingGateway, nil, wait(time.Second), nil}
// Use the runner.DefaultInterruptDuration as defaults for the individual service shutdown timeouts.
_defaultShutdownTimeoutDuration = runner.DefaultInterruptDuration
// Use the runner.DefaultGroupInterruptDuration as defaults for the server interruption timeout.
_defaultInterruptTimeoutDuration = runner.DefaultGroupInterruptDuration
)
type serviceFuncMap map[string]func(*occfg.Config) suture.Service
// Service represents a RPC service.
type Service struct {
Supervisor *suture.Supervisor
Services []serviceFuncMap
Additional serviceFuncMap
Log log.Logger
serviceToken map[string][]suture.ServiceToken
cfg *occfg.Config
}
// NewService returns a configured service with a controller and a default logger.
// When used as a library, flags are not parsed, and in order to avoid introducing a global state with init functions
// calls are done explicitly to loadFromEnv().
// Since this is the public constructor, options need to be added, at the moment only logging options
// are supported in order to match the running OpenCloud services structured log.
func NewService(ctx context.Context, options ...Option) (*Service, error) {
opts := NewOptions()
for _, f := range options {
f(opts)
}
l := log.NewLogger(
log.Color(opts.Config.Log.Color),
log.Pretty(opts.Config.Log.Pretty),
log.Level(opts.Config.Log.Level),
)
s := &Service{
Services: make([]serviceFuncMap, len(_waitFuncs)),
Additional: make(serviceFuncMap),
Log: l,
serviceToken: make(map[string][]suture.ServiceToken),
cfg: opts.Config,
}
// populate services
reg := func(priority int, name string, exec func(context.Context, *occfg.Config) error) {
if s.Services[priority] == nil {
s.Services[priority] = make(serviceFuncMap)
}
s.Services[priority][name] = NewSutureServiceBuilder(exec)
}
// nats is in priority group 0. It needs to start before all other services
reg(0, opts.Config.Nats.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Nats.Context = ctx
cfg.Nats.Commons = cfg.Commons
return nats.Execute(cfg.Nats)
})
// gateway is in priority group 1. It needs to start before the reva services
reg(1, opts.Config.Gateway.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Gateway.Context = ctx
cfg.Gateway.Commons = cfg.Commons
return gateway.Execute(cfg.Gateway)
})
// priority group 2 is empty for now
// most services are in priority group 3
reg(3, opts.Config.Activitylog.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Activitylog.Context = ctx
cfg.Activitylog.Commons = cfg.Commons
return activitylog.Execute(cfg.Activitylog)
})
reg(3, opts.Config.AppProvider.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AppProvider.Context = ctx
cfg.AppProvider.Commons = cfg.Commons
return appProvider.Execute(cfg.AppProvider)
})
reg(3, opts.Config.AppRegistry.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AppRegistry.Context = ctx
cfg.AppRegistry.Commons = cfg.Commons
return appRegistry.Execute(cfg.AppRegistry)
})
reg(3, opts.Config.AuthApp.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AuthApp.Context = ctx
cfg.AuthApp.Commons = cfg.Commons
return authapp.Execute(cfg.AuthApp)
})
reg(3, opts.Config.AuthBasic.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AuthBasic.Context = ctx
cfg.AuthBasic.Commons = cfg.Commons
return authbasic.Execute(cfg.AuthBasic)
})
reg(3, opts.Config.AuthMachine.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AuthMachine.Context = ctx
cfg.AuthMachine.Commons = cfg.Commons
return authmachine.Execute(cfg.AuthMachine)
})
reg(3, opts.Config.AuthService.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AuthService.Context = ctx
cfg.AuthService.Commons = cfg.Commons
return authservice.Execute(cfg.AuthService)
})
reg(3, opts.Config.Clientlog.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Clientlog.Context = ctx
cfg.Clientlog.Commons = cfg.Commons
return clientlog.Execute(cfg.Clientlog)
})
reg(3, opts.Config.EventHistory.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.EventHistory.Context = ctx
cfg.EventHistory.Commons = cfg.Commons
return eventhistory.Execute(cfg.EventHistory)
})
reg(3, opts.Config.Graph.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Graph.Context = ctx
cfg.Graph.Commons = cfg.Commons
return graph.Execute(cfg.Graph)
})
reg(3, opts.Config.Groups.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Groups.Context = ctx
cfg.Groups.Commons = cfg.Commons
return groups.Execute(cfg.Groups)
})
reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.IDM.Context = ctx
cfg.IDM.Commons = cfg.Commons
return idm.Execute(cfg.IDM)
})
reg(3, opts.Config.OCDav.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.OCDav.Context = ctx
cfg.OCDav.Commons = cfg.Commons
return ocdav.Execute(cfg.OCDav)
})
reg(3, opts.Config.OCS.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.OCS.Context = ctx
cfg.OCS.Commons = cfg.Commons
return ocs.Execute(cfg.OCS)
})
reg(3, opts.Config.Postprocessing.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Postprocessing.Context = ctx
cfg.Postprocessing.Commons = cfg.Commons
return postprocessing.Execute(cfg.Postprocessing)
})
reg(3, opts.Config.Search.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Search.Context = ctx
cfg.Search.Commons = cfg.Commons
return search.Execute(cfg.Search)
})
reg(3, opts.Config.Settings.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Settings.Context = ctx
cfg.Settings.Commons = cfg.Commons
return settings.Execute(cfg.Settings)
})
reg(3, opts.Config.StoragePublicLink.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.StoragePublicLink.Context = ctx
cfg.StoragePublicLink.Commons = cfg.Commons
return storagepublic.Execute(cfg.StoragePublicLink)
})
reg(3, opts.Config.StorageShares.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.StorageShares.Context = ctx
cfg.StorageShares.Commons = cfg.Commons
return storageshares.Execute(cfg.StorageShares)
})
reg(3, opts.Config.StorageSystem.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.StorageSystem.Context = ctx
cfg.StorageSystem.Commons = cfg.Commons
return storageSystem.Execute(cfg.StorageSystem)
})
reg(3, opts.Config.StorageUsers.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.StorageUsers.Context = ctx
cfg.StorageUsers.Commons = cfg.Commons
return storageusers.Execute(cfg.StorageUsers)
})
reg(3, opts.Config.Thumbnails.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Thumbnails.Context = ctx
cfg.Thumbnails.Commons = cfg.Commons
return thumbnails.Execute(cfg.Thumbnails)
})
reg(3, opts.Config.Userlog.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Userlog.Context = ctx
cfg.Userlog.Commons = cfg.Commons
return userlog.Execute(cfg.Userlog)
})
reg(3, opts.Config.Users.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Users.Context = ctx
cfg.Users.Commons = cfg.Commons
return users.Execute(cfg.Users)
})
reg(3, opts.Config.Web.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Web.Context = ctx
cfg.Web.Commons = cfg.Commons
return web.Execute(cfg.Web)
})
reg(3, opts.Config.WebDAV.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.WebDAV.Context = ctx
cfg.WebDAV.Commons = cfg.Commons
return webdav.Execute(cfg.WebDAV)
})
reg(3, opts.Config.Webfinger.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Webfinger.Context = ctx
cfg.Webfinger.Commons = cfg.Commons
return webfinger.Execute(cfg.Webfinger)
})
reg(3, opts.Config.IDP.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.IDP.Context = ctx
cfg.IDP.Commons = cfg.Commons
return idp.Execute(cfg.IDP)
})
reg(3, opts.Config.Proxy.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Proxy.Context = ctx
cfg.Proxy.Commons = cfg.Commons
return proxy.Execute(cfg.Proxy)
})
reg(3, opts.Config.Sharing.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Sharing.Context = ctx
cfg.Sharing.Commons = cfg.Commons
return sharing.Execute(cfg.Sharing)
})
reg(3, opts.Config.SSE.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.SSE.Context = ctx
cfg.SSE.Commons = cfg.Commons
return sse.Execute(cfg.SSE)
})
reg(3, opts.Config.OCM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.OCM.Context = ctx
cfg.OCM.Commons = cfg.Commons
return ocm.Execute(cfg.OCM)
})
// out of some unknown reason ci gets angry when frontend service starts in priority group 3
// this is not reproducible locally, it can start when nats and gateway are already running
// FIXME: find out why
reg(4, opts.Config.Frontend.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Frontend.Context = ctx
cfg.Frontend.Commons = cfg.Commons
return frontend.Execute(cfg.Frontend)
})
// populate optional services
areg := func(name string, exec func(context.Context, *occfg.Config) error) {
s.Additional[name] = NewSutureServiceBuilder(exec)
}
areg(opts.Config.Antivirus.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Antivirus.Context = ctx
// cfg.Antivirus.Commons = cfg.Commons // antivirus holds no Commons atm
return antivirus.Execute(cfg.Antivirus)
})
areg(opts.Config.Audit.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Audit.Context = ctx
cfg.Audit.Commons = cfg.Commons
return audit.Execute(cfg.Audit)
})
areg(opts.Config.Collaboration.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Collaboration.Context = ctx
cfg.Collaboration.Commons = cfg.Commons
return collaboration.Execute(cfg.Collaboration)
})
areg(opts.Config.Policies.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Policies.Context = ctx
cfg.Policies.Commons = cfg.Commons
return policies.Execute(cfg.Policies)
})
areg(opts.Config.Invitations.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Invitations.Context = ctx
cfg.Invitations.Commons = cfg.Commons
return invitations.Execute(cfg.Invitations)
})
areg(opts.Config.Notifications.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Notifications.Context = ctx
cfg.Notifications.Commons = cfg.Commons
return notifications.Execute(cfg.Notifications)
})
return s, nil
}
// Start a rpc service. By default, the package scope Start will run all default services to provide with a working
// OpenCloud instance.
func Start(ctx context.Context, o ...Option) error {
// Start the runtime. Most likely this was called ONLY by the `opencloud server` subcommand, but since we cannot protect
// from the caller, the previous statement holds truth.
// prepare a new rpc Service struct.
s, err := NewService(ctx, o...)
if err != nil {
return err
}
// create a context that will be cancelled when too many backoff cycles on one of the services happens
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// tolerance controls backoff cycles from the supervisor.
tolerance := 5
totalBackoff := 0
// Start creates its own supervisor. Running services under `opencloud server` will create its own supervision tree.
s.Supervisor = suture.New("opencloud", suture.Spec{
EventHook: func(e suture.Event) {
if e.Type() == suture.EventTypeBackoff {
totalBackoff++
if totalBackoff == tolerance {
cancel()
}
}
switch ev := e.(type) {
case suture.EventServicePanic:
l := s.Log.Fatal()
if ev.Restarting {
l = s.Log.Error()
}
l.Str("event", e.String()).Str("service", ev.ServiceName).Str("supervisor", ev.SupervisorName).
Bool("restarting", ev.Restarting).Float64("failures", ev.CurrentFailures).Float64("threshold", ev.FailureThreshold).
Str("message", ev.PanicMsg).Msg("service panic")
case suture.EventServiceTerminate:
l := s.Log.Fatal()
if ev.Restarting {
l = s.Log.Error()
}
l.Str("event", e.String()).Str("service", ev.ServiceName).Str("supervisor", ev.SupervisorName).
Bool("restarting", ev.Restarting).Float64("failures", ev.CurrentFailures).Float64("threshold", ev.FailureThreshold).
Interface("error", ev.Err).Msg("service terminated")
case suture.EventBackoff:
s.Log.Warn().Str("event", e.String()).Str("supervisor", ev.SupervisorName).Msg("service backoff")
case suture.EventResume:
s.Log.Info().Str("event", e.String()).Str("supervisor", ev.SupervisorName).Msg("service resume")
case suture.EventStopTimeout:
s.Log.Warn().Str("event", e.String()).Str("service", ev.ServiceName).Str("supervisor", ev.SupervisorName).Msg("service resume")
default:
s.Log.Warn().Str("event", e.String()).Msgf("supervisor: %v", e.Map()["supervisor_name"])
}
},
FailureThreshold: 5,
FailureBackoff: 3 * time.Second,
})
if s.cfg.Commons == nil {
s.cfg.Commons = &shared.Commons{
Log: &shared.Log{},
}
}
if err = rpc.Register(s); err != nil {
if s != nil {
s.Log.Fatal().Err(err).Msg("could not register rpc service")
}
}
rpc.HandleHTTP()
l, err := net.Listen("tcp", net.JoinHostPort(s.cfg.Runtime.Host, s.cfg.Runtime.Port))
if err != nil {
s.Log.Fatal().Err(err).Msg("could not start listener")
}
srv := new(http.Server)
// prepare the set of services to run
s.generateRunSet(s.cfg)
// There are reasons not to do this, but we have race conditions ourselves. Until we resolve them, mind the following disclaimer:
// Calling ServeBackground will CORRECTLY start the supervisor running in a new goroutine. It is risky to directly run
// go supervisor.Serve()
// because that will briefly create a race condition as it starts up, if you try to .Add() services immediately afterward.
// https://pkg.go.dev/github.com/thejerf/suture/v4@v4.0.0#Supervisor
go s.Supervisor.ServeBackground(ctx)
for i, service := range s.Services {
scheduleServiceTokens(s, service)
if _waitFuncs[i] != nil {
if err := _waitFuncs[i](s.cfg); err != nil {
s.Log.Fatal().Err(err).Msg("wait func failed")
}
}
}
// schedule services that are optional
scheduleServiceTokens(s, s.Additional)
go func() {
if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.Log.Fatal().Err(err).Msg("could not start rpc server")
}
}()
// trapShutdownCtx will block on the context-done channel for interruptions.
return trapShutdownCtx(s, srv, ctx)
}
// scheduleServiceTokens adds service tokens to the service supervisor.
func scheduleServiceTokens(s *Service, funcSet serviceFuncMap) {
for name := range runset {
if _, ok := funcSet[name]; !ok {
continue
}
swap := deepcopy.Copy(s.cfg)
s.serviceToken[name] = append(s.serviceToken[name], s.Supervisor.Add(funcSet[name](swap.(*occfg.Config))))
}
}
// generateRunSet interprets the cfg.Runtime.Services config option to cherry-pick which services to start using
// the runtime.
func (s *Service) generateRunSet(cfg *occfg.Config) {
runset = make(map[string]struct{})
if cfg.Runtime.Services != nil {
for _, name := range cfg.Runtime.Services {
runset[name] = struct{}{}
}
return
}
for _, service := range s.Services {
for name := range service {
runset[name] = struct{}{}
}
}
// add additional services if explicitly added by config
for _, name := range cfg.Runtime.Additional {
runset[name] = struct{}{}
}
// remove services if explicitly excluded by config
for _, name := range cfg.Runtime.Disabled {
delete(runset, name)
}
}
// List running processes for the Service Controller.
func (s *Service) List(_ struct{}, reply *string) error {
tableString := &strings.Builder{}
table := tablewriter.NewTable(tableString)
table.Header([]string{"Service"})
names := []string{}
for t := range s.serviceToken {
if len(s.serviceToken[t]) > 0 {
names = append(names, t)
}
}
sort.Strings(names)
for n := range names {
table.Append([]string{names[n]})
}
table.Render()
*reply = tableString.String()
return nil
}
func trapShutdownCtx(s *Service, srv *http.Server, ctx context.Context) error {
<-ctx.Done()
s.Log.Info().Msg("starting graceful shutdown")
start := time.Now()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), _defaultShutdownTimeoutDuration)
defer cancel()
s.Log.Debug().Msg("starting runtime listener shutdown")
if err := srv.Shutdown(ctx); err != nil {
s.Log.Error().Err(err).Msg("could not shutdown runtime listener")
return
}
s.Log.Debug().Msg("runtime listener shutdown done")
}()
// shutdown services in the order defined in the config
// any services not listed will be shutdown in parallel afterwards
for _, sName := range s.cfg.Runtime.ShutdownOrder {
if _, ok := s.serviceToken[sName]; !ok {
s.Log.Warn().Str("service", sName).Msg("unknown service for ordered shutdown, skipping")
continue
}
for i := range s.serviceToken[sName] {
if err := s.Supervisor.RemoveAndWait(s.serviceToken[sName][i], _defaultShutdownTimeoutDuration); err != nil && !errors.Is(err, suture.ErrSupervisorNotRunning) {
s.Log.Error().Err(err).Str("service", sName).Msg("could not shutdown service in order, skipping to next")
// continue shutting down other services
continue
}
s.Log.Debug().Str("service", sName).Msg("graceful ordered shutdown for service done")
}
delete(s.serviceToken, sName)
}
for sName := range s.serviceToken {
for i := range s.serviceToken[sName] {
wg.Add(1)
go func() {
s.Log.Debug().Str("service", sName).Msg("starting graceful shutdown for service")
defer wg.Done()
if err := s.Supervisor.RemoveAndWait(s.serviceToken[sName][i], _defaultShutdownTimeoutDuration); err != nil && !errors.Is(err, suture.ErrSupervisorNotRunning) {
s.Log.Error().Err(err).Str("service", sName).Msg("could not shutdown service")
return
}
s.Log.Debug().Str("service", sName).Msg("graceful shutdown for service done")
}()
}
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-time.After(_defaultInterruptTimeoutDuration):
s.Log.Error().Dur("timeoutDuration", _defaultInterruptTimeoutDuration).Msg("graceful shutdown timeout reached, terminating")
return errors.New("graceful shutdown timeout reached, terminating")
case <-done:
duration := time.Since(start)
s.Log.Info().Dur("duration", duration).Msg("graceful shutdown done")
return nil
}
}
// pingNats will attempt to connect to nats, blocking until a connection is established
func pingNats(cfg *occfg.Config) error {
// We need to get a natsconfig from somewhere. We can use any one.
evcfg := cfg.Postprocessing.Postprocessing.Events
_, err := stream.NatsFromConfig("initial", true, stream.NatsConfig{
Endpoint: evcfg.Endpoint,
Cluster: evcfg.Cluster,
EnableTLS: evcfg.EnableTLS,
TLSInsecure: evcfg.TLSInsecure,
TLSRootCACertificate: evcfg.TLSRootCACertificate,
AuthUsername: evcfg.AuthUsername,
AuthPassword: evcfg.AuthPassword,
})
return err
}
func pingGateway(cfg *occfg.Config) error {
// init grpc connection
_, err := ogrpc.NewClient()
if err != nil {
return err
}
b := backoff.NewExponentialBackOff()
o := func() error {
n := b.NextBackOff()
_, err := pool.GetGatewayServiceClient(cfg.Reva.Address)
if err != nil && n > time.Second {
logger.New().Error().Err(err).Dur("backoff", n).Msg("can't connect to gateway service, retrying")
}
return err
}
err = backoff.Retry(o, b)
return err
}
func wait(d time.Duration) func(cfg *occfg.Config) error {
return func(cfg *occfg.Config) error {
time.Sleep(d)
return nil
}
}