Files
moby/daemon/container/state.go
Sebastiaan van Stijn ee4b7a8374 daemon/container: remove NewState() constructor
This constructor did not do anything other than creating an empty struct
for an exported type. While we should look at initializing with a proper
state, we currently do not, so let's not pretend we do some magic here,
and leave it for a future exercise to create a proper constructor if we
need one.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-19 15:33:36 +01:00

449 lines
13 KiB
Go

package container
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/container"
libcontainerdtypes "github.com/moby/moby/v2/daemon/internal/libcontainerd/types"
)
// State holds the current container state, and has methods to get and
// set the state. State is embedded in the [Container] struct.
//
// State contains an exported [sync.Mutex] which is used as a global lock
// for both the State and the Container it's embedded in.
type State struct {
// This Mutex is exported by design and is used as a global lock
// for both the State and the Container it's embedded in.
sync.Mutex
// Note that [State.Running], [State.Restarting], and [State.Paused] are
// not mutually exclusive.
//
// When pausing a container (on Linux), the freezer cgroup is used to suspend
// all processes in the container. Freezing the process requires the process to
// be running. As a result, paused containers can have both [State.Running]
// and [State.Paused] set to true.
//
// In a similar fashion, [State.Running] and [State.Restarting] can both
// be true in a situation where a container is in process of being restarted.
// Refer to [State.StateString] for order of precedence.
Running bool
Paused bool
Restarting bool
OOMKilled bool
RemovalInProgress bool `json:"-"` // No need for this to be persistent on disk.
Dead bool
Pid int
ExitCodeValue int `json:"ExitCode"`
ErrorMsg string `json:"Error"` // contains last known error during container start, stop, or remove
StartedAt time.Time
FinishedAt time.Time
Health *Health
Removed bool `json:"-"`
stopWaiters []chan<- StateStatus
removeOnlyWaiters []chan<- StateStatus
// The libcontainerd reference fields are unexported to force consumers
// to access them through the getter methods with multi-valued returns
// so that they can't forget to nil-check: the code won't compile unless
// the nil-check result is explicitly consumed or discarded.
ctr libcontainerdtypes.Container
task libcontainerdtypes.Task
}
// StateStatus is used to return container wait results.
// Implements exec.ExitCode interface.
// This type is needed as State include a sync.Mutex field which make
// copying it unsafe.
type StateStatus struct {
exitCode int
err error
}
// ExitCode returns current exitcode for the state.
func (s StateStatus) ExitCode() int {
return s.exitCode
}
// Err returns current error for the state. Returns nil if the container had
// exited on its own.
func (s StateStatus) Err() error {
return s.err
}
// String returns a human-readable description of the state
func (s *State) String() string {
if s.Running {
if s.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
}
if s.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago", s.ExitCodeValue, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
}
if h := s.Health; h != nil {
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)), h.String())
}
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
}
if s.RemovalInProgress {
return "Removal In Progress"
}
if s.Dead {
return "Dead"
}
if s.StartedAt.IsZero() {
return "Created"
}
if s.FinishedAt.IsZero() {
return ""
}
return fmt.Sprintf("Exited (%d) %s ago", s.ExitCodeValue, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
}
// StateString returns the container's current [ContainerState], based on the
// [State.Running], [State.Paused], [State.Restarting], [State.RemovalInProgress],
// [State.StartedAt] and [State.Dead] fields.
func (s *State) StateString() container.ContainerState {
if s.Running {
if s.Paused {
return container.StatePaused
}
if s.Restarting {
return container.StateRestarting
}
return container.StateRunning
}
// TODO(thaJeztah): should [State.Removed] also have an corresponding string?
// TODO(thaJeztah): should [State.OOMKilled] be taken into account anywhere?
if s.RemovalInProgress {
return container.StateRemoving
}
if s.Dead {
return container.StateDead
}
if s.StartedAt.IsZero() {
return container.StateCreated
}
return container.StateExited
}
// Wait waits until the container is in a certain state indicated by the given
// condition. A context must be used for cancelling the request, controlling
// timeouts, and avoiding goroutine leaks. Wait must be called without holding
// the state lock. Returns a channel from which the caller will receive the
// result. If the container exited on its own, the result's Err() method will
// be nil and its ExitCode() method will return the container's exit code,
// otherwise, the results Err() method will return an error indicating why the
// wait operation failed.
func (s *State) Wait(ctx context.Context, condition container.WaitCondition) <-chan StateStatus {
s.Lock()
defer s.Unlock()
// Buffer so we can put status and finish even nobody receives it.
resultC := make(chan StateStatus, 1)
if s.conditionAlreadyMet(condition) {
resultC <- StateStatus{
exitCode: s.ExitCodeValue,
err: s.Err(),
}
return resultC
}
waitC := make(chan StateStatus, 1)
// Removal wakes up both removeOnlyWaiters and stopWaiters
// Container could be removed while still in "created" state
// in which case it is never actually stopped
if condition == container.WaitConditionRemoved {
s.removeOnlyWaiters = append(s.removeOnlyWaiters, waitC)
} else {
s.stopWaiters = append(s.stopWaiters, waitC)
}
go func() {
select {
case <-ctx.Done():
// Context timeout or cancellation.
resultC <- StateStatus{
exitCode: -1,
err: ctx.Err(),
}
return
case status := <-waitC:
resultC <- status
}
}()
return resultC
}
func (s *State) conditionAlreadyMet(condition container.WaitCondition) bool {
switch condition {
case container.WaitConditionNotRunning:
return !s.Running
case container.WaitConditionRemoved:
return s.Removed
default:
// TODO(thaJeztah): how do we want to handle "WaitConditionNextExit"?
return false
}
}
// IsRunning returns whether the [State.Running] flag is set.
//
// Note that [State.Running], [State.Restarting], and [State.Paused] are
// not mutually exclusive.
//
// When pausing a container (on Linux), the freezer cgroup is used to suspend
// all processes in the container. Freezing the process requires the process to
// be running. As a result, paused containers can have both [State.Running]
// and [State.Paused] set to true.
//
// In a similar fashion, [State.Running] and [State.Restarting] can both
// be true in a situation where a container is in process of being restarted.
// Refer to [State.StateString] for order of precedence.
func (s *State) IsRunning() bool {
s.Lock()
defer s.Unlock()
return s.Running
}
// GetPID holds the process id of a container.
func (s *State) GetPID() int {
s.Lock()
defer s.Unlock()
return s.Pid
}
// ExitCode returns current exitcode for the state. Take lock before if state
// may be shared.
func (s *State) ExitCode() int {
return s.ExitCodeValue
}
// SetExitCode sets current exitcode for the state. Take lock before if state
// may be shared.
func (s *State) SetExitCode(ec int) {
s.ExitCodeValue = ec
}
// SetRunning sets the running state along with StartedAt time.
func (s *State) SetRunning(ctr libcontainerdtypes.Container, tsk libcontainerdtypes.Task, start time.Time) {
s.setRunning(ctr, tsk, &start)
}
// SetRunningExternal sets the running state without setting the `StartedAt` time (used for containers not started by Docker instead of SetRunning).
func (s *State) SetRunningExternal(ctr libcontainerdtypes.Container, tsk libcontainerdtypes.Task) {
s.setRunning(ctr, tsk, nil)
}
// setRunning sets the state of the container to "running".
func (s *State) setRunning(ctr libcontainerdtypes.Container, tsk libcontainerdtypes.Task, start *time.Time) {
s.ErrorMsg = ""
s.Paused = false
s.Running = true
s.Restarting = false
if start != nil {
s.Paused = false
}
s.ExitCodeValue = 0
s.ctr = ctr
s.task = tsk
if tsk != nil {
s.Pid = int(tsk.Pid())
} else {
s.Pid = 0
}
s.OOMKilled = false
if start != nil {
s.StartedAt = start.UTC()
}
}
// SetStopped sets the container state to "stopped" without locking.
func (s *State) SetStopped(exitStatus *ExitStatus) {
s.Running = false
s.Paused = false
s.Restarting = false
s.Pid = 0
if exitStatus.ExitedAt.IsZero() {
s.FinishedAt = time.Now().UTC()
} else {
s.FinishedAt = exitStatus.ExitedAt
}
s.ExitCodeValue = exitStatus.ExitCode
s.notifyAndClear(&s.stopWaiters)
}
// SetRestarting sets the container state to "restarting" without locking.
// It also sets the container PID to 0.
func (s *State) SetRestarting(exitStatus *ExitStatus) {
// we should consider the container running when it is restarting because of
// all the checks in docker around rm/stop/etc
s.Running = true
s.Restarting = true
s.Paused = false
s.Pid = 0
s.FinishedAt = time.Now().UTC()
s.ExitCodeValue = exitStatus.ExitCode
s.notifyAndClear(&s.stopWaiters)
}
// SetError sets the container's error state. This is useful when we want to
// know the error that occurred when container transits to another state
// when inspecting it
func (s *State) SetError(err error) {
s.ErrorMsg = ""
if err != nil {
s.ErrorMsg = err.Error()
}
}
// IsPaused returns whether the container is paused.
//
// Note that [State.Running], [State.Restarting], and [State.Paused] are
// not mutually exclusive.
//
// When pausing a container (on Linux), the freezer cgroup is used to suspend
// all processes in the container. Freezing the process requires the process to
// be running. As a result, paused containers can have both [State.Running]
// and [State.Paused] set to true.
//
// In a similar fashion, [State.Running] and [State.Restarting] can both
// be true in a situation where a container is in process of being restarted.
// Refer to [State.StateString] for order of precedence.
func (s *State) IsPaused() bool {
s.Lock()
defer s.Unlock()
return s.Paused
}
// IsRestarting returns whether the container is restarting.
//
// Note that [State.Running], [State.Restarting], and [State.Paused] are
// not mutually exclusive.
//
// When pausing a container (on Linux), the freezer cgroup is used to suspend
// all processes in the container. Freezing the process requires the process to
// be running. As a result, paused containers can have both [State.Running]
// and [State.Paused] set to true.
//
// In a similar fashion, [State.Running] and [State.Restarting] can both
// be true in a situation where a container is in process of being restarted.
// Refer to [State.StateString] for order of precedence.
func (s *State) IsRestarting() bool {
s.Lock()
defer s.Unlock()
return s.Restarting
}
// SetRemovalInProgress sets the container state as being removed.
// It returns true if the container was already in that state.
func (s *State) SetRemovalInProgress() bool {
s.Lock()
defer s.Unlock()
if s.RemovalInProgress {
return true
}
s.RemovalInProgress = true
return false
}
// ResetRemovalInProgress makes the RemovalInProgress state to false.
func (s *State) ResetRemovalInProgress() {
s.Lock()
s.RemovalInProgress = false
s.Unlock()
}
// IsRemovalInProgress returns whether the RemovalInProgress flag is set.
// Used by Container to check whether a container is being removed.
func (s *State) IsRemovalInProgress() bool {
s.Lock()
defer s.Unlock()
return s.RemovalInProgress
}
// IsDead returns whether the Dead flag is set. Used by Container to check whether a container is dead.
func (s *State) IsDead() bool {
s.Lock()
defer s.Unlock()
return s.Dead
}
// SetRemoved assumes this container is already in the "dead" state and notifies all waiters.
func (s *State) SetRemoved() {
s.SetRemovalError(nil)
}
// SetRemovalError is to be called in case a container remove failed.
// It sets an error and notifies all waiters.
func (s *State) SetRemovalError(err error) {
s.SetError(err)
s.Lock()
s.Removed = true
s.notifyAndClear(&s.removeOnlyWaiters)
s.notifyAndClear(&s.stopWaiters)
s.Unlock()
}
// Err returns an error if there is one.
func (s *State) Err() error {
if s.ErrorMsg != "" {
return errors.New(s.ErrorMsg)
}
return nil
}
func (s *State) notifyAndClear(waiters *[]chan<- StateStatus) {
result := StateStatus{
exitCode: s.ExitCodeValue,
err: s.Err(),
}
for _, c := range *waiters {
c <- result
}
*waiters = nil
}
// C8dContainer returns a reference to the libcontainerd Container object for
// the container and whether the reference is valid.
//
// The container lock must be held when calling this method.
func (s *State) C8dContainer() (_ libcontainerdtypes.Container, ok bool) {
return s.ctr, s.ctr != nil
}
// Task returns a reference to the libcontainerd Task object for the container
// and whether the reference is valid.
//
// The container lock must be held when calling this method.
//
// See also: (*Container).GetRunningTask().
func (s *State) Task() (_ libcontainerdtypes.Task, ok bool) {
return s.task, s.task != nil
}