mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
See https://github.com/grafana/dskit/issues/576 Signed-off-by: Daniel Strobusch <[email protected]>
389 lines
10 KiB
Go
389 lines
10 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
"go.uber.org/atomic"
|
|
)
|
|
|
|
type managerState int
|
|
|
|
const (
|
|
unknown managerState = iota // initial state
|
|
healthy // all services running
|
|
stopped // all services stopped (failed or terminated)
|
|
)
|
|
|
|
// ManagerListener listens for events from Manager.
|
|
type ManagerListener interface {
|
|
// Healthy is called when Manager reaches Healthy state (all services Running)
|
|
Healthy()
|
|
|
|
// Stopped is called when Manager reaches Stopped state (all services are either Terminated or Failed)
|
|
Stopped()
|
|
|
|
// Failure is called when service fails.
|
|
Failure(service Service)
|
|
}
|
|
|
|
// Manager is initialized with a collection of services. They all must be in New state.
|
|
// Manager can start them, and observe their state as a group.
|
|
// Once all services are running, Manager is said to be Healthy. It is possible for manager to never reach the Healthy state, if some services fail to start.
|
|
// When all services are stopped (Terminated or Failed), manager is Stopped.
|
|
//
|
|
// Note: Manager's state is defined by state of services. Services can be started outside of Manager and if all become Running, Manager will be Healthy as well.
|
|
//
|
|
// Note: Creating a manager immediately installs listeners to all services (to compute manager's state), which may start goroutines.
|
|
// To avoid leaking goroutines, make sure to eventually stop all services or the manager (which stops services), even if manager wasn't explicitly started.
|
|
type Manager struct {
|
|
services []Service
|
|
|
|
healthyCh chan struct{} // closed when healthy state is reached, or if it cannot be reached anymore (whatever happens first)
|
|
stoppedCh chan struct{} // closed when stopped state is reached.
|
|
|
|
mu sync.Mutex
|
|
state managerState
|
|
byState map[State][]Service // Services sorted by state
|
|
healthyClosed bool // was healthyCh closed already?
|
|
listeners []chan func(listener ManagerListener)
|
|
}
|
|
|
|
// NewManager creates new service manager. It needs at least one service, and all services must be in New state.
|
|
func NewManager(services ...Service) (*Manager, error) {
|
|
if len(services) == 0 {
|
|
return nil, errors.New("no services")
|
|
}
|
|
|
|
m := &Manager{
|
|
services: services,
|
|
byState: map[State][]Service{},
|
|
healthyCh: make(chan struct{}),
|
|
stoppedCh: make(chan struct{}),
|
|
}
|
|
|
|
for _, s := range services {
|
|
st := s.State()
|
|
if st != New {
|
|
return nil, fmt.Errorf("unexpected service state: %v", st)
|
|
}
|
|
|
|
m.byState[st] = append(m.byState[st], s)
|
|
}
|
|
|
|
for _, s := range services {
|
|
s.AddListener(newManagerServiceListener(m, s))
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// StartAsync initiates service startup on all the services being managed.
|
|
// It is only valid to call this method if all of the services are New.
|
|
func (m *Manager) StartAsync(ctx context.Context) error {
|
|
for _, s := range m.services {
|
|
err := s.StartAsync(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StopAsync initiates service shutdown if necessary on all the services being managed.
|
|
func (m *Manager) StopAsync() {
|
|
if m == nil {
|
|
return
|
|
}
|
|
|
|
for _, s := range m.services {
|
|
s.StopAsync()
|
|
}
|
|
}
|
|
|
|
// IsHealthy returns true if all services are currently in the Running state.
|
|
func (m *Manager) IsHealthy() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.state == healthy
|
|
}
|
|
|
|
// AwaitHealthy waits for the ServiceManager to become healthy. Returns nil, if manager is healthy, error otherwise (eg. manager
|
|
// is in a state in which it cannot get healthy anymore).
|
|
func (m *Manager) AwaitHealthy(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-m.healthyCh:
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state != healthy {
|
|
terminated := len(m.byState[Terminated])
|
|
|
|
var failedReasons []string
|
|
for _, s := range m.byState[Failed] {
|
|
err := s.FailureCase()
|
|
if err != nil {
|
|
// err is never nil for a failed service.
|
|
failedReasons = append(failedReasons, err.Error())
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("not healthy, %d terminated, %d failed: %v", terminated, len(failedReasons), failedReasons)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsStopped returns true if all services are in terminal state (Terminated or Failed)
|
|
func (m *Manager) IsStopped() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.state == stopped
|
|
}
|
|
|
|
// AwaitStopped waits for the ServiceManager to become stopped. Returns nil, if manager is stopped, error when context finishes earlier.
|
|
func (m *Manager) AwaitStopped(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-m.stoppedCh:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ServicesByState provides a snapshot of the current state of all the services under management.
|
|
func (m *Manager) ServicesByState() map[State][]Service {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
result := map[State][]Service{}
|
|
for st, ss := range m.byState {
|
|
result[st] = append([]Service(nil), ss...) // make a copy
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) serviceStateChanged(s Service, from State, to State) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
fs := m.byState[from]
|
|
for ix, ss := range fs {
|
|
if s == ss {
|
|
fs = append(fs[:ix], fs[ix+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(fs) == 0 {
|
|
delete(m.byState, from)
|
|
} else {
|
|
m.byState[from] = fs
|
|
}
|
|
|
|
m.byState[to] = append(m.byState[to], s)
|
|
|
|
if to == Failed {
|
|
m.notifyListeners(func(l ManagerListener) { l.Failure(s) }, false)
|
|
}
|
|
|
|
running := len(m.byState[Running])
|
|
stopping := len(m.byState[Stopping])
|
|
done := len(m.byState[Terminated]) + len(m.byState[Failed])
|
|
|
|
all := len(m.services)
|
|
|
|
switch {
|
|
case running == all:
|
|
close(m.healthyCh)
|
|
m.state = healthy
|
|
m.healthyClosed = true
|
|
m.notifyListeners(func(l ManagerListener) { l.Healthy() }, false)
|
|
|
|
case done == all:
|
|
if !m.healthyClosed {
|
|
// healthy cannot be reached anymore
|
|
close(m.healthyCh)
|
|
m.healthyClosed = true
|
|
}
|
|
close(m.stoppedCh) // happens at most once
|
|
m.state = stopped
|
|
m.notifyListeners(func(l ManagerListener) { l.Stopped() }, true)
|
|
|
|
default:
|
|
if !m.healthyClosed && (done > 0 || stopping > 0) {
|
|
// healthy cannot be reached anymore
|
|
close(m.healthyCh)
|
|
m.healthyClosed = true
|
|
}
|
|
|
|
m.state = unknown
|
|
}
|
|
}
|
|
|
|
// AddListener registers a ManagerListener to be run when this Manager changes state.
|
|
// The listener will not have previous state changes replayed, so it is suggested that listeners are added before any of the managed services are started.
|
|
//
|
|
// AddListener guarantees execution ordering across calls to a given listener but not across calls to multiple listeners.
|
|
// Specifically, a given listener will have its callbacks invoked in the same order as the underlying service enters those states.
|
|
// Additionally, at most one of the listener's callbacks will execute at once.
|
|
// However, multiple listeners' callbacks may execute concurrently, and listeners may execute in an order different from the one in which they were registered.
|
|
//
|
|
// Returned function can be used to stop the listener and free resources used by it (e.g. goroutine).
|
|
func (m *Manager) AddListener(listener ManagerListener) func() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.state == stopped {
|
|
// no need to register listener, as no more events will be sent
|
|
return func() {}
|
|
}
|
|
|
|
// max number of events is: failed notification for each service + healthy + stopped.
|
|
// we use buffer to avoid blocking the sender, which holds the manager's lock.
|
|
listenerCh := make(chan func(l ManagerListener), len(m.services)+2)
|
|
m.listeners = append(m.listeners, listenerCh)
|
|
|
|
stop := make(chan struct{})
|
|
stopClosed := atomic.NewBool(false)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
|
|
// each listener has its own goroutine, processing events.
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
// Process events from service.
|
|
case fn, ok := <-listenerCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
fn(listener)
|
|
|
|
case <-stop:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return func() {
|
|
if stopClosed.CompareAndSwap(false, true) {
|
|
// Tell listener goroutine to stop.
|
|
close(stop)
|
|
}
|
|
|
|
// Remove channel for notifications from manager's list of listeners.
|
|
m.mu.Lock()
|
|
m.listeners = slices.DeleteFunc(m.listeners, func(c chan func(listener ManagerListener)) bool {
|
|
return listenerCh == c
|
|
})
|
|
m.mu.Unlock()
|
|
|
|
wg.Wait()
|
|
}
|
|
}
|
|
|
|
// called with lock
|
|
func (m *Manager) notifyListeners(fn func(l ManagerListener), closeChan bool) {
|
|
for _, ch := range m.listeners {
|
|
ch <- fn
|
|
|
|
if closeChan {
|
|
close(ch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newManagerServiceListener(m *Manager, s Service) *managerServiceListener {
|
|
return &managerServiceListener{m: m, s: s}
|
|
}
|
|
|
|
// managerServiceListener is a service listener, that updates Service state in the Manager
|
|
type managerServiceListener struct {
|
|
m *Manager
|
|
s Service
|
|
}
|
|
|
|
func (l managerServiceListener) Starting() {
|
|
l.m.serviceStateChanged(l.s, New, Starting)
|
|
}
|
|
|
|
func (l managerServiceListener) Running() {
|
|
l.m.serviceStateChanged(l.s, Starting, Running)
|
|
}
|
|
|
|
func (l managerServiceListener) Stopping(from State) {
|
|
l.m.serviceStateChanged(l.s, from, Stopping)
|
|
}
|
|
|
|
func (l managerServiceListener) Terminated(from State) {
|
|
l.m.serviceStateChanged(l.s, from, Terminated)
|
|
}
|
|
|
|
func (l managerServiceListener) Failed(from State, _ error) {
|
|
l.m.serviceStateChanged(l.s, from, Failed)
|
|
}
|
|
|
|
// NewManagerListener provides a simple way to build manager listener from supplied functions.
|
|
// Functions will only be called when not nil.
|
|
func NewManagerListener(healthy, stopped func(), failure func(service Service)) ManagerListener {
|
|
return &funcBasedManagerListener{
|
|
healthy: healthy,
|
|
stopped: stopped,
|
|
failure: failure,
|
|
}
|
|
}
|
|
|
|
type funcBasedManagerListener struct {
|
|
healthy func()
|
|
stopped func()
|
|
failure func(service Service)
|
|
}
|
|
|
|
func (f funcBasedManagerListener) Healthy() {
|
|
if f.healthy != nil {
|
|
f.healthy()
|
|
}
|
|
}
|
|
|
|
func (f funcBasedManagerListener) Stopped() {
|
|
if f.stopped != nil {
|
|
f.stopped()
|
|
}
|
|
}
|
|
|
|
func (f funcBasedManagerListener) Failure(service Service) {
|
|
if f.failure != nil {
|
|
f.failure(service)
|
|
}
|
|
}
|
|
|
|
// StartManagerAndAwaitHealthy starts the manager (which in turns starts all services managed by it), and then waits
|
|
// until it reaches Running state. All services that this manager manages must be in New state, otherwise starting
|
|
// will fail.
|
|
//
|
|
// Notice that context passed to the manager for starting its services is the same as context used for waiting!
|
|
func StartManagerAndAwaitHealthy(ctx context.Context, manager *Manager) error {
|
|
err := manager.StartAsync(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return manager.AwaitHealthy(ctx)
|
|
}
|
|
|
|
// StopManagerAndAwaitStopped asks manager to stop its services, and then waits
|
|
// until manager reaches the stopped state or context is stopped.
|
|
func StopManagerAndAwaitStopped(ctx context.Context, manager *Manager) error {
|
|
manager.StopAsync()
|
|
return manager.AwaitStopped(ctx)
|
|
}
|