dotcloud/docker

View on GitHub
container/state.go

Summary

Maintainability
B
5 hrs
Test Coverage
package container // import "github.com/docker/docker/container"

import (
    "context"
    "errors"
    "fmt"
    "sync"
    "time"

    "github.com/docker/docker/api/types/container"
    libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
    units "github.com/docker/go-units"
)

// 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 `Running` and `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 are both `Running` _and_ `Paused`.
    Running           bool
    Paused            bool
    Restarting        bool
    OOMKilled         bool
    RemovalInProgress bool // Not 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
}

// NewState creates a default state object.
func NewState() *State {
    return &State{}
}

// 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)))
}

// IsValidHealthString checks if the provided string is a valid container health status or not.
func IsValidHealthString(s string) bool {
    return s == container.Starting ||
        s == container.Healthy ||
        s == container.Unhealthy ||
        s == container.NoHealthcheck
}

// StateString returns a single string to describe state
func (s *State) StateString() string {
    if s.Running {
        if s.Paused {
            return "paused"
        }
        if s.Restarting {
            return "restarting"
        }
        return "running"
    }

    if s.RemovalInProgress {
        return "removing"
    }

    if s.Dead {
        return "dead"
    }

    if s.StartedAt.IsZero() {
        return "created"
    }

    return "exited"
}

// IsValidStateString checks if the provided string is a valid container state or not.
func IsValidStateString(s string) bool {
    if s != "paused" &&
        s != "restarting" &&
        s != "removing" &&
        s != "running" &&
        s != "dead" &&
        s != "created" &&
        s != "exited" {
        return false
    }
    return true
}

// WaitCondition is an enum type for different states to wait for.
type WaitCondition int

// Possible WaitCondition Values.
//
// WaitConditionNotRunning (default) is used to wait for any of the non-running
// states: "created", "exited", "dead", "removing", or "removed".
//
// WaitConditionNextExit is used to wait for the next time the state changes
// to a non-running state. If the state is currently "created" or "exited",
// this would cause Wait() to block until either the container runs and exits
// or is removed.
//
// WaitConditionRemoved is used to wait for the container to be removed.
const (
    WaitConditionNotRunning WaitCondition = iota
    WaitConditionNextExit
    WaitConditionRemoved
)

// 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 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.ExitCode(),
            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 == 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 WaitCondition) bool {
    switch condition {
    case WaitConditionNotRunning:
        return !s.Running
    case WaitConditionRemoved:
        return s.Removed
    }

    return false
}

// IsRunning returns whether the running flag is set. Used by Container to check whether a container is running.
func (s *State) IsRunning() bool {
    s.Lock()
    res := s.Running
    s.Unlock()
    return res
}

// GetPID holds the process id of a container.
func (s *State) GetPID() int {
    s.Lock()
    res := s.Pid
    s.Unlock()
    return res
}

// 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 or not.
func (s *State) IsPaused() bool {
    s.Lock()
    res := s.Paused
    s.Unlock()
    return res
}

// IsRestarting returns whether the container is restarting or not.
func (s *State) IsRestarting() bool {
    s.Lock()
    res := s.Restarting
    s.Unlock()
    return res
}

// 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()
    res := s.RemovalInProgress
    s.Unlock()
    return res
}

// 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()
    res := s.Dead
    s.Unlock()
    return res
}

// 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
}