431 lines
13 KiB
Go
431 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tasklifecycle
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
// coordinatorState represents a state of the task lifecycle Coordinator FSM.
|
|
type coordinatorState uint8
|
|
|
|
const (
|
|
coordinatorStateInit coordinatorState = iota
|
|
coordinatorStatePrestart
|
|
coordinatorStateMain
|
|
coordinatorStatePoststart
|
|
coordinatorStateWaitAlloc
|
|
coordinatorStatePoststop
|
|
)
|
|
|
|
func (s coordinatorState) String() string {
|
|
switch s {
|
|
case coordinatorStateInit:
|
|
return "init"
|
|
case coordinatorStatePrestart:
|
|
return "prestart"
|
|
case coordinatorStateMain:
|
|
return "main"
|
|
case coordinatorStatePoststart:
|
|
return "poststart"
|
|
case coordinatorStateWaitAlloc:
|
|
return "wait_alloc"
|
|
case coordinatorStatePoststop:
|
|
return "poststart"
|
|
}
|
|
panic(fmt.Sprintf("Unexpected task coordinator state %d", s))
|
|
}
|
|
|
|
// lifecycleStage represents a lifecycle configuration used for task
|
|
// coordination.
|
|
//
|
|
// Not all possible combinations of hook X sidecar are defined, only the ones
|
|
// that are relevant for coordinating task initialization order. For example, a
|
|
// main task with sidecar set to `true` starts at the same time as a
|
|
// non-sidecar main task, so there is no need to treat them differently.
|
|
type lifecycleStage uint8
|
|
|
|
const (
|
|
// lifecycleStagePrestartEphemeral are tasks with the "prestart" hook and
|
|
// sidecar set to "false".
|
|
lifecycleStagePrestartEphemeral lifecycleStage = iota
|
|
|
|
// lifecycleStagePrestartSidecar are tasks with the "prestart" hook and
|
|
// sidecar set to "true".
|
|
lifecycleStagePrestartSidecar
|
|
|
|
// lifecycleStageMain are tasks without a lifecycle or a lifecycle with an
|
|
// empty hook value.
|
|
lifecycleStageMain
|
|
|
|
// lifecycleStagePoststartEphemeral are tasks with the "poststart" hook and
|
|
// sidecar set to "false"
|
|
lifecycleStagePoststartEphemeral
|
|
|
|
// lifecycleStagePoststartSidecar are tasks with the "poststart" hook and
|
|
// sidecar set to "true".
|
|
lifecycleStagePoststartSidecar
|
|
|
|
// lifecycleStagePoststop are tasks with the "poststop" hook.
|
|
lifecycleStagePoststop
|
|
)
|
|
|
|
// Coordinator controls when tasks with a given lifecycle configuration are
|
|
// allowed to start and run.
|
|
//
|
|
// It behaves like a finite state machine where each state transition blocks or
|
|
// allows some task lifecycle types to run.
|
|
type Coordinator struct {
|
|
logger hclog.Logger
|
|
|
|
// tasksByLifecycle is an index used to group and quickly access tasks by
|
|
// their lifecycle stage.
|
|
tasksByLifecycle map[lifecycleStage][]string
|
|
|
|
// currentState is the current state of the FSM. It must only be accessed
|
|
// while holding the lock.
|
|
currentState coordinatorState
|
|
currentStateLock sync.RWMutex
|
|
|
|
// gates store the gates that control each task lifecycle stage.
|
|
gates map[lifecycleStage]*Gate
|
|
}
|
|
|
|
// NewCoordinator returns a new Coordinator with all tasks initially blocked.
|
|
func NewCoordinator(logger hclog.Logger, tasks []*structs.Task, shutdownCh <-chan struct{}) *Coordinator {
|
|
c := &Coordinator{
|
|
logger: logger.Named("task_coordinator"),
|
|
tasksByLifecycle: indexTasksByLifecycle(tasks),
|
|
gates: make(map[lifecycleStage]*Gate),
|
|
}
|
|
|
|
for lifecycle := range c.tasksByLifecycle {
|
|
c.gates[lifecycle] = NewGate(shutdownCh)
|
|
}
|
|
|
|
c.enterStateLocked(coordinatorStateInit)
|
|
return c
|
|
}
|
|
|
|
// Restart sets the Coordinator state back to "init" and is used to coordinate
|
|
// a full alloc restart. Since all tasks will run again they need to be pending
|
|
// before they are allowed to proceed.
|
|
func (c *Coordinator) Restart() {
|
|
c.currentStateLock.Lock()
|
|
defer c.currentStateLock.Unlock()
|
|
c.enterStateLocked(coordinatorStateInit)
|
|
}
|
|
|
|
// Restore is used to set the Coordinator FSM to the correct state when an
|
|
// alloc is restored. Must be called before the allocrunner is running.
|
|
func (c *Coordinator) Restore(states map[string]*structs.TaskState) {
|
|
// Skip the "init" state when restoring since the tasks were likely already
|
|
// running, causing the Coordinator to be stuck waiting for them to be
|
|
// "pending".
|
|
c.enterStateLocked(coordinatorStatePrestart)
|
|
c.TaskStateUpdated(states)
|
|
}
|
|
|
|
// StartConditionForTask returns a channel that is unblocked when the task is
|
|
// allowed to run.
|
|
func (c *Coordinator) StartConditionForTask(task *structs.Task) <-chan struct{} {
|
|
lifecycle := taskLifecycleStage(task)
|
|
return c.gates[lifecycle].WaitCh()
|
|
}
|
|
|
|
// TaskStateUpdated notifies that a task state has changed. This may cause the
|
|
// Coordinator to transition to another state.
|
|
func (c *Coordinator) TaskStateUpdated(states map[string]*structs.TaskState) {
|
|
c.currentStateLock.Lock()
|
|
defer c.currentStateLock.Unlock()
|
|
|
|
// We may be able to move directly through some states (for example, when
|
|
// an alloc doesn't have any prestart task we can skip the prestart state),
|
|
// so loop until we stabilize.
|
|
// This is also important when restoring an alloc since we need to find the
|
|
// state where FSM was last positioned.
|
|
for {
|
|
nextState := c.nextStateLocked(states)
|
|
if nextState == c.currentState {
|
|
return
|
|
}
|
|
|
|
c.enterStateLocked(nextState)
|
|
}
|
|
}
|
|
|
|
// nextStateLocked returns the state the FSM should transition to given its
|
|
// current internal state and the received states of the tasks.
|
|
// The currentStateLock must be held before calling this method.
|
|
func (c *Coordinator) nextStateLocked(states map[string]*structs.TaskState) coordinatorState {
|
|
|
|
// coordinatorStatePoststop is the terminal state of the FSM, and can be
|
|
// reached at any time.
|
|
if c.isAllocDone(states) {
|
|
return coordinatorStatePoststop
|
|
}
|
|
|
|
switch c.currentState {
|
|
case coordinatorStateInit:
|
|
if !c.isInitDone(states) {
|
|
return coordinatorStateInit
|
|
}
|
|
return coordinatorStatePrestart
|
|
|
|
case coordinatorStatePrestart:
|
|
if !c.isPrestartDone(states) {
|
|
return coordinatorStatePrestart
|
|
}
|
|
return coordinatorStateMain
|
|
|
|
case coordinatorStateMain:
|
|
if !c.isMainDone(states) {
|
|
return coordinatorStateMain
|
|
}
|
|
return coordinatorStatePoststart
|
|
|
|
case coordinatorStatePoststart:
|
|
if !c.isPoststartDone(states) {
|
|
return coordinatorStatePoststart
|
|
}
|
|
return coordinatorStateWaitAlloc
|
|
|
|
case coordinatorStateWaitAlloc:
|
|
if !c.isAllocDone(states) {
|
|
return coordinatorStateWaitAlloc
|
|
}
|
|
return coordinatorStatePoststop
|
|
|
|
case coordinatorStatePoststop:
|
|
return coordinatorStatePoststop
|
|
}
|
|
|
|
// If the code reaches here it's a programming error, since the switch
|
|
// statement should cover all possible states and return the next state.
|
|
panic(fmt.Sprintf("unexpected state %s", c.currentState))
|
|
}
|
|
|
|
// enterStateLocked updates the current state of the Coordinator FSM and
|
|
// executes any action necessary for the state transition.
|
|
// The currentStateLock must be held before calling this method.
|
|
func (c *Coordinator) enterStateLocked(state coordinatorState) {
|
|
c.logger.Trace("state transition", "from", c.currentState, "to", state)
|
|
|
|
switch state {
|
|
case coordinatorStateInit:
|
|
c.block(lifecycleStagePrestartEphemeral)
|
|
c.block(lifecycleStagePrestartSidecar)
|
|
c.block(lifecycleStageMain)
|
|
c.block(lifecycleStagePoststartEphemeral)
|
|
c.block(lifecycleStagePoststartSidecar)
|
|
c.block(lifecycleStagePoststop)
|
|
|
|
case coordinatorStatePrestart:
|
|
c.block(lifecycleStageMain)
|
|
c.block(lifecycleStagePoststartEphemeral)
|
|
c.block(lifecycleStagePoststartSidecar)
|
|
c.block(lifecycleStagePoststop)
|
|
|
|
c.allow(lifecycleStagePrestartEphemeral)
|
|
c.allow(lifecycleStagePrestartSidecar)
|
|
|
|
case coordinatorStateMain:
|
|
c.block(lifecycleStagePrestartEphemeral)
|
|
c.block(lifecycleStagePoststartEphemeral)
|
|
c.block(lifecycleStagePoststartSidecar)
|
|
c.block(lifecycleStagePoststop)
|
|
|
|
c.allow(lifecycleStagePrestartSidecar)
|
|
c.allow(lifecycleStageMain)
|
|
|
|
case coordinatorStatePoststart:
|
|
c.block(lifecycleStagePrestartEphemeral)
|
|
c.block(lifecycleStagePoststop)
|
|
|
|
c.allow(lifecycleStagePrestartSidecar)
|
|
c.allow(lifecycleStageMain)
|
|
c.allow(lifecycleStagePoststartEphemeral)
|
|
c.allow(lifecycleStagePoststartSidecar)
|
|
|
|
case coordinatorStateWaitAlloc:
|
|
c.block(lifecycleStagePrestartEphemeral)
|
|
c.block(lifecycleStagePoststartEphemeral)
|
|
c.block(lifecycleStagePoststop)
|
|
|
|
c.allow(lifecycleStagePrestartSidecar)
|
|
c.allow(lifecycleStageMain)
|
|
c.allow(lifecycleStagePoststartSidecar)
|
|
|
|
case coordinatorStatePoststop:
|
|
c.block(lifecycleStagePrestartEphemeral)
|
|
c.block(lifecycleStagePrestartSidecar)
|
|
c.block(lifecycleStageMain)
|
|
c.block(lifecycleStagePoststartEphemeral)
|
|
c.block(lifecycleStagePoststartSidecar)
|
|
|
|
c.allow(lifecycleStagePoststop)
|
|
}
|
|
|
|
c.currentState = state
|
|
}
|
|
|
|
// isInitDone returns true when the following conditions are met:
|
|
// - all tasks are in the "pending" state.
|
|
func (c *Coordinator) isInitDone(states map[string]*structs.TaskState) bool {
|
|
for _, task := range states {
|
|
if task.State != structs.TaskStatePending {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isPrestartDone returns true when the following conditions are met:
|
|
// - there is at least one prestart task
|
|
// - all ephemeral prestart tasks are successful.
|
|
// - no ephemeral prestart task has failed.
|
|
// - all prestart sidecar tasks are running.
|
|
func (c *Coordinator) isPrestartDone(states map[string]*structs.TaskState) bool {
|
|
if !c.hasPrestart() {
|
|
return true
|
|
}
|
|
|
|
for _, task := range c.tasksByLifecycle[lifecycleStagePrestartEphemeral] {
|
|
if !states[task].Successful() {
|
|
return false
|
|
}
|
|
}
|
|
for _, task := range c.tasksByLifecycle[lifecycleStagePrestartSidecar] {
|
|
if states[task].State != structs.TaskStateRunning {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isMainDone returns true when the following conditions are met:
|
|
// - there is at least one main task.
|
|
// - all main tasks are no longer "pending".
|
|
func (c *Coordinator) isMainDone(states map[string]*structs.TaskState) bool {
|
|
if !c.hasMain() {
|
|
return true
|
|
}
|
|
|
|
for _, task := range c.tasksByLifecycle[lifecycleStageMain] {
|
|
if states[task].State == structs.TaskStatePending {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isPoststartDone returns true when the following conditions are met:
|
|
// - there is at least one poststart task.
|
|
// - all ephemeral poststart tasks are in the "dead" state.
|
|
func (c *Coordinator) isPoststartDone(states map[string]*structs.TaskState) bool {
|
|
if !c.hasPoststart() {
|
|
return true
|
|
}
|
|
|
|
for _, task := range c.tasksByLifecycle[lifecycleStagePoststartEphemeral] {
|
|
if states[task].State != structs.TaskStateDead {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isAllocDone returns true when the following conditions are met:
|
|
// - all non-poststop tasks are in the "dead" state.
|
|
func (c *Coordinator) isAllocDone(states map[string]*structs.TaskState) bool {
|
|
for lifecycle, tasks := range c.tasksByLifecycle {
|
|
if lifecycle == lifecycleStagePoststop {
|
|
continue
|
|
}
|
|
|
|
for _, task := range tasks {
|
|
if states[task].State != structs.TaskStateDead {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Coordinator) hasPrestart() bool {
|
|
return len(c.tasksByLifecycle[lifecycleStagePrestartEphemeral])+
|
|
len(c.tasksByLifecycle[lifecycleStagePrestartSidecar]) > 0
|
|
}
|
|
|
|
func (c *Coordinator) hasMain() bool {
|
|
return len(c.tasksByLifecycle[lifecycleStageMain]) > 0
|
|
}
|
|
|
|
func (c *Coordinator) hasPoststart() bool {
|
|
return len(c.tasksByLifecycle[lifecycleStagePoststartEphemeral])+
|
|
len(c.tasksByLifecycle[lifecycleStagePoststartSidecar]) > 0
|
|
}
|
|
|
|
func (c *Coordinator) hasPoststop() bool {
|
|
return len(c.tasksByLifecycle[lifecycleStagePoststop]) > 0
|
|
}
|
|
|
|
// block is used to block the execution of tasks in the given lifecycle stage.
|
|
func (c *Coordinator) block(lifecycle lifecycleStage) {
|
|
gate := c.gates[lifecycle]
|
|
if gate != nil {
|
|
gate.Close()
|
|
}
|
|
}
|
|
|
|
// allows is used to allow the execution of tasks in the given lifecycle stage.
|
|
func (c *Coordinator) allow(lifecycle lifecycleStage) {
|
|
gate := c.gates[lifecycle]
|
|
if gate != nil {
|
|
gate.Open()
|
|
}
|
|
}
|
|
|
|
// indexTasksByLifecycle generates a map that groups tasks by their lifecycle
|
|
// configuration. This makes it easier to retrieve tasks by these groups or to
|
|
// determine if a task has a certain lifecycle configuration.
|
|
func indexTasksByLifecycle(tasks []*structs.Task) map[lifecycleStage][]string {
|
|
index := make(map[lifecycleStage][]string)
|
|
|
|
for _, task := range tasks {
|
|
lifecycle := taskLifecycleStage(task)
|
|
|
|
if _, ok := index[lifecycle]; !ok {
|
|
index[lifecycle] = []string{}
|
|
}
|
|
index[lifecycle] = append(index[lifecycle], task.Name)
|
|
}
|
|
|
|
return index
|
|
}
|
|
|
|
// taskLifecycleStage returns the relevant lifecycle stage for a given task.
|
|
func taskLifecycleStage(task *structs.Task) lifecycleStage {
|
|
if task.IsPrestart() {
|
|
if task.Lifecycle.Sidecar {
|
|
return lifecycleStagePrestartSidecar
|
|
}
|
|
return lifecycleStagePrestartEphemeral
|
|
} else if task.IsPoststart() {
|
|
if task.Lifecycle.Sidecar {
|
|
return lifecycleStagePoststartSidecar
|
|
}
|
|
return lifecycleStagePoststartEphemeral
|
|
} else if task.IsPoststop() {
|
|
return lifecycleStagePoststop
|
|
}
|
|
|
|
// Assume task is "main" by default.
|
|
return lifecycleStageMain
|
|
}
|