Files
Mikołaj ŚwiątekandGitHub cbbb8710de Allow status transitions between the same status value for Ok and RecoverableError (#14638)
#### Description

Allow status transitions between the same status value, if the status is
`Ok` or `RecoverableError`. Status events now carry metadata, so it's
valid to emit the same status value multiple times with new metadata.
This is particularly important for subcomponents, which appear as a
single component to the status reporting framework.

We're leaving other status values alone for now due to
https://github.com/open-telemetry/opentelemetry-collector/issues/14692
making things more complex.

#### Link to tracking issue
Partially addresses
https://github.com/open-telemetry/opentelemetry-collector/issues/14282

#### Testing
I modified the respective unit tests.
2026-03-10 04:09:34 +00:00

169 lines
5.7 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package status // import "go.opentelemetry.io/collector/service/internal/status"
import (
"errors"
"fmt"
"sync"
"go.opentelemetry.io/collector/component/componentstatus"
)
// onTransitionFunc receives a componentstatus.Event on a successful state transition
type onTransitionFunc func(*componentstatus.Event)
// errInvalidStateTransition is returned for invalid state transitions
var errInvalidStateTransition = errors.New("invalid state transition")
// fsm is a finite state machine that models transitions for component status
type fsm struct {
current *componentstatus.Event
transitions map[componentstatus.Status]map[componentstatus.Status]struct{}
onTransition onTransitionFunc
}
// transition will attempt to execute a state transition. If it's successful, it calls the
// onTransitionFunc with a Event representing the new state. Returns an error if the arguments
// result in an invalid status, or if the state transition is not valid.
func (m *fsm) transition(ev *componentstatus.Event) error {
if _, ok := m.transitions[m.current.Status()][ev.Status()]; !ok {
return fmt.Errorf(
"cannot transition from %s to %s: %w",
m.current.Status(),
ev.Status(),
errInvalidStateTransition,
)
}
m.current = ev
m.onTransition(ev)
return nil
}
// newFSM creates a state machine with all valid transitions for componentstatus.Status.
// The initial state is set to componentstatus.StatusNone.
// Transitions between the same status value are always allowed, as the new event may come with updated metadata.
func newFSM(onTransition onTransitionFunc) *fsm {
return &fsm{
current: componentstatus.NewEvent(componentstatus.StatusNone),
onTransition: onTransition,
transitions: map[componentstatus.Status]map[componentstatus.Status]struct{}{
componentstatus.StatusNone: {
componentstatus.StatusStarting: {},
},
componentstatus.StatusStarting: {
componentstatus.StatusOK: {},
componentstatus.StatusRecoverableError: {},
componentstatus.StatusPermanentError: {},
componentstatus.StatusFatalError: {},
componentstatus.StatusStopping: {},
},
componentstatus.StatusOK: {
componentstatus.StatusOK: {},
componentstatus.StatusRecoverableError: {},
componentstatus.StatusPermanentError: {},
componentstatus.StatusFatalError: {},
componentstatus.StatusStopping: {},
},
componentstatus.StatusRecoverableError: {
componentstatus.StatusRecoverableError: {},
componentstatus.StatusOK: {},
componentstatus.StatusPermanentError: {},
componentstatus.StatusFatalError: {},
componentstatus.StatusStopping: {},
},
componentstatus.StatusPermanentError: {
componentstatus.StatusStopping: {},
},
componentstatus.StatusFatalError: {},
componentstatus.StatusStopping: {
componentstatus.StatusRecoverableError: {},
componentstatus.StatusPermanentError: {},
componentstatus.StatusFatalError: {},
componentstatus.StatusStopped: {},
},
componentstatus.StatusStopped: {},
},
}
}
// NotifyStatusFunc is the receiver of status events after successful state transitions
type NotifyStatusFunc func(*componentstatus.InstanceID, *componentstatus.Event)
// InvalidTransitionFunc is the receiver of invalid transition errors
type InvalidTransitionFunc func(error)
// ServiceStatusFunc is the expected type of ReportStatus
type ServiceStatusFunc func(*componentstatus.InstanceID, *componentstatus.Event)
// ErrStatusNotReady is returned when trying to report status before service start
var ErrStatusNotReady = errors.New("report component status is not ready until service start")
// Reporter handles component status reporting
type Reporter interface {
ReportStatus(id *componentstatus.InstanceID, ev *componentstatus.Event)
ReportOKIfStarting(id *componentstatus.InstanceID)
}
type reporter struct {
mu sync.Mutex
fsmMap map[*componentstatus.InstanceID]*fsm
onStatusChange NotifyStatusFunc
onInvalidTransition InvalidTransitionFunc
}
// NewReporter returns a reporter that will invoke the NotifyStatusFunc when a component's status
// has changed.
func NewReporter(onStatusChange NotifyStatusFunc, onInvalidTransition InvalidTransitionFunc) Reporter {
return &reporter{
fsmMap: make(map[*componentstatus.InstanceID]*fsm),
onStatusChange: onStatusChange,
onInvalidTransition: onInvalidTransition,
}
}
// ReportStatus reports status for the given InstanceID
func (r *reporter) ReportStatus(
id *componentstatus.InstanceID,
ev *componentstatus.Event,
) {
r.mu.Lock()
defer r.mu.Unlock()
if err := r.componentFSM(id).transition(ev); err != nil {
r.onInvalidTransition(err)
}
}
func (r *reporter) ReportOKIfStarting(id *componentstatus.InstanceID) {
r.mu.Lock()
defer r.mu.Unlock()
fsm := r.componentFSM(id)
if fsm.current.Status() == componentstatus.StatusStarting {
if err := fsm.transition(componentstatus.NewEvent(componentstatus.StatusOK)); err != nil {
r.onInvalidTransition(err)
}
}
}
// Note: a lock must be acquired before calling this method.
func (r *reporter) componentFSM(id *componentstatus.InstanceID) *fsm {
fsm, ok := r.fsmMap[id]
if !ok {
fsm = newFSM(func(ev *componentstatus.Event) { r.onStatusChange(id, ev) })
r.fsmMap[id] = fsm
}
return fsm
}
// NewReportStatusFunc returns a function to be used as ReportStatus for componentstatus.TelemetrySettings
func NewReportStatusFunc(
id *componentstatus.InstanceID,
srvStatus ServiceStatusFunc,
) func(*componentstatus.Event) {
return func(ev *componentstatus.Event) {
srvStatus(id, ev)
}
}