2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
package taskrunner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2018-10-06 01:42:15 +00:00
|
|
|
"errors"
|
2018-06-22 00:35:07 +00:00
|
|
|
"fmt"
|
2018-10-10 00:27:51 +00:00
|
|
|
"strings"
|
2018-06-22 00:35:07 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2022-09-21 19:53:25 +00:00
|
|
|
"golang.org/x/exp/slices"
|
2021-04-08 05:04:47 +00:00
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
metrics "github.com/armon/go-metrics"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
2018-10-11 00:08:57 +00:00
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
2020-05-19 16:50:47 +00:00
|
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
2023-05-08 18:17:10 +00:00
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
"github.com/hashicorp/nomad/client/allocdir"
|
2018-10-04 23:22:01 +00:00
|
|
|
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
|
|
|
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/restarts"
|
|
|
|
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/state"
|
2018-06-29 00:01:05 +00:00
|
|
|
"github.com/hashicorp/nomad/client/config"
|
2018-07-20 00:40:25 +00:00
|
|
|
"github.com/hashicorp/nomad/client/consul"
|
2018-11-16 23:29:59 +00:00
|
|
|
"github.com/hashicorp/nomad/client/devicemanager"
|
2019-10-22 13:20:26 +00:00
|
|
|
"github.com/hashicorp/nomad/client/dynamicplugins"
|
2018-11-15 15:13:14 +00:00
|
|
|
cinterfaces "github.com/hashicorp/nomad/client/interfaces"
|
2023-05-08 18:17:10 +00:00
|
|
|
"github.com/hashicorp/nomad/client/lib/cgutil"
|
2020-01-08 12:47:07 +00:00
|
|
|
"github.com/hashicorp/nomad/client/pluginmanager/csimanager"
|
2018-11-28 03:42:22 +00:00
|
|
|
"github.com/hashicorp/nomad/client/pluginmanager/drivermanager"
|
2022-03-15 08:38:30 +00:00
|
|
|
"github.com/hashicorp/nomad/client/serviceregistration"
|
2022-03-21 09:29:57 +00:00
|
|
|
"github.com/hashicorp/nomad/client/serviceregistration/wrapper"
|
2018-08-08 00:46:37 +00:00
|
|
|
cstate "github.com/hashicorp/nomad/client/state"
|
2018-09-15 00:08:26 +00:00
|
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
2018-11-30 11:18:39 +00:00
|
|
|
"github.com/hashicorp/nomad/client/taskenv"
|
2018-07-12 23:15:33 +00:00
|
|
|
"github.com/hashicorp/nomad/client/vaultclient"
|
2021-04-13 15:39:33 +00:00
|
|
|
"github.com/hashicorp/nomad/helper"
|
2019-01-23 14:27:14 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/pluginutils/hclspecutils"
|
|
|
|
"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
|
2018-12-18 22:11:25 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
2018-06-22 00:35:07 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2018-12-21 19:23:21 +00:00
|
|
|
bstructs "github.com/hashicorp/nomad/plugins/base/structs"
|
2018-10-06 01:42:15 +00:00
|
|
|
"github.com/hashicorp/nomad/plugins/drivers"
|
2018-07-11 04:22:04 +00:00
|
|
|
)
|
|
|
|
|
2018-07-16 21:37:27 +00:00
|
|
|
const (
|
test: port TestTaskRunner_CheckWatcher_Restart
Added ability to adjust the number of events the TaskRunner keeps as
there's no way to observe all events otherwise.
Task events differ slightly from 0.8 because 0.9 emits Terminated every
time a task exits instead of only when it exits on its own (not due to
restart or kill).
0.9 does not emit Killing/Killed for restarts like 0.8 which seems fine
as `Restart Signaled/Terminated/Restarting` is more descriptive.
Original v0.8 events emitted:
```
expected := []string{
"Received",
"Task Setup",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Not Restarting",
}
```
2019-01-18 16:30:44 +00:00
|
|
|
// defaultMaxEvents is the default max capacity for task events on the
|
|
|
|
// task state. Overrideable for testing.
|
|
|
|
defaultMaxEvents = 10
|
|
|
|
|
2018-07-16 21:37:27 +00:00
|
|
|
// killBackoffBaseline is the baseline time for exponential backoff while
|
|
|
|
// killing a task.
|
|
|
|
killBackoffBaseline = 5 * time.Second
|
|
|
|
|
|
|
|
// killBackoffLimit is the limit of the exponential backoff for killing
|
|
|
|
// the task.
|
|
|
|
killBackoffLimit = 2 * time.Minute
|
|
|
|
|
|
|
|
// killFailureLimit is how many times we will attempt to kill a task before
|
|
|
|
// giving up and potentially leaking resources.
|
|
|
|
killFailureLimit = 5
|
2018-08-01 18:03:52 +00:00
|
|
|
|
2019-11-27 21:41:45 +00:00
|
|
|
// triggerUpdateChCap is the capacity for the triggerUpdateCh used for
|
2018-08-01 18:03:52 +00:00
|
|
|
// triggering updates. It should be exactly 1 as even if multiple
|
|
|
|
// updates have come in since the last one was handled, we only need to
|
|
|
|
// handle the last one.
|
|
|
|
triggerUpdateChCap = 1
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
|
|
|
|
// restartChCap is the capacity for the restartCh used for triggering task
|
|
|
|
// restarts. It should be exactly 1 as even if multiple restarts have come
|
|
|
|
// we only need to handle the last one.
|
|
|
|
restartChCap = 1
|
2018-07-16 21:37:27 +00:00
|
|
|
)
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
type TaskRunner struct {
|
2018-11-16 23:29:59 +00:00
|
|
|
// allocID, taskName, taskLeader, and taskResources are immutable so these fields may
|
2018-10-12 01:03:48 +00:00
|
|
|
// be accessed without locks
|
2018-11-16 23:29:59 +00:00
|
|
|
allocID string
|
|
|
|
taskName string
|
|
|
|
taskLeader bool
|
|
|
|
taskResources *structs.AllocatedTaskResources
|
2018-06-29 00:20:13 +00:00
|
|
|
|
|
|
|
alloc *structs.Allocation
|
|
|
|
allocLock sync.Mutex
|
|
|
|
|
|
|
|
clientConfig *config.Config
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2018-07-18 20:45:55 +00:00
|
|
|
// stateUpdater is used to emit updated task state
|
|
|
|
stateUpdater interfaces.TaskStateHandler
|
|
|
|
|
2018-07-11 04:22:04 +00:00
|
|
|
// state captures the state of the task for updating the allocation
|
2018-11-14 18:29:07 +00:00
|
|
|
// Must acquire stateLock to access.
|
|
|
|
state *structs.TaskState
|
2018-07-11 04:22:04 +00:00
|
|
|
|
|
|
|
// localState captures the node-local state of the task for when the
|
2018-11-14 18:29:07 +00:00
|
|
|
// Nomad agent restarts.
|
|
|
|
// Must acquire stateLock to access.
|
|
|
|
localState *state.LocalState
|
|
|
|
|
|
|
|
// stateLock must be acquired when accessing state or localState.
|
|
|
|
stateLock sync.RWMutex
|
2018-07-11 04:22:04 +00:00
|
|
|
|
2018-08-08 00:46:37 +00:00
|
|
|
// stateDB is for persisting localState and taskState
|
|
|
|
stateDB cstate.StateDB
|
2018-07-11 04:22:04 +00:00
|
|
|
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
// restartCh is used to signal that the task should restart.
|
|
|
|
restartCh chan struct{}
|
|
|
|
|
2019-01-09 00:42:26 +00:00
|
|
|
// shutdownCtx is used to exit the TaskRunner *without* affecting task state.
|
|
|
|
shutdownCtx context.Context
|
|
|
|
|
|
|
|
// shutdownCtxCancel causes the TaskRunner to exit immediately without
|
|
|
|
// affecting task state. Useful for testing or graceful agent shutdown.
|
|
|
|
shutdownCtxCancel context.CancelFunc
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
// killCtx is the task runner's context representing the tasks's lifecycle.
|
|
|
|
// The context is canceled when the task is killed.
|
|
|
|
killCtx context.Context
|
|
|
|
|
|
|
|
// killCtxCancel is called when killing a task.
|
|
|
|
killCtxCancel context.CancelFunc
|
|
|
|
|
2019-01-09 19:42:40 +00:00
|
|
|
// killErr is populated when killing a task. Access should be done use the
|
2019-01-09 00:42:26 +00:00
|
|
|
// getter/setter
|
|
|
|
killErr error
|
|
|
|
killErrLock sync.Mutex
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2021-12-13 19:54:53 +00:00
|
|
|
// shutdownDelayCtx is a context from the alloc runner which will
|
|
|
|
// tell us to exit early from shutdown_delay
|
|
|
|
shutdownDelayCtx context.Context
|
|
|
|
shutdownDelayCancelFn context.CancelFunc
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// Logger is the logger for the task runner.
|
|
|
|
logger log.Logger
|
|
|
|
|
2018-08-01 18:03:52 +00:00
|
|
|
// triggerUpdateCh is ticked whenever update hooks need to be run and
|
|
|
|
// must be created with cap=1 to signal a pending update and prevent
|
|
|
|
// callers from deadlocking if the receiver has exited.
|
|
|
|
triggerUpdateCh chan struct{}
|
2018-06-29 21:53:31 +00:00
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// waitCh is closed when the task runner has transitioned to a terminal
|
|
|
|
// state
|
|
|
|
waitCh chan struct{}
|
|
|
|
|
|
|
|
// driver is the driver for the task.
|
2018-10-06 01:42:15 +00:00
|
|
|
driver drivers.DriverPlugin
|
|
|
|
|
|
|
|
// driverCapabilities is the set capabilities the driver supports
|
|
|
|
driverCapabilities *drivers.Capabilities
|
|
|
|
|
|
|
|
// taskSchema is the hcl spec for the task driver configuration
|
|
|
|
taskSchema hcldec.Spec
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2018-09-28 00:32:17 +00:00
|
|
|
// handleLock guards access to handle and handleResult
|
|
|
|
handleLock sync.Mutex
|
|
|
|
|
|
|
|
// handle to the running driver
|
2018-10-10 03:01:20 +00:00
|
|
|
handle *DriverHandle
|
2018-09-28 00:32:17 +00:00
|
|
|
|
2018-07-11 04:22:04 +00:00
|
|
|
// task is the task being run
|
2018-06-22 00:35:07 +00:00
|
|
|
task *structs.Task
|
|
|
|
taskLock sync.RWMutex
|
|
|
|
|
|
|
|
// taskDir is the directory structure for this task.
|
|
|
|
taskDir *allocdir.TaskDir
|
|
|
|
|
|
|
|
// envBuilder is used to build the task's environment
|
2018-11-30 11:18:39 +00:00
|
|
|
envBuilder *taskenv.Builder
|
2018-06-22 00:35:07 +00:00
|
|
|
|
|
|
|
// restartTracker is used to decide if the task should be restarted.
|
|
|
|
restartTracker *restarts.RestartTracker
|
|
|
|
|
|
|
|
// runnerHooks are task runner lifecycle hooks that should be run on state
|
|
|
|
// transistions.
|
|
|
|
runnerHooks []interfaces.TaskHook
|
|
|
|
|
2018-11-16 23:29:59 +00:00
|
|
|
// hookResources captures the resources provided by hooks
|
|
|
|
hookResources *hookResources
|
|
|
|
|
2023-04-03 15:03:36 +00:00
|
|
|
// allocHookResources captures the resources provided by the allocrunner hooks
|
|
|
|
allocHookResources *cstructs.AllocHookResources
|
|
|
|
|
2018-07-20 00:40:25 +00:00
|
|
|
// consulClient is the client used by the consul service hook for
|
|
|
|
// registering services and checks
|
2022-03-15 08:38:30 +00:00
|
|
|
consulServiceClient serviceregistration.Handler
|
2020-09-04 17:50:11 +00:00
|
|
|
|
|
|
|
// consulProxiesClient is the client used by the envoy version hook for
|
|
|
|
// asking consul what version of envoy nomad should inject into the connect
|
|
|
|
// sidecar or gateway task.
|
|
|
|
consulProxiesClient consul.SupportedProxiesAPI
|
2018-07-20 00:40:25 +00:00
|
|
|
|
2019-11-27 21:41:45 +00:00
|
|
|
// sidsClient is the client used by the service identity hook for managing
|
|
|
|
// service identity tokens
|
|
|
|
siClient consul.ServiceIdentityAPI
|
|
|
|
|
2018-07-12 23:15:33 +00:00
|
|
|
// vaultClient is the client to use to derive and renew Vault tokens
|
|
|
|
vaultClient vaultclient.VaultClient
|
|
|
|
|
|
|
|
// vaultToken is the current Vault token. It should be accessed with the
|
|
|
|
// getter.
|
|
|
|
vaultToken string
|
|
|
|
vaultTokenLock sync.Mutex
|
|
|
|
|
2022-06-10 13:41:54 +00:00
|
|
|
// nomadToken is the current Nomad workload identity token. It
|
|
|
|
// should be accessed with the getter.
|
|
|
|
nomadToken string
|
|
|
|
nomadTokenLock sync.Mutex
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// baseLabels are used when emitting tagged metrics. All task runner metrics
|
|
|
|
// will have these tags, and optionally more.
|
|
|
|
baseLabels []metrics.Label
|
2018-09-15 00:08:26 +00:00
|
|
|
|
2018-09-24 18:37:45 +00:00
|
|
|
// logmonHookConfig is used to get the paths to the stdout and stderr fifos
|
|
|
|
// to be passed to the driver for task logging
|
|
|
|
logmonHookConfig *logmonHookConfig
|
|
|
|
|
2018-09-15 00:08:26 +00:00
|
|
|
// resourceUsage is written via UpdateStats and read via
|
|
|
|
// LatestResourceUsage. May be nil at all times.
|
|
|
|
resourceUsage *cstructs.TaskResourceUsage
|
|
|
|
resourceUsageLock sync.Mutex
|
2018-10-06 01:42:15 +00:00
|
|
|
|
2018-11-15 15:13:14 +00:00
|
|
|
// deviceStatsReporter is used to lookup resource usage for alloc devices
|
|
|
|
deviceStatsReporter cinterfaces.DeviceStatsReporter
|
|
|
|
|
2020-01-08 12:47:07 +00:00
|
|
|
// csiManager is used to manage the mounting of CSI volumes into tasks
|
|
|
|
csiManager csimanager.Manager
|
|
|
|
|
2018-11-16 23:29:59 +00:00
|
|
|
// devicemanager is used to mount devices as well as lookup device
|
|
|
|
// statistics
|
|
|
|
devicemanager devicemanager.Manager
|
2018-11-28 03:42:22 +00:00
|
|
|
|
2021-04-08 05:04:47 +00:00
|
|
|
// cpusetCgroupPathGetter is used to lookup the cgroup path if supported by the platform
|
|
|
|
cpusetCgroupPathGetter cgutil.CgroupPathGetter
|
|
|
|
|
2018-11-28 03:42:22 +00:00
|
|
|
// driverManager is used to dispense driver plugins and register event
|
|
|
|
// handlers
|
|
|
|
driverManager drivermanager.Manager
|
2019-01-09 00:42:26 +00:00
|
|
|
|
2019-10-22 13:20:26 +00:00
|
|
|
// dynamicRegistry is where dynamic plugins should be registered.
|
|
|
|
dynamicRegistry dynamicplugins.Registry
|
|
|
|
|
test: port TestTaskRunner_CheckWatcher_Restart
Added ability to adjust the number of events the TaskRunner keeps as
there's no way to observe all events otherwise.
Task events differ slightly from 0.8 because 0.9 emits Terminated every
time a task exits instead of only when it exits on its own (not due to
restart or kill).
0.9 does not emit Killing/Killed for restarts like 0.8 which seems fine
as `Restart Signaled/Terminated/Restarting` is more descriptive.
Original v0.8 events emitted:
```
expected := []string{
"Received",
"Task Setup",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Not Restarting",
}
```
2019-01-18 16:30:44 +00:00
|
|
|
// maxEvents is the capacity of the TaskEvents on the TaskState.
|
|
|
|
// Defaults to defaultMaxEvents but overrideable for testing.
|
|
|
|
maxEvents int
|
2019-05-08 06:04:40 +00:00
|
|
|
|
2019-05-10 15:51:06 +00:00
|
|
|
// serversContactedCh is passed to TaskRunners so they can detect when
|
2019-05-14 16:10:09 +00:00
|
|
|
// GetClientAllocs has been called in case of a failed restore.
|
2019-05-10 15:51:06 +00:00
|
|
|
serversContactedCh <-chan struct{}
|
|
|
|
|
2022-08-22 22:38:49 +00:00
|
|
|
// startConditionMetCh signals the TaskRunner when it should start the task
|
|
|
|
startConditionMetCh <-chan struct{}
|
2019-12-04 20:44:21 +00:00
|
|
|
|
2019-05-10 15:51:06 +00:00
|
|
|
// waitOnServers defaults to false but will be set true if a restore
|
|
|
|
// fails and the Run method should wait until serversContactedCh is
|
|
|
|
// closed.
|
|
|
|
waitOnServers bool
|
2019-04-29 17:35:15 +00:00
|
|
|
|
|
|
|
networkIsolationLock sync.Mutex
|
|
|
|
networkIsolationSpec *drivers.NetworkIsolationSpec
|
2020-02-11 16:39:16 +00:00
|
|
|
|
2022-03-21 09:29:57 +00:00
|
|
|
// serviceRegWrapper is the handler wrapper that is used by service hooks
|
|
|
|
// to perform service and check registration and deregistration.
|
|
|
|
serviceRegWrapper *wrapper.HandlerWrapper
|
2022-05-03 22:38:32 +00:00
|
|
|
|
|
|
|
// getter is an interface for retrieving artifacts.
|
|
|
|
getter cinterfaces.ArtifactGetter
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Config struct {
|
2018-06-29 00:01:05 +00:00
|
|
|
Alloc *structs.Allocation
|
|
|
|
ClientConfig *config.Config
|
|
|
|
Task *structs.Task
|
|
|
|
TaskDir *allocdir.TaskDir
|
|
|
|
Logger log.Logger
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2019-11-27 21:41:45 +00:00
|
|
|
// Consul is the client to use for managing Consul service registrations
|
2022-03-15 08:38:30 +00:00
|
|
|
Consul serviceregistration.Handler
|
2019-11-27 21:41:45 +00:00
|
|
|
|
2020-09-04 17:50:11 +00:00
|
|
|
// ConsulProxies is the client to use for looking up supported envoy versions
|
|
|
|
// from Consul.
|
|
|
|
ConsulProxies consul.SupportedProxiesAPI
|
|
|
|
|
2019-11-27 21:41:45 +00:00
|
|
|
// ConsulSI is the client to use for managing Consul SI tokens
|
|
|
|
ConsulSI consul.ServiceIdentityAPI
|
|
|
|
|
2019-10-22 13:20:26 +00:00
|
|
|
// DynamicRegistry is where dynamic plugins should be registered.
|
|
|
|
DynamicRegistry dynamicplugins.Registry
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
// Vault is the client to use to derive and renew Vault tokens
|
|
|
|
Vault vaultclient.VaultClient
|
2018-07-12 23:15:33 +00:00
|
|
|
|
2018-07-11 04:22:04 +00:00
|
|
|
// StateDB is used to store and restore state.
|
2018-08-08 00:46:37 +00:00
|
|
|
StateDB cstate.StateDB
|
2018-07-18 20:45:55 +00:00
|
|
|
|
|
|
|
// StateUpdater is used to emit updated task state
|
|
|
|
StateUpdater interfaces.TaskStateHandler
|
2018-10-06 01:42:15 +00:00
|
|
|
|
2018-11-15 15:13:14 +00:00
|
|
|
// deviceStatsReporter is used to lookup resource usage for alloc devices
|
|
|
|
DeviceStatsReporter cinterfaces.DeviceStatsReporter
|
|
|
|
|
2020-01-08 12:47:07 +00:00
|
|
|
// CSIManager is used to manage the mounting of CSI volumes into tasks
|
|
|
|
CSIManager csimanager.Manager
|
|
|
|
|
2021-04-08 05:04:47 +00:00
|
|
|
// CpusetCgroupPathGetter is used to lookup the cgroup path if supported by the platform
|
|
|
|
CpusetCgroupPathGetter cgutil.CgroupPathGetter
|
|
|
|
|
2018-11-16 23:29:59 +00:00
|
|
|
// DeviceManager is used to mount devices as well as lookup device
|
|
|
|
// statistics
|
|
|
|
DeviceManager devicemanager.Manager
|
2018-11-28 03:42:22 +00:00
|
|
|
|
|
|
|
// DriverManager is used to dispense driver plugins and register event
|
|
|
|
// handlers
|
|
|
|
DriverManager drivermanager.Manager
|
2019-05-10 15:51:06 +00:00
|
|
|
|
|
|
|
// ServersContactedCh is closed when the first GetClientAllocs call to
|
|
|
|
// servers succeeds and allocs are synced.
|
|
|
|
ServersContactedCh chan struct{}
|
2019-12-04 20:44:21 +00:00
|
|
|
|
2022-08-22 22:38:49 +00:00
|
|
|
// StartConditionMetCh signals the TaskRunner when it should start the task
|
|
|
|
StartConditionMetCh <-chan struct{}
|
2021-12-13 19:54:53 +00:00
|
|
|
|
|
|
|
// ShutdownDelayCtx is a context from the alloc runner which will
|
|
|
|
// tell us to exit early from shutdown_delay
|
|
|
|
ShutdownDelayCtx context.Context
|
|
|
|
|
|
|
|
// ShutdownDelayCancelFn should only be used in testing.
|
|
|
|
ShutdownDelayCancelFn context.CancelFunc
|
2022-03-21 09:29:57 +00:00
|
|
|
|
|
|
|
// ServiceRegWrapper is the handler wrapper that is used by service hooks
|
|
|
|
// to perform service and check registration and deregistration.
|
|
|
|
ServiceRegWrapper *wrapper.HandlerWrapper
|
2022-05-03 22:38:32 +00:00
|
|
|
|
|
|
|
// Getter is an interface for retrieving artifacts.
|
|
|
|
Getter cinterfaces.ArtifactGetter
|
2023-04-03 15:03:36 +00:00
|
|
|
|
|
|
|
// AllocHookResources is how taskrunner hooks can get state written by
|
|
|
|
// allocrunner hooks
|
|
|
|
AllocHookResources *cstructs.AllocHookResources
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewTaskRunner(config *Config) (*TaskRunner, error) {
|
2018-10-18 20:39:02 +00:00
|
|
|
// Create a context for causing the runner to exit
|
2018-06-22 00:35:07 +00:00
|
|
|
trCtx, trCancel := context.WithCancel(context.Background())
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
// Create a context for killing the runner
|
|
|
|
killCtx, killCancel := context.WithCancel(context.Background())
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// Initialize the environment builder
|
2018-11-30 11:18:39 +00:00
|
|
|
envBuilder := taskenv.NewBuilder(
|
2018-06-29 00:01:05 +00:00
|
|
|
config.ClientConfig.Node,
|
|
|
|
config.Alloc,
|
2018-06-22 00:35:07 +00:00
|
|
|
config.Task,
|
2018-06-29 21:53:31 +00:00
|
|
|
config.ClientConfig.Region,
|
|
|
|
)
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2018-10-16 22:17:36 +00:00
|
|
|
// Initialize state from alloc if it is set
|
|
|
|
tstate := structs.NewTaskState()
|
|
|
|
if ts := config.Alloc.TaskStates[config.Task.Name]; ts != nil {
|
|
|
|
tstate = ts.Copy()
|
|
|
|
}
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
tr := &TaskRunner{
|
2021-04-08 05:04:47 +00:00
|
|
|
alloc: config.Alloc,
|
|
|
|
allocID: config.Alloc.ID,
|
|
|
|
clientConfig: config.ClientConfig,
|
|
|
|
task: config.Task,
|
|
|
|
taskDir: config.TaskDir,
|
|
|
|
taskName: config.Task.Name,
|
|
|
|
taskLeader: config.Task.Leader,
|
|
|
|
envBuilder: envBuilder,
|
|
|
|
dynamicRegistry: config.DynamicRegistry,
|
|
|
|
consulServiceClient: config.Consul,
|
|
|
|
consulProxiesClient: config.ConsulProxies,
|
|
|
|
siClient: config.ConsulSI,
|
|
|
|
vaultClient: config.Vault,
|
|
|
|
state: tstate,
|
|
|
|
localState: state.NewLocalState(),
|
2023-04-03 15:03:36 +00:00
|
|
|
allocHookResources: config.AllocHookResources,
|
2021-04-08 05:04:47 +00:00
|
|
|
stateDB: config.StateDB,
|
|
|
|
stateUpdater: config.StateUpdater,
|
|
|
|
deviceStatsReporter: config.DeviceStatsReporter,
|
|
|
|
killCtx: killCtx,
|
|
|
|
killCtxCancel: killCancel,
|
|
|
|
shutdownCtx: trCtx,
|
|
|
|
shutdownCtxCancel: trCancel,
|
|
|
|
triggerUpdateCh: make(chan struct{}, triggerUpdateChCap),
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
restartCh: make(chan struct{}, restartChCap),
|
2021-04-08 05:04:47 +00:00
|
|
|
waitCh: make(chan struct{}),
|
|
|
|
csiManager: config.CSIManager,
|
|
|
|
cpusetCgroupPathGetter: config.CpusetCgroupPathGetter,
|
|
|
|
devicemanager: config.DeviceManager,
|
|
|
|
driverManager: config.DriverManager,
|
|
|
|
maxEvents: defaultMaxEvents,
|
|
|
|
serversContactedCh: config.ServersContactedCh,
|
2022-08-22 22:38:49 +00:00
|
|
|
startConditionMetCh: config.StartConditionMetCh,
|
2021-12-13 19:54:53 +00:00
|
|
|
shutdownDelayCtx: config.ShutdownDelayCtx,
|
|
|
|
shutdownDelayCancelFn: config.ShutdownDelayCancelFn,
|
2022-03-21 09:29:57 +00:00
|
|
|
serviceRegWrapper: config.ServiceRegWrapper,
|
2022-05-03 22:38:32 +00:00
|
|
|
getter: config.Getter,
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create the logger based on the allocation ID
|
|
|
|
tr.logger = config.Logger.Named("task_runner").With("task", config.Task.Name)
|
|
|
|
|
2018-11-16 23:29:59 +00:00
|
|
|
// Pull out the task's resources
|
|
|
|
ares := tr.alloc.AllocatedResources
|
2020-01-15 13:57:05 +00:00
|
|
|
if ares == nil {
|
2020-01-09 14:25:07 +00:00
|
|
|
return nil, fmt.Errorf("no task resources found on allocation")
|
2018-11-16 23:29:59 +00:00
|
|
|
}
|
|
|
|
|
2020-01-15 13:57:05 +00:00
|
|
|
tres, ok := ares.Tasks[tr.taskName]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("no task resources found on allocation")
|
|
|
|
}
|
|
|
|
tr.taskResources = tres
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// Build the restart tracker.
|
2020-03-07 02:52:58 +00:00
|
|
|
rp := config.Task.RestartPolicy
|
|
|
|
if rp == nil {
|
|
|
|
tg := tr.alloc.Job.LookupTaskGroup(tr.alloc.TaskGroup)
|
|
|
|
if tg == nil {
|
|
|
|
tr.logger.Error("alloc missing task group")
|
|
|
|
return nil, fmt.Errorf("alloc missing task group")
|
|
|
|
}
|
|
|
|
rp = tg.RestartPolicy
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
2020-03-07 02:52:58 +00:00
|
|
|
tr.restartTracker = restarts.NewRestartTracker(rp, tr.alloc.Job.Type, config.Task.Lifecycle)
|
2018-06-22 00:35:07 +00:00
|
|
|
|
|
|
|
// Get the driver
|
|
|
|
if err := tr.initDriver(); err != nil {
|
|
|
|
tr.logger.Error("failed to create driver", "error", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-11-03 15:10:11 +00:00
|
|
|
// Use the client secret only as the initial value; the identity hook will
|
|
|
|
// update this with a workload identity if one is available
|
|
|
|
tr.setNomadToken(config.ClientConfig.Node.SecretID)
|
|
|
|
|
2020-12-17 23:21:46 +00:00
|
|
|
// Initialize the runners hooks. Must come after initDriver so hooks
|
|
|
|
// can use tr.driverCapabilities
|
2018-06-22 00:35:07 +00:00
|
|
|
tr.initHooks()
|
|
|
|
|
|
|
|
// Initialize base labels
|
|
|
|
tr.initLabels()
|
|
|
|
|
2019-01-04 17:00:29 +00:00
|
|
|
// Initialize initial task received event
|
|
|
|
tr.appendEvent(structs.NewTaskEvent(structs.TaskReceived))
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
return tr, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tr *TaskRunner) initLabels() {
|
2018-06-29 00:20:13 +00:00
|
|
|
alloc := tr.Alloc()
|
2018-06-22 00:35:07 +00:00
|
|
|
tr.baseLabels = []metrics.Label{
|
|
|
|
{
|
|
|
|
Name: "job",
|
|
|
|
Value: alloc.Job.Name,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "task_group",
|
|
|
|
Value: alloc.TaskGroup,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "alloc_id",
|
2018-06-29 00:20:13 +00:00
|
|
|
Value: tr.allocID,
|
2018-06-22 00:35:07 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "task",
|
2018-06-29 22:39:22 +00:00
|
|
|
Value: tr.taskName,
|
2018-06-22 00:35:07 +00:00
|
|
|
},
|
2019-06-18 14:00:57 +00:00
|
|
|
{
|
|
|
|
Name: "namespace",
|
2019-06-17 16:52:49 +00:00
|
|
|
Value: tr.alloc.Namespace,
|
2019-06-18 14:00:57 +00:00
|
|
|
},
|
2019-06-17 16:52:49 +00:00
|
|
|
}
|
|
|
|
|
2018-10-10 00:27:51 +00:00
|
|
|
if tr.alloc.Job.ParentID != "" {
|
|
|
|
tr.baseLabels = append(tr.baseLabels, metrics.Label{
|
|
|
|
Name: "parent_id",
|
|
|
|
Value: tr.alloc.Job.ParentID,
|
|
|
|
})
|
|
|
|
if strings.Contains(tr.alloc.Job.Name, "/dispatch-") {
|
|
|
|
tr.baseLabels = append(tr.baseLabels, metrics.Label{
|
|
|
|
Name: "dispatch_id",
|
|
|
|
Value: strings.Split(tr.alloc.Job.Name, "/dispatch-")[1],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if strings.Contains(tr.alloc.Job.Name, "/periodic-") {
|
|
|
|
tr.baseLabels = append(tr.baseLabels, metrics.Label{
|
|
|
|
Name: "periodic_id",
|
|
|
|
Value: strings.Split(tr.alloc.Job.Name, "/periodic-")[1],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2023-05-08 18:17:10 +00:00
|
|
|
// MarkFailedKill marks a task as failed and should be killed.
|
|
|
|
// It should be invoked when alloc runner prestart hooks fail.
|
|
|
|
// Afterwards, Run() will perform any necessary cleanup.
|
|
|
|
func (tr *TaskRunner) MarkFailedKill(reason string) {
|
|
|
|
// Emit an event that fails the task and gives reasons for humans.
|
2019-06-29 08:56:40 +00:00
|
|
|
event := structs.NewTaskEvent(structs.TaskSetupFailure).
|
2023-05-08 18:17:10 +00:00
|
|
|
SetKillReason(structs.TaskRestoreFailed).
|
2019-06-29 08:56:40 +00:00
|
|
|
SetDisplayMessage(reason).
|
|
|
|
SetFailsTask()
|
2023-05-08 18:17:10 +00:00
|
|
|
tr.EmitEvent(event)
|
2019-07-02 10:37:52 +00:00
|
|
|
|
2023-05-08 18:17:10 +00:00
|
|
|
// Cancel kill context, so later when allocRunner runs tr.Run(),
|
|
|
|
// we'll follow the usual kill path and do all the appropriate cleanup steps.
|
|
|
|
tr.killCtxCancel()
|
2019-06-29 08:56:40 +00:00
|
|
|
}
|
|
|
|
|
2018-11-26 20:50:35 +00:00
|
|
|
// Run the TaskRunner. Starts the user's task or reattaches to a restored task.
|
|
|
|
// Run closes WaitCh when it exits. Should be started in a goroutine.
|
2018-06-22 00:35:07 +00:00
|
|
|
func (tr *TaskRunner) Run() {
|
|
|
|
defer close(tr.waitCh)
|
2018-10-06 01:42:15 +00:00
|
|
|
var result *drivers.ExitResult
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2019-06-26 15:39:10 +00:00
|
|
|
tr.stateLock.RLock()
|
|
|
|
dead := tr.state.State == structs.TaskStateDead
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
runComplete := tr.localState.RunComplete
|
2019-06-26 15:39:10 +00:00
|
|
|
tr.stateLock.RUnlock()
|
|
|
|
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
// If restoring a dead task, ensure the task is cleared and, if the local
|
|
|
|
// state indicates that the previous Run() call is complete, execute all
|
|
|
|
// post stop hooks and exit early, otherwise proceed until the
|
|
|
|
// ALLOC_RESTART loop skipping MAIN since the task is dead.
|
2019-06-26 15:39:10 +00:00
|
|
|
if dead {
|
2019-07-02 06:53:50 +00:00
|
|
|
// do cleanup functions without emitting any additional events/work
|
|
|
|
// to handle cases where we restored a dead task where client terminated
|
|
|
|
// after task finished before completing post-run actions.
|
2019-06-26 15:39:10 +00:00
|
|
|
tr.clearDriverHandle()
|
2019-07-02 06:53:50 +00:00
|
|
|
tr.stateUpdater.TaskStateUpdated()
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
if runComplete {
|
|
|
|
if err := tr.stop(); err != nil {
|
|
|
|
tr.logger.Error("stop failed on terminal task", "error", err)
|
|
|
|
}
|
|
|
|
return
|
2019-06-26 15:39:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-01 18:03:52 +00:00
|
|
|
// Updates are handled asynchronously with the other hooks but each
|
|
|
|
// triggered update - whether due to alloc updates or a new vault token
|
|
|
|
// - should be handled serially.
|
|
|
|
go tr.handleUpdates()
|
|
|
|
|
2019-05-10 15:51:06 +00:00
|
|
|
// If restore failed wait until servers are contacted before running.
|
|
|
|
// #1795
|
|
|
|
if tr.waitOnServers {
|
2019-05-08 06:04:40 +00:00
|
|
|
tr.logger.Info("task failed to restore; waiting to contact server before restarting")
|
|
|
|
select {
|
|
|
|
case <-tr.killCtx.Done():
|
2020-12-17 23:21:46 +00:00
|
|
|
tr.logger.Info("task killed while waiting for server contact")
|
2019-05-08 06:04:40 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
|
|
|
return
|
2019-05-10 15:51:06 +00:00
|
|
|
case <-tr.serversContactedCh:
|
2019-05-22 11:47:35 +00:00
|
|
|
tr.logger.Info("server contacted; unblocking waiting task")
|
2019-05-08 06:04:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-22 22:38:49 +00:00
|
|
|
// Set the initial task state.
|
|
|
|
tr.stateUpdater.TaskStateUpdated()
|
|
|
|
|
2022-11-11 15:31:34 +00:00
|
|
|
// start with a stopped timer; actual restart delay computed later
|
|
|
|
timer, stop := helper.NewStoppedTimer()
|
2022-02-02 16:59:53 +00:00
|
|
|
defer stop()
|
|
|
|
|
2018-06-29 23:22:18 +00:00
|
|
|
MAIN:
|
2020-11-12 16:01:42 +00:00
|
|
|
for !tr.shouldShutdown() {
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
if dead {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
select {
|
|
|
|
case <-tr.killCtx.Done():
|
|
|
|
break MAIN
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
2018-10-18 20:39:02 +00:00
|
|
|
// TaskRunner was told to exit immediately
|
|
|
|
return
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
case <-tr.startConditionMetCh:
|
|
|
|
tr.logger.Debug("lifecycle start condition has been met, proceeding")
|
|
|
|
// yay proceed
|
2018-10-18 20:39:02 +00:00
|
|
|
}
|
|
|
|
|
2018-07-17 00:19:56 +00:00
|
|
|
// Run the prestart hooks
|
|
|
|
if err := tr.prestart(); err != nil {
|
|
|
|
tr.logger.Error("prestart failed", "error", err)
|
2018-06-22 00:35:07 +00:00
|
|
|
tr.restartTracker.SetStartError(err)
|
|
|
|
goto RESTART
|
|
|
|
}
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
select {
|
|
|
|
case <-tr.killCtx.Done():
|
2018-07-16 21:37:27 +00:00
|
|
|
break MAIN
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
2018-10-18 20:39:02 +00:00
|
|
|
// TaskRunner was told to exit immediately
|
|
|
|
return
|
|
|
|
default:
|
2018-07-16 21:37:27 +00:00
|
|
|
}
|
|
|
|
|
2018-06-29 21:53:31 +00:00
|
|
|
// Run the task
|
2018-06-29 23:22:18 +00:00
|
|
|
if err := tr.runDriver(); err != nil {
|
2018-06-22 00:35:07 +00:00
|
|
|
tr.logger.Error("running driver failed", "error", err)
|
|
|
|
tr.restartTracker.SetStartError(err)
|
|
|
|
goto RESTART
|
|
|
|
}
|
|
|
|
|
2018-07-17 00:19:56 +00:00
|
|
|
// Run the poststart hooks
|
|
|
|
if err := tr.poststart(); err != nil {
|
|
|
|
tr.logger.Error("poststart failed", "error", err)
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-09-20 22:44:27 +00:00
|
|
|
// Grab the result proxy and wait for task to exit
|
2018-12-21 19:23:21 +00:00
|
|
|
WAIT:
|
2018-09-20 22:44:27 +00:00
|
|
|
{
|
2018-10-06 01:42:15 +00:00
|
|
|
handle := tr.getDriverHandle()
|
2018-12-21 19:23:21 +00:00
|
|
|
result = nil
|
2018-07-16 21:37:27 +00:00
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
// Do *not* use tr.killCtx here as it would cause
|
|
|
|
// Wait() to unblock before the task exits when Kill()
|
|
|
|
// is called.
|
2018-10-06 01:42:15 +00:00
|
|
|
if resultCh, err := handle.WaitCh(context.Background()); err != nil {
|
|
|
|
tr.logger.Error("wait task failed", "error", err)
|
|
|
|
} else {
|
2018-10-18 20:39:02 +00:00
|
|
|
select {
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.killCtx.Done():
|
|
|
|
// We can go through the normal should restart check since
|
|
|
|
// the restart tracker knowns it is killed
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
result = tr.handleKill(resultCh)
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
2018-10-18 20:39:02 +00:00
|
|
|
// TaskRunner was told to exit immediately
|
|
|
|
return
|
2019-01-04 22:04:45 +00:00
|
|
|
case result = <-resultCh:
|
2018-10-18 20:39:02 +00:00
|
|
|
}
|
2018-12-21 19:23:21 +00:00
|
|
|
|
|
|
|
// WaitCh returned a result
|
|
|
|
if retryWait := tr.handleTaskExitResult(result); retryWait {
|
|
|
|
goto WAIT
|
|
|
|
}
|
2018-10-06 01:42:15 +00:00
|
|
|
}
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-09-20 22:44:27 +00:00
|
|
|
// Clear the handle
|
|
|
|
tr.clearDriverHandle()
|
|
|
|
|
|
|
|
// Store the wait result on the restart tracker
|
2018-10-06 01:42:15 +00:00
|
|
|
tr.restartTracker.SetExitResult(result)
|
2018-09-20 22:44:27 +00:00
|
|
|
|
2018-07-20 00:40:25 +00:00
|
|
|
if err := tr.exited(); err != nil {
|
|
|
|
tr.logger.Error("exited hooks failed", "error", err)
|
|
|
|
}
|
2018-07-16 21:37:27 +00:00
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
RESTART:
|
2018-09-20 22:44:27 +00:00
|
|
|
restart, restartDelay := tr.shouldRestart()
|
2018-06-29 23:22:18 +00:00
|
|
|
if !restart {
|
2018-07-16 21:37:27 +00:00
|
|
|
break MAIN
|
2018-06-29 23:22:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-02 16:59:53 +00:00
|
|
|
timer.Reset(restartDelay)
|
|
|
|
|
2018-09-20 22:44:27 +00:00
|
|
|
// Actually restart by sleeping and also watching for destroy events
|
|
|
|
select {
|
2022-02-02 16:59:53 +00:00
|
|
|
case <-timer.C:
|
2018-10-18 20:39:02 +00:00
|
|
|
case <-tr.killCtx.Done():
|
2018-09-20 22:44:27 +00:00
|
|
|
tr.logger.Trace("task killed between restarts", "delay", restartDelay)
|
|
|
|
break MAIN
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
2018-10-18 20:39:02 +00:00
|
|
|
// TaskRunner was told to exit immediately
|
2019-02-21 23:37:22 +00:00
|
|
|
tr.logger.Trace("gracefully shutting down during restart delay")
|
2018-10-18 20:39:02 +00:00
|
|
|
return
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
2018-09-20 22:44:27 +00:00
|
|
|
}
|
|
|
|
|
2019-02-21 23:37:22 +00:00
|
|
|
// Ensure handle is cleaned up. Restore could have recovered a task
|
|
|
|
// that should be terminal, so if the handle still exists we should
|
|
|
|
// kill it here.
|
|
|
|
if tr.getDriverHandle() != nil {
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
if result = tr.handleKill(nil); result != nil {
|
2019-03-05 23:12:02 +00:00
|
|
|
tr.emitExitResultEvent(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
tr.clearDriverHandle()
|
|
|
|
|
|
|
|
if err := tr.exited(); err != nil {
|
|
|
|
tr.logger.Error("exited hooks failed while cleaning up terminal task", "error", err)
|
|
|
|
}
|
2019-02-21 23:37:22 +00:00
|
|
|
}
|
|
|
|
|
2018-12-05 22:27:07 +00:00
|
|
|
// Mark the task as dead
|
|
|
|
tr.UpdateState(structs.TaskStateDead, nil)
|
2018-06-22 00:35:07 +00:00
|
|
|
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
// Wait here in case the allocation is restarted. Poststop tasks will never
|
|
|
|
// run again so skip them to avoid blocking forever.
|
|
|
|
if !tr.Task().IsPoststop() {
|
|
|
|
ALLOC_RESTART:
|
|
|
|
// Run in a loop to handle cases where restartCh is triggered but the
|
|
|
|
// task runner doesn't need to restart.
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-tr.killCtx.Done():
|
|
|
|
break ALLOC_RESTART
|
|
|
|
case <-tr.shutdownCtx.Done():
|
|
|
|
return
|
|
|
|
case <-tr.restartCh:
|
|
|
|
// Restart without delay since the task is not running anymore.
|
|
|
|
restart, _ := tr.shouldRestart()
|
|
|
|
if restart {
|
|
|
|
// Set runner as not dead to allow the MAIN loop to run.
|
|
|
|
dead = false
|
|
|
|
goto MAIN
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tr.stateLock.Lock()
|
|
|
|
tr.localState.RunComplete = true
|
|
|
|
err := tr.stateDB.PutTaskRunnerLocalState(tr.allocID, tr.taskName, tr.localState)
|
|
|
|
if err != nil {
|
|
|
|
tr.logger.Warn("error persisting task state on run loop exit", "error", err)
|
|
|
|
}
|
|
|
|
tr.stateLock.Unlock()
|
|
|
|
|
2018-07-17 00:19:56 +00:00
|
|
|
// Run the stop hooks
|
|
|
|
if err := tr.stop(); err != nil {
|
|
|
|
tr.logger.Error("stop failed", "error", err)
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
2018-06-29 23:22:18 +00:00
|
|
|
|
|
|
|
tr.logger.Debug("task run loop exiting")
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2020-11-12 16:01:42 +00:00
|
|
|
func (tr *TaskRunner) shouldShutdown() bool {
|
2020-12-17 23:21:46 +00:00
|
|
|
alloc := tr.Alloc()
|
|
|
|
if alloc.ClientTerminalStatus() {
|
2020-11-12 16:01:42 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-12-17 23:21:46 +00:00
|
|
|
if !tr.IsPoststopTask() && alloc.ServerTerminalStatus() {
|
2020-11-12 16:01:42 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-01-03 23:24:01 +00:00
|
|
|
// handleTaskExitResult handles the results returned by the task exiting. If
|
|
|
|
// retryWait is true, the caller should attempt to wait on the task again since
|
|
|
|
// it has not actually finished running. This can happen if the driver plugin
|
|
|
|
// has exited.
|
2018-12-21 19:23:21 +00:00
|
|
|
func (tr *TaskRunner) handleTaskExitResult(result *drivers.ExitResult) (retryWait bool) {
|
2018-12-05 22:27:07 +00:00
|
|
|
if result == nil {
|
2018-12-21 19:23:21 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if result.Err == bstructs.ErrPluginShutdown {
|
2019-01-07 22:47:49 +00:00
|
|
|
dn := tr.Task().Driver
|
|
|
|
tr.logger.Debug("driver plugin has shutdown; attempting to recover task", "driver", dn)
|
2018-12-21 19:23:21 +00:00
|
|
|
|
|
|
|
// Initialize a new driver handle
|
|
|
|
if err := tr.initDriver(); err != nil {
|
2019-01-07 22:47:49 +00:00
|
|
|
tr.logger.Error("failed to initialize driver after it exited unexpectedly", "error", err, "driver", dn)
|
2018-12-21 19:23:21 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to restore the handle
|
|
|
|
tr.stateLock.RLock()
|
|
|
|
h := tr.localState.TaskHandle
|
|
|
|
net := tr.localState.DriverNetwork
|
|
|
|
tr.stateLock.RUnlock()
|
|
|
|
if !tr.restoreHandle(h, net) {
|
2019-01-07 22:47:49 +00:00
|
|
|
tr.logger.Error("failed to restore handle on driver after it exited unexpectedly", "driver", dn)
|
2018-12-21 19:23:21 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-01-07 22:47:49 +00:00
|
|
|
tr.logger.Debug("task successfully recovered on driver", "driver", dn)
|
2018-12-21 19:23:21 +00:00
|
|
|
return true
|
2018-12-05 22:27:07 +00:00
|
|
|
}
|
|
|
|
|
2019-03-05 23:12:02 +00:00
|
|
|
// Emit Terminated event
|
|
|
|
tr.emitExitResultEvent(result)
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// emitExitResultEvent emits a TaskTerminated event for an ExitResult.
|
|
|
|
func (tr *TaskRunner) emitExitResultEvent(result *drivers.ExitResult) {
|
2018-11-16 19:17:23 +00:00
|
|
|
event := structs.NewTaskEvent(structs.TaskTerminated).
|
|
|
|
SetExitCode(result.ExitCode).
|
|
|
|
SetSignal(result.Signal).
|
|
|
|
SetOOMKilled(result.OOMKilled).
|
|
|
|
SetExitMessage(result.Err)
|
|
|
|
|
|
|
|
tr.EmitEvent(event)
|
|
|
|
|
2020-10-13 19:56:54 +00:00
|
|
|
if result.OOMKilled {
|
2018-11-16 19:17:23 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "oom_killed"}, 1, tr.baseLabels)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-01 18:03:52 +00:00
|
|
|
// handleUpdates runs update hooks when triggerUpdateCh is ticked and exits
|
|
|
|
// when Run has returned. Should only be run in a goroutine from Run.
|
|
|
|
func (tr *TaskRunner) handleUpdates() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-tr.triggerUpdateCh:
|
|
|
|
case <-tr.waitCh:
|
|
|
|
return
|
|
|
|
}
|
2018-08-23 19:03:17 +00:00
|
|
|
|
|
|
|
// Non-terminal update; run hooks
|
|
|
|
tr.updateHooks()
|
2018-08-01 18:03:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-20 22:44:27 +00:00
|
|
|
// shouldRestart determines whether the task should be restarted and updates
|
|
|
|
// the task state unless the task is killed or terminated.
|
2018-06-22 00:35:07 +00:00
|
|
|
func (tr *TaskRunner) shouldRestart() (bool, time.Duration) {
|
|
|
|
// Determine if we should restart
|
|
|
|
state, when := tr.restartTracker.GetState()
|
|
|
|
reason := tr.restartTracker.GetReason()
|
|
|
|
switch state {
|
2018-07-16 21:37:27 +00:00
|
|
|
case structs.TaskKilled:
|
2018-09-20 22:44:27 +00:00
|
|
|
// Never restart an explicitly killed task. Kill method handles
|
|
|
|
// updating the server.
|
2019-01-04 23:19:57 +00:00
|
|
|
tr.EmitEvent(structs.NewTaskEvent(state))
|
2018-07-16 21:37:27 +00:00
|
|
|
return false, 0
|
2018-06-22 00:35:07 +00:00
|
|
|
case structs.TaskNotRestarting, structs.TaskTerminated:
|
|
|
|
tr.logger.Info("not restarting task", "reason", reason)
|
|
|
|
if state == structs.TaskNotRestarting {
|
2018-09-11 00:34:45 +00:00
|
|
|
tr.UpdateState(structs.TaskStateDead, structs.NewTaskEvent(structs.TaskNotRestarting).SetRestartReason(reason).SetFailsTask())
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
return false, 0
|
|
|
|
case structs.TaskRestarting:
|
|
|
|
tr.logger.Info("restarting task", "reason", reason, "delay", when)
|
2018-09-11 00:34:45 +00:00
|
|
|
tr.UpdateState(structs.TaskStatePending, structs.NewTaskEvent(structs.TaskRestarting).SetRestartDelay(when).SetRestartReason(reason))
|
2018-11-20 20:52:23 +00:00
|
|
|
return true, when
|
2018-06-22 00:35:07 +00:00
|
|
|
default:
|
|
|
|
tr.logger.Error("restart tracker returned unknown state", "state", state)
|
|
|
|
return true, when
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// runDriver runs the driver and waits for it to exit
|
2019-07-19 17:04:39 +00:00
|
|
|
// runDriver emits an appropriate task event on success/failure
|
2018-06-29 23:22:18 +00:00
|
|
|
func (tr *TaskRunner) runDriver() error {
|
2018-10-06 01:42:15 +00:00
|
|
|
|
2018-10-11 00:08:57 +00:00
|
|
|
taskConfig := tr.buildTaskConfig()
|
2021-04-08 05:04:47 +00:00
|
|
|
if tr.cpusetCgroupPathGetter != nil {
|
cgroups: make sure cgroup still exists after task restart
This PR modifies raw_exec and exec to ensure the cgroup for a task
they are driving still exists during a task restart. These drivers
have the same bug but with different root cause.
For raw_exec, we were removing the cgroup in 2 places - the cpuset
manager, and in the unix containment implementation (the thing that
uses freezer cgroup to clean house). During a task restart, the
containment would remove the cgroup, and when the task runner hooks
went to start again would block on waiting for the cgroup to exist,
which will never happen, because it gets created by the cpuset manager
which only runs as an alloc pre-start hook. The fix here is to simply
not delete the cgroup in the containment implementation; killing the
PIDs is enough. The removal happens in the cpuset manager later anyway.
For exec, it's the same idea, except DestroyTask is called on task
failure, which in turn calls into libcontainer, which in turn deletes
the cgroup. In this case we do not have control over the deletion of
the cgroup, so instead we hack the cgroup back into life after the
call to DestroyTask.
All of this only applies to cgroups v2.
2022-05-04 18:51:53 +00:00
|
|
|
tr.logger.Trace("waiting for cgroup to exist for", "allocID", tr.allocID, "task", tr.task)
|
2021-04-08 05:04:47 +00:00
|
|
|
cpusetCgroupPath, err := tr.cpusetCgroupPathGetter(tr.killCtx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
taskConfig.Resources.LinuxResources.CpusetCgroupPath = cpusetCgroupPath
|
|
|
|
}
|
2018-10-06 01:42:15 +00:00
|
|
|
|
2018-11-12 18:13:25 +00:00
|
|
|
// Build hcl context variables
|
|
|
|
vars, errs, err := tr.envBuilder.Build().AllValues()
|
|
|
|
if err != nil {
|
2018-11-16 00:07:56 +00:00
|
|
|
return fmt.Errorf("error building environment variables: %v", err)
|
2018-11-12 18:13:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handle per-key errors
|
|
|
|
if len(errs) > 0 {
|
|
|
|
keys := make([]string, 0, len(errs))
|
|
|
|
for k, err := range errs {
|
|
|
|
keys = append(keys, k)
|
|
|
|
|
|
|
|
if tr.logger.IsTrace() {
|
|
|
|
// Verbosely log every diagnostic for debugging
|
|
|
|
tr.logger.Trace("error building environment variables", "key", k, "error", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tr.logger.Warn("some environment variables not available for rendering", "keys", strings.Join(keys, ", "))
|
|
|
|
}
|
|
|
|
|
2019-07-19 17:04:39 +00:00
|
|
|
val, diag, diagErrs := hclutils.ParseHclInterface(tr.task.Config, tr.taskSchema, vars)
|
2018-10-06 01:42:15 +00:00
|
|
|
if diag.HasErrors() {
|
2019-07-19 17:04:39 +00:00
|
|
|
parseErr := multierror.Append(errors.New("failed to parse config: "), diagErrs...)
|
|
|
|
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(parseErr))
|
|
|
|
return parseErr
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-10-06 01:42:15 +00:00
|
|
|
if err := taskConfig.EncodeDriverConfig(val); err != nil {
|
2019-07-19 17:04:39 +00:00
|
|
|
encodeErr := fmt.Errorf("failed to encode driver config: %v", err)
|
|
|
|
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(encodeErr))
|
|
|
|
return encodeErr
|
2018-10-06 01:42:15 +00:00
|
|
|
}
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2018-11-05 20:20:45 +00:00
|
|
|
// If there's already a task handle (eg from a Restore) there's nothing
|
|
|
|
// to do except update state.
|
|
|
|
if tr.getDriverHandle() != nil {
|
2018-10-18 20:39:02 +00:00
|
|
|
// Ensure running state is persisted but do *not* append a new
|
|
|
|
// task event as restoring is a client event and not relevant
|
|
|
|
// to a task's lifecycle.
|
|
|
|
if err := tr.updateStateImpl(structs.TaskStateRunning); err != nil {
|
2018-11-05 20:20:45 +00:00
|
|
|
//TODO return error and destroy task to avoid an orphaned task?
|
2018-10-18 20:39:02 +00:00
|
|
|
tr.logger.Warn("error persisting task state", "error", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the job if there's no existing handle (or if RecoverTask failed)
|
2018-11-05 20:20:45 +00:00
|
|
|
handle, net, err := tr.driver.StartTask(taskConfig)
|
2018-06-22 00:35:07 +00:00
|
|
|
if err != nil {
|
2018-12-21 19:23:21 +00:00
|
|
|
// The plugin has died, try relaunching it
|
|
|
|
if err == bstructs.ErrPluginShutdown {
|
|
|
|
tr.logger.Info("failed to start task because plugin shutdown unexpectedly; attempting to recover")
|
|
|
|
if err := tr.initDriver(); err != nil {
|
2019-07-19 17:04:39 +00:00
|
|
|
taskErr := fmt.Errorf("failed to initialize driver after it exited unexpectedly: %v", err)
|
|
|
|
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
|
|
|
|
return taskErr
|
2018-12-21 19:23:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handle, net, err = tr.driver.StartTask(taskConfig)
|
|
|
|
if err != nil {
|
2019-07-19 17:04:39 +00:00
|
|
|
taskErr := fmt.Errorf("failed to start task after driver exited unexpectedly: %v", err)
|
|
|
|
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
|
|
|
|
return taskErr
|
2018-12-21 19:23:21 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-07-19 17:04:39 +00:00
|
|
|
// Do *NOT* wrap the error here without maintaining whether or not is Recoverable.
|
|
|
|
// You must emit a task event failure to be considered Recoverable
|
|
|
|
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(err))
|
2019-02-13 23:34:17 +00:00
|
|
|
return err
|
2018-12-21 19:23:21 +00:00
|
|
|
}
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-11-14 18:29:07 +00:00
|
|
|
tr.stateLock.Lock()
|
2018-10-06 01:42:15 +00:00
|
|
|
tr.localState.TaskHandle = handle
|
2018-10-18 20:39:02 +00:00
|
|
|
tr.localState.DriverNetwork = net
|
|
|
|
if err := tr.stateDB.PutTaskRunnerLocalState(tr.allocID, tr.taskName, tr.localState); err != nil {
|
|
|
|
//TODO Nomad will be unable to restore this task; try to kill
|
|
|
|
// it now and fail? In general we prefer to leave running
|
|
|
|
// tasks running even if the agent encounters an error.
|
|
|
|
tr.logger.Warn("error persisting local task state; may be unable to restore after a Nomad restart",
|
|
|
|
"error", err, "task_id", handle.Config.ID)
|
|
|
|
}
|
2018-11-14 18:29:07 +00:00
|
|
|
tr.stateLock.Unlock()
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2022-07-06 19:44:58 +00:00
|
|
|
tr.setDriverHandle(NewDriverHandle(tr.driver, taskConfig.ID, tr.Task(), tr.clientConfig.MaxKillTimeout, net))
|
2018-10-18 20:39:02 +00:00
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// Emit an event that we started
|
2018-09-11 00:34:45 +00:00
|
|
|
tr.UpdateState(structs.TaskStateRunning, structs.NewTaskEvent(structs.TaskStarted))
|
2018-06-29 23:22:18 +00:00
|
|
|
return nil
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-10-06 01:42:15 +00:00
|
|
|
// initDriver retrives the DriverPlugin from the plugin loader for this task
|
|
|
|
func (tr *TaskRunner) initDriver() error {
|
2018-11-28 03:42:22 +00:00
|
|
|
driver, err := tr.driverManager.Dispense(tr.Task().Driver)
|
2018-10-06 01:42:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tr.driver = driver
|
|
|
|
|
|
|
|
schema, err := tr.driver.TaskConfigSchema()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-01-15 19:12:26 +00:00
|
|
|
spec, diag := hclspecutils.Convert(schema)
|
2018-10-11 00:08:57 +00:00
|
|
|
if diag.HasErrors() {
|
|
|
|
return multierror.Append(errors.New("failed to convert task schema"), diag.Errs()...)
|
|
|
|
}
|
2018-10-06 01:42:15 +00:00
|
|
|
tr.taskSchema = spec
|
|
|
|
|
|
|
|
caps, err := tr.driver.Capabilities()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tr.driverCapabilities = caps
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-17 17:32:32 +00:00
|
|
|
// handleKill is used to handle the a request to kill a task. It will return
|
2019-02-21 23:37:22 +00:00
|
|
|
// the handle exit result if one is available and store any error in the task
|
|
|
|
// runner killErr value.
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
func (tr *TaskRunner) handleKill(resultCh <-chan *drivers.ExitResult) *drivers.ExitResult {
|
2019-01-17 21:27:15 +00:00
|
|
|
// Run the pre killing hooks
|
|
|
|
tr.preKill()
|
2019-01-09 00:42:26 +00:00
|
|
|
|
2020-04-08 14:24:19 +00:00
|
|
|
// Wait for task ShutdownDelay after running prekill hooks
|
|
|
|
// This allows for things like service de-registration to run
|
|
|
|
// before waiting to kill task
|
|
|
|
if delay := tr.Task().ShutdownDelay; delay != 0 {
|
2023-03-03 23:22:06 +00:00
|
|
|
var ev *structs.TaskEvent
|
|
|
|
if tr.alloc.DesiredTransition.ShouldIgnoreShutdownDelay() {
|
|
|
|
tr.logger.Debug("skipping shutdown_delay", "shutdown_delay", delay)
|
|
|
|
ev = structs.NewTaskEvent(structs.TaskSkippingShutdownDelay).
|
|
|
|
SetDisplayMessage(fmt.Sprintf("Skipping shutdown_delay of %s before killing the task.", delay))
|
|
|
|
} else {
|
|
|
|
tr.logger.Debug("waiting before killing task", "shutdown_delay", delay)
|
|
|
|
ev = structs.NewTaskEvent(structs.TaskWaitingShuttingDownDelay).
|
|
|
|
SetDisplayMessage(fmt.Sprintf("Waiting for shutdown_delay of %s before killing the task.", delay))
|
|
|
|
}
|
2022-10-06 20:33:28 +00:00
|
|
|
tr.UpdateState(structs.TaskStatePending, ev)
|
|
|
|
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
select {
|
|
|
|
case result := <-resultCh:
|
|
|
|
return result
|
2021-12-13 19:54:53 +00:00
|
|
|
case <-tr.shutdownDelayCtx.Done():
|
|
|
|
break
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
case <-time.After(delay):
|
|
|
|
}
|
2020-04-08 14:24:19 +00:00
|
|
|
}
|
|
|
|
|
2019-01-09 00:42:26 +00:00
|
|
|
// Tell the restart tracker that the task has been killed so it doesn't
|
|
|
|
// attempt to restart it.
|
|
|
|
tr.restartTracker.SetKilled()
|
|
|
|
|
|
|
|
// Check it is running
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
select {
|
|
|
|
case result := <-resultCh:
|
|
|
|
return result
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2019-01-09 00:42:26 +00:00
|
|
|
handle := tr.getDriverHandle()
|
|
|
|
if handle == nil {
|
2019-01-17 17:32:32 +00:00
|
|
|
return nil
|
2019-01-09 00:42:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Kill the task using an exponential backoff in-case of failures.
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
result, killErr := tr.killTask(handle, resultCh)
|
2019-01-09 00:42:26 +00:00
|
|
|
if killErr != nil {
|
|
|
|
// We couldn't successfully destroy the resource created.
|
|
|
|
tr.logger.Error("failed to kill task. Resources may have been leaked", "error", killErr)
|
|
|
|
tr.setKillErr(killErr)
|
|
|
|
}
|
|
|
|
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
if result != nil {
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2019-01-09 00:42:26 +00:00
|
|
|
// Block until task has exited.
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
if resultCh == nil {
|
|
|
|
var err error
|
|
|
|
resultCh, err = handle.WaitCh(tr.shutdownCtx)
|
2019-01-09 00:42:26 +00:00
|
|
|
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
// The error should be nil or TaskNotFound, if it's something else then a
|
|
|
|
// failure in the driver or transport layer occurred
|
|
|
|
if err != nil {
|
|
|
|
if err == drivers.ErrTaskNotFound {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
tr.logger.Error("failed to wait on task. Resources may have been leaked", "error", err)
|
|
|
|
tr.setKillErr(killErr)
|
2019-01-17 17:32:32 +00:00
|
|
|
return nil
|
2019-01-09 00:42:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
case result := <-resultCh:
|
2019-01-17 17:32:32 +00:00
|
|
|
return result
|
2019-01-09 00:42:26 +00:00
|
|
|
case <-tr.shutdownCtx.Done():
|
2019-01-17 17:32:32 +00:00
|
|
|
return nil
|
2019-01-09 00:42:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-05 20:20:45 +00:00
|
|
|
// killTask kills the task handle. In the case that killing fails,
|
|
|
|
// killTask will retry with an exponential backoff and will give up at a
|
|
|
|
// given limit. Returns an error if the task could not be killed.
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
func (tr *TaskRunner) killTask(handle *DriverHandle, resultCh <-chan *drivers.ExitResult) (*drivers.ExitResult, error) {
|
2018-07-16 21:37:27 +00:00
|
|
|
// Cap the number of times we attempt to kill the task.
|
2018-11-05 20:20:45 +00:00
|
|
|
var err error
|
2018-07-16 21:37:27 +00:00
|
|
|
for i := 0; i < killFailureLimit; i++ {
|
|
|
|
if err = handle.Kill(); err != nil {
|
2018-10-06 01:42:15 +00:00
|
|
|
if err == drivers.ErrTaskNotFound {
|
|
|
|
tr.logger.Warn("couldn't find task to kill", "task_id", handle.ID())
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
return nil, nil
|
2018-10-06 01:42:15 +00:00
|
|
|
}
|
2018-07-16 21:37:27 +00:00
|
|
|
// Calculate the new backoff
|
|
|
|
backoff := (1 << (2 * uint64(i))) * killBackoffBaseline
|
|
|
|
if backoff > killBackoffLimit {
|
|
|
|
backoff = killBackoffLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
tr.logger.Error("failed to kill task", "backoff", backoff, "error", err)
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
select {
|
|
|
|
case result := <-resultCh:
|
|
|
|
return result, nil
|
|
|
|
case <-time.After(backoff):
|
|
|
|
}
|
2018-07-16 21:37:27 +00:00
|
|
|
} else {
|
|
|
|
// Kill was successful
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
return nil, nil
|
2018-07-16 21:37:27 +00:00
|
|
|
}
|
|
|
|
}
|
drivers: Capture exit code when task is killed (#10494)
This commit ensures Nomad captures the task code more reliably even when the task is killed. This issue affect to `raw_exec` driver, as noted in https://github.com/hashicorp/nomad/issues/10430 .
We fix this issue by ensuring that the TaskRunner only calls `driver.WaitTask` once. The TaskRunner monitors the completion of the task by calling `driver.WaitTask` which should return the task exit code on completion. However, it also could return a "context canceled" error if the agent/executor is shutdown.
Previously, when a task is to be stopped, the killTask path makes two WaitTask calls, and the second returns "context canceled" occasionally because of a "race" in task shutting down and depending on driver, and how fast it shuts down after task completes.
By having a single WaitTask call and consistently waiting for the task, we ensure we capture the exit code reliably before the executor is shutdown or the contexts expired.
I opted to change the TaskRunner implementation to avoid changing the driver interface or requiring 3rd party drivers to update.
Additionally, the PR ensures that attempts to kill the task terminate when the task "naturally" dies. Without this change, if the task dies at the right moment, the `killTask` call may retry to kill an already-dead task for up to 5 minutes before giving up.
2021-05-04 14:54:00 +00:00
|
|
|
return nil, err
|
2018-07-16 21:37:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-11 04:22:04 +00:00
|
|
|
// persistLocalState persists local state to disk synchronously.
|
|
|
|
func (tr *TaskRunner) persistLocalState() error {
|
2018-11-14 18:29:07 +00:00
|
|
|
tr.stateLock.RLock()
|
|
|
|
defer tr.stateLock.RUnlock()
|
2018-08-08 00:46:37 +00:00
|
|
|
|
2018-08-09 00:06:56 +00:00
|
|
|
return tr.stateDB.PutTaskRunnerLocalState(tr.allocID, tr.taskName, tr.localState)
|
2018-07-11 04:22:04 +00:00
|
|
|
}
|
|
|
|
|
2018-10-11 00:08:57 +00:00
|
|
|
// buildTaskConfig builds a drivers.TaskConfig with an unique ID for the task.
|
2018-12-18 22:11:25 +00:00
|
|
|
// The ID is unique for every invocation, it is built from the alloc ID, task
|
|
|
|
// name and 8 random characters.
|
2018-10-11 00:08:57 +00:00
|
|
|
func (tr *TaskRunner) buildTaskConfig() *drivers.TaskConfig {
|
2018-11-20 18:34:46 +00:00
|
|
|
task := tr.Task()
|
|
|
|
alloc := tr.Alloc()
|
2018-12-18 22:11:25 +00:00
|
|
|
invocationid := uuid.Generate()[:8]
|
2018-12-14 00:21:41 +00:00
|
|
|
taskResources := tr.taskResources
|
2020-08-11 22:30:22 +00:00
|
|
|
ports := tr.Alloc().AllocatedResources.Shared.Ports
|
2018-12-19 22:23:09 +00:00
|
|
|
env := tr.envBuilder.Build()
|
2019-04-29 17:35:15 +00:00
|
|
|
tr.networkIsolationLock.Lock()
|
|
|
|
defer tr.networkIsolationLock.Unlock()
|
2018-12-13 23:06:48 +00:00
|
|
|
|
2020-04-28 03:11:06 +00:00
|
|
|
var dns *drivers.DNSConfig
|
|
|
|
if alloc.AllocatedResources != nil && len(alloc.AllocatedResources.Shared.Networks) > 0 {
|
|
|
|
allocDNS := alloc.AllocatedResources.Shared.Networks[0].DNS
|
|
|
|
if allocDNS != nil {
|
2022-04-29 14:02:20 +00:00
|
|
|
interpolatedNetworks := taskenv.InterpolateNetworks(env, alloc.AllocatedResources.Shared.Networks)
|
2020-04-28 03:11:06 +00:00
|
|
|
dns = &drivers.DNSConfig{
|
2022-04-28 21:09:53 +00:00
|
|
|
Servers: interpolatedNetworks[0].DNS.Servers,
|
|
|
|
Searches: interpolatedNetworks[0].DNS.Searches,
|
|
|
|
Options: interpolatedNetworks[0].DNS.Options,
|
2020-04-28 03:11:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-26 20:13:05 +00:00
|
|
|
memoryLimit := taskResources.Memory.MemoryMB
|
|
|
|
if max := taskResources.Memory.MemoryMaxMB; max > memoryLimit {
|
|
|
|
memoryLimit = max
|
|
|
|
}
|
|
|
|
|
2021-04-15 17:31:51 +00:00
|
|
|
cpusetCpus := make([]string, len(taskResources.Cpu.ReservedCores))
|
|
|
|
for i, v := range taskResources.Cpu.ReservedCores {
|
|
|
|
cpusetCpus[i] = fmt.Sprintf("%d", v)
|
|
|
|
}
|
|
|
|
|
2018-10-11 00:08:57 +00:00
|
|
|
return &drivers.TaskConfig{
|
2019-01-18 20:25:11 +00:00
|
|
|
ID: fmt.Sprintf("%s/%s/%s", alloc.ID, task.Name, invocationid),
|
|
|
|
Name: task.Name,
|
|
|
|
JobName: alloc.Job.Name,
|
2021-03-08 13:59:52 +00:00
|
|
|
JobID: alloc.Job.ID,
|
2019-01-18 20:25:11 +00:00
|
|
|
TaskGroupName: alloc.TaskGroup,
|
2021-03-08 13:59:52 +00:00
|
|
|
Namespace: alloc.Namespace,
|
|
|
|
NodeName: alloc.NodeName,
|
|
|
|
NodeID: alloc.NodeID,
|
2023-07-10 15:27:45 +00:00
|
|
|
ParentJobID: alloc.Job.ParentID,
|
2018-10-11 00:08:57 +00:00
|
|
|
Resources: &drivers.Resources{
|
2018-12-14 00:21:41 +00:00
|
|
|
NomadResources: taskResources,
|
2018-11-06 05:39:48 +00:00
|
|
|
LinuxResources: &drivers.LinuxResources{
|
2021-03-26 20:13:05 +00:00
|
|
|
MemoryLimitBytes: memoryLimit * 1024 * 1024,
|
2018-12-18 23:10:13 +00:00
|
|
|
CPUShares: taskResources.Cpu.CpuShares,
|
2021-04-15 17:31:51 +00:00
|
|
|
CpusetCpus: strings.Join(cpusetCpus, ","),
|
2018-12-18 22:47:54 +00:00
|
|
|
PercentTicks: float64(taskResources.Cpu.CpuShares) / float64(tr.clientConfig.Node.NodeResources.Cpu.CpuShares),
|
2018-11-06 05:39:48 +00:00
|
|
|
},
|
2020-08-11 22:30:22 +00:00
|
|
|
Ports: &ports,
|
2018-10-11 00:08:57 +00:00
|
|
|
},
|
2019-04-29 17:35:15 +00:00
|
|
|
Devices: tr.hookResources.getDevices(),
|
|
|
|
Mounts: tr.hookResources.getMounts(),
|
|
|
|
Env: env.Map(),
|
|
|
|
DeviceEnv: env.DeviceEnv(),
|
|
|
|
User: task.User,
|
|
|
|
AllocDir: tr.taskDir.AllocDir,
|
|
|
|
StdoutPath: tr.logmonHookConfig.stdoutFifo,
|
|
|
|
StderrPath: tr.logmonHookConfig.stderrFifo,
|
|
|
|
AllocID: tr.allocID,
|
|
|
|
NetworkIsolation: tr.networkIsolationSpec,
|
2020-04-28 03:11:06 +00:00
|
|
|
DNS: dns,
|
2018-10-11 00:08:57 +00:00
|
|
|
}
|
2018-10-06 01:42:15 +00:00
|
|
|
}
|
|
|
|
|
2018-07-13 00:56:52 +00:00
|
|
|
// Restore task runner state. Called by AllocRunner.Restore after NewTaskRunner
|
2018-07-16 23:32:37 +00:00
|
|
|
// but before Run so no locks need to be acquired.
|
2018-08-08 00:46:37 +00:00
|
|
|
func (tr *TaskRunner) Restore() error {
|
|
|
|
ls, ts, err := tr.stateDB.GetTaskRunnerState(tr.allocID, tr.taskName)
|
2018-07-13 00:56:52 +00:00
|
|
|
if err != nil {
|
2018-08-08 00:46:37 +00:00
|
|
|
return err
|
2018-07-16 23:32:37 +00:00
|
|
|
}
|
2018-07-18 18:43:08 +00:00
|
|
|
|
2018-10-16 22:17:36 +00:00
|
|
|
if ls != nil {
|
|
|
|
ls.Canonicalize()
|
|
|
|
tr.localState = ls
|
|
|
|
}
|
2018-11-05 20:20:45 +00:00
|
|
|
|
2018-10-16 22:17:36 +00:00
|
|
|
if ts != nil {
|
|
|
|
ts.Canonicalize()
|
|
|
|
tr.state = ts
|
|
|
|
}
|
2018-11-05 20:20:45 +00:00
|
|
|
|
|
|
|
// If a TaskHandle was persisted, ensure it is valid or destroy it.
|
|
|
|
if taskHandle := tr.localState.TaskHandle; taskHandle != nil {
|
|
|
|
//TODO if RecoverTask returned the DriverNetwork we wouldn't
|
|
|
|
// have to persist it at all!
|
2019-05-08 06:04:40 +00:00
|
|
|
restored := tr.restoreHandle(taskHandle, tr.localState.DriverNetwork)
|
2019-05-10 15:51:06 +00:00
|
|
|
|
|
|
|
// If the handle could not be restored, the alloc is
|
|
|
|
// non-terminal, and the task isn't a system job: wait until
|
|
|
|
// servers have been contacted before running. #1795
|
|
|
|
if restored {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
alloc := tr.Alloc()
|
2019-06-26 15:39:10 +00:00
|
|
|
if tr.state.State == structs.TaskStateDead || alloc.TerminalStatus() || alloc.Job.Type == structs.JobTypeSystem {
|
2019-05-10 15:51:06 +00:00
|
|
|
return nil
|
2019-05-08 06:04:40 +00:00
|
|
|
}
|
2019-05-10 15:51:06 +00:00
|
|
|
|
|
|
|
tr.logger.Trace("failed to reattach to task; will not run until server is contacted")
|
|
|
|
tr.waitOnServers = true
|
|
|
|
|
|
|
|
ev := structs.NewTaskEvent(structs.TaskRestoreFailed).
|
|
|
|
SetDisplayMessage("failed to restore task; will not run until server is contacted")
|
|
|
|
tr.UpdateState(structs.TaskStatePending, ev)
|
2018-11-05 20:20:45 +00:00
|
|
|
}
|
2019-05-10 15:51:06 +00:00
|
|
|
|
2018-07-13 00:56:52 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-11-05 20:20:45 +00:00
|
|
|
// restoreHandle ensures a TaskHandle is valid by calling Driver.RecoverTask
|
|
|
|
// and sets the driver handle. If the TaskHandle is not valid, DestroyTask is
|
|
|
|
// called.
|
2019-01-04 23:01:35 +00:00
|
|
|
func (tr *TaskRunner) restoreHandle(taskHandle *drivers.TaskHandle, net *drivers.DriverNetwork) (success bool) {
|
2018-11-05 20:20:45 +00:00
|
|
|
// Ensure handle is well-formed
|
|
|
|
if taskHandle.Config == nil {
|
2018-12-21 19:23:21 +00:00
|
|
|
return true
|
2018-11-05 20:20:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := tr.driver.RecoverTask(taskHandle); err != nil {
|
2018-11-14 18:29:07 +00:00
|
|
|
if tr.TaskState().State != structs.TaskStateRunning {
|
|
|
|
// RecoverTask should fail if the Task wasn't running
|
2018-12-21 19:23:21 +00:00
|
|
|
return true
|
2018-11-14 18:29:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
tr.logger.Error("error recovering task; cleaning up",
|
2018-11-05 20:20:45 +00:00
|
|
|
"error", err, "task_id", taskHandle.Config.ID)
|
|
|
|
|
|
|
|
// Try to cleanup any existing task state in the plugin before restarting
|
|
|
|
if err := tr.driver.DestroyTask(taskHandle.Config.ID, true); err != nil {
|
|
|
|
// Ignore ErrTaskNotFound errors as ideally
|
|
|
|
// this task has already been stopped and
|
|
|
|
// therefore doesn't exist.
|
|
|
|
if err != drivers.ErrTaskNotFound {
|
|
|
|
tr.logger.Warn("error destroying unrecoverable task",
|
|
|
|
"error", err, "task_id", taskHandle.Config.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-07-03 01:39:35 +00:00
|
|
|
return false
|
2018-11-05 20:20:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update driver handle on task runner
|
2022-07-06 19:44:58 +00:00
|
|
|
tr.setDriverHandle(NewDriverHandle(tr.driver, taskHandle.Config.ID, tr.Task(), tr.clientConfig.MaxKillTimeout, net))
|
2018-12-21 19:23:21 +00:00
|
|
|
return true
|
2018-11-05 20:20:45 +00:00
|
|
|
}
|
|
|
|
|
2018-09-11 00:34:45 +00:00
|
|
|
// UpdateState sets the task runners allocation state and triggers a server
|
2018-08-30 21:33:50 +00:00
|
|
|
// update.
|
2018-09-11 00:34:45 +00:00
|
|
|
func (tr *TaskRunner) UpdateState(state string, event *structs.TaskEvent) {
|
2018-10-18 20:39:02 +00:00
|
|
|
tr.stateLock.Lock()
|
|
|
|
defer tr.stateLock.Unlock()
|
|
|
|
|
Task lifecycle restart (#14127)
* allocrunner: handle lifecycle when all tasks die
When all tasks die the Coordinator must transition to its terminal
state, coordinatorStatePoststop, to unblock poststop tasks. Since this
could happen at any time (for example, a prestart task dies), all states
must be able to transition to this terminal state.
* allocrunner: implement different alloc restarts
Add a new alloc restart mode where all tasks are restarted, even if they
have already exited. Also unifies the alloc restart logic to use the
implementation that restarts tasks concurrently and ignores
ErrTaskNotRunning errors since those are expected when restarting the
allocation.
* allocrunner: allow tasks to run again
Prevent the task runner Run() method from exiting to allow a dead task
to run again. When the task runner is signaled to restart, the function
will jump back to the MAIN loop and run it again.
The task runner determines if a task needs to run again based on two new
task events that were added to differentiate between a request to
restart a specific task, the tasks that are currently running, or all
tasks that have already run.
* api/cli: add support for all tasks alloc restart
Implement the new -all-tasks alloc restart CLI flag and its API
counterpar, AllTasks. The client endpoint calls the appropriate restart
method from the allocrunner depending on the restart parameters used.
* test: fix tasklifecycle Coordinator test
* allocrunner: kill taskrunners if all tasks are dead
When all non-poststop tasks are dead we need to kill the taskrunners so
we don't leak their goroutines, which are blocked in the alloc restart
loop. This also ensures the allocrunner exits on its own.
* taskrunner: fix tests that waited on WaitCh
Now that "dead" tasks may run again, the taskrunner Run() method will
not return when the task finishes running, so tests must wait for the
task state to be "dead" instead of using the WaitCh, since it won't be
closed until the taskrunner is killed.
* tests: add tests for all tasks alloc restart
* changelog: add entry for #14127
* taskrunner: fix restore logic.
The first implementation of the task runner restore process relied on
server data (`tr.Alloc().TerminalStatus()`) which may not be available
to the client at the time of restore.
It also had the incorrect code path. When restoring a dead task the
driver handle always needs to be clear cleanly using `clearDriverHandle`
otherwise, after exiting the MAIN loop, the task may be killed by
`tr.handleKill`.
The fix is to store the state of the Run() loop in the task runner local
client state: if the task runner ever exits this loop cleanly (not with
a shutdown) it will never be able to run again. So if the Run() loops
starts with this local state flag set, it must exit early.
This local state flag is also being checked on task restart requests. If
the task is "dead" and its Run() loop is not active it will never be
able to run again.
* address code review requests
* apply more code review changes
* taskrunner: add different Restart modes
Using the task event to differentiate between the allocrunner restart
methods proved to be confusing for developers to understand how it all
worked.
So instead of relying on the event type, this commit separated the logic
of restarting an taskRunner into two methods:
- `Restart` will retain the current behaviour and only will only restart
the task if it's currently running.
- `ForceRestart` is the new method where a `dead` task is allowed to
restart if its `Run()` method is still active. Callers will need to
restart the allocRunner taskCoordinator to make sure it will allow the
task to run again.
* minor fixes
2022-08-24 21:43:07 +00:00
|
|
|
tr.logger.Trace("setting task state", "state", state)
|
|
|
|
|
2018-12-05 22:27:07 +00:00
|
|
|
if event != nil {
|
|
|
|
// Append the event
|
|
|
|
tr.appendEvent(event)
|
|
|
|
}
|
2018-10-18 20:39:02 +00:00
|
|
|
|
|
|
|
// Update the state
|
|
|
|
if err := tr.updateStateImpl(state); err != nil {
|
|
|
|
// Only log the error as we persistence errors should not
|
|
|
|
// affect task state.
|
|
|
|
tr.logger.Error("error persisting task state", "error", err, "event", event, "state", state)
|
|
|
|
}
|
2018-07-19 00:06:44 +00:00
|
|
|
|
2020-12-17 23:21:46 +00:00
|
|
|
// Store task handle for remote tasks
|
|
|
|
if tr.driverCapabilities != nil && tr.driverCapabilities.RemoteTasks {
|
|
|
|
tr.logger.Trace("storing remote task handle state")
|
|
|
|
tr.localState.TaskHandle.Store(tr.state)
|
|
|
|
}
|
|
|
|
|
2018-07-19 17:48:01 +00:00
|
|
|
// Notify the alloc runner of the transition
|
2018-10-16 03:38:12 +00:00
|
|
|
tr.stateUpdater.TaskStateUpdated()
|
2018-07-19 00:06:44 +00:00
|
|
|
}
|
|
|
|
|
2018-10-18 20:39:02 +00:00
|
|
|
// updateStateImpl updates the in-memory task state and persists to disk.
|
|
|
|
func (tr *TaskRunner) updateStateImpl(state string) error {
|
2018-06-22 00:35:07 +00:00
|
|
|
|
2018-07-19 00:06:44 +00:00
|
|
|
// Update the task state
|
2018-09-07 00:46:32 +00:00
|
|
|
oldState := tr.state.State
|
2018-07-19 00:06:44 +00:00
|
|
|
taskState := tr.state
|
|
|
|
taskState.State = state
|
|
|
|
|
2018-07-16 23:32:37 +00:00
|
|
|
// Handle the state transition.
|
2018-06-22 00:35:07 +00:00
|
|
|
switch state {
|
|
|
|
case structs.TaskStateRunning:
|
|
|
|
// Capture the start time if it is just starting
|
2018-09-07 00:46:32 +00:00
|
|
|
if oldState != structs.TaskStateRunning {
|
2018-07-16 23:32:37 +00:00
|
|
|
taskState.StartedAt = time.Now().UTC()
|
2020-10-13 19:56:54 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "running"}, 1, tr.baseLabels)
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
case structs.TaskStateDead:
|
|
|
|
// Capture the finished time if not already set
|
2018-07-16 23:32:37 +00:00
|
|
|
if taskState.FinishedAt.IsZero() {
|
|
|
|
taskState.FinishedAt = time.Now().UTC()
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Emitting metrics to indicate task complete and failures
|
2018-07-16 23:32:37 +00:00
|
|
|
if taskState.Failed {
|
2020-10-13 19:56:54 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "failed"}, 1, tr.baseLabels)
|
2018-06-22 00:35:07 +00:00
|
|
|
} else {
|
2020-10-13 19:56:54 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "complete"}, 1, tr.baseLabels)
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-16 23:32:37 +00:00
|
|
|
// Persist the state and event
|
2018-10-18 20:39:02 +00:00
|
|
|
return tr.stateDB.PutTaskState(tr.allocID, tr.taskName, taskState)
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
2018-07-16 23:32:37 +00:00
|
|
|
// EmitEvent appends a new TaskEvent to this task's TaskState. The actual
|
2018-09-11 00:34:45 +00:00
|
|
|
// TaskState.State (pending, running, dead) is not changed. Use UpdateState to
|
2018-07-16 23:32:37 +00:00
|
|
|
// transition states.
|
2018-09-11 00:34:45 +00:00
|
|
|
// Events are persisted locally and sent to the server, but errors are simply
|
|
|
|
// logged. Use AppendEvent to simply add a new event.
|
2018-07-16 21:37:27 +00:00
|
|
|
func (tr *TaskRunner) EmitEvent(event *structs.TaskEvent) {
|
2018-07-16 23:32:37 +00:00
|
|
|
tr.stateLock.Lock()
|
2018-10-16 03:38:12 +00:00
|
|
|
defer tr.stateLock.Unlock()
|
2018-07-16 23:32:37 +00:00
|
|
|
|
2018-09-11 00:34:45 +00:00
|
|
|
tr.appendEvent(event)
|
|
|
|
|
|
|
|
if err := tr.stateDB.PutTaskState(tr.allocID, tr.taskName, tr.state); err != nil {
|
|
|
|
// Only a warning because the next event/state-transition will
|
|
|
|
// try to persist it again.
|
|
|
|
tr.logger.Warn("error persisting event", "error", err, "event", event)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Notify the alloc runner of the event
|
2018-10-16 03:38:12 +00:00
|
|
|
tr.stateUpdater.TaskStateUpdated()
|
2018-09-11 00:34:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AppendEvent appends a new TaskEvent to this task's TaskState. The actual
|
|
|
|
// TaskState.State (pending, running, dead) is not changed. Use UpdateState to
|
|
|
|
// transition states.
|
|
|
|
// Events are persisted locally and errors are simply logged. Use EmitEvent
|
|
|
|
// also update AllocRunner.
|
|
|
|
func (tr *TaskRunner) AppendEvent(event *structs.TaskEvent) {
|
|
|
|
tr.stateLock.Lock()
|
|
|
|
defer tr.stateLock.Unlock()
|
|
|
|
|
|
|
|
tr.appendEvent(event)
|
2018-07-16 23:32:37 +00:00
|
|
|
|
2018-08-08 00:46:37 +00:00
|
|
|
if err := tr.stateDB.PutTaskState(tr.allocID, tr.taskName, tr.state); err != nil {
|
2018-07-16 23:32:37 +00:00
|
|
|
// Only a warning because the next event/state-transition will
|
|
|
|
// try to persist it again.
|
|
|
|
tr.logger.Warn("error persisting event", "error", err, "event", event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-11 00:34:45 +00:00
|
|
|
// appendEvent to task's event slice. Caller must acquire stateLock.
|
|
|
|
func (tr *TaskRunner) appendEvent(event *structs.TaskEvent) error {
|
2018-07-16 23:32:37 +00:00
|
|
|
// Ensure the event is populated with human readable strings
|
|
|
|
event.PopulateEventDisplayMessage()
|
|
|
|
|
2018-10-16 23:42:19 +00:00
|
|
|
// Propagate failure from event to task state
|
2018-07-16 23:32:37 +00:00
|
|
|
if event.FailsTask {
|
|
|
|
tr.state.Failed = true
|
|
|
|
}
|
|
|
|
|
2018-07-18 20:45:55 +00:00
|
|
|
// XXX This seems like a super awkward spot for this? Why not shouldRestart?
|
2018-07-16 23:32:37 +00:00
|
|
|
// Update restart metrics
|
|
|
|
if event.Type == structs.TaskRestarting {
|
2020-10-13 19:56:54 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "restart"}, 1, tr.baseLabels)
|
2018-07-16 23:32:37 +00:00
|
|
|
tr.state.Restarts++
|
|
|
|
tr.state.LastRestart = time.Unix(0, event.Time)
|
|
|
|
}
|
|
|
|
|
2023-01-20 22:35:00 +00:00
|
|
|
tr.logger.Info("Task event", "type", event.Type, "msg", event.DisplayMessage, "failed", event.FailsTask)
|
|
|
|
|
2018-07-16 23:32:37 +00:00
|
|
|
// Append event to slice
|
test: port TestTaskRunner_CheckWatcher_Restart
Added ability to adjust the number of events the TaskRunner keeps as
there's no way to observe all events otherwise.
Task events differ slightly from 0.8 because 0.9 emits Terminated every
time a task exits instead of only when it exits on its own (not due to
restart or kill).
0.9 does not emit Killing/Killed for restarts like 0.8 which seems fine
as `Restart Signaled/Terminated/Restarting` is more descriptive.
Original v0.8 events emitted:
```
expected := []string{
"Received",
"Task Setup",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Not Restarting",
}
```
2019-01-18 16:30:44 +00:00
|
|
|
appendTaskEvent(tr.state, event, tr.maxEvents)
|
2018-07-17 20:42:45 +00:00
|
|
|
|
|
|
|
return nil
|
2018-07-13 20:45:57 +00:00
|
|
|
}
|
|
|
|
|
2018-06-29 21:53:31 +00:00
|
|
|
// WaitCh is closed when TaskRunner.Run exits.
|
|
|
|
func (tr *TaskRunner) WaitCh() <-chan struct{} {
|
|
|
|
return tr.waitCh
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the running allocation with a new version received from the server.
|
2019-02-21 23:37:22 +00:00
|
|
|
// Calls Update hooks asynchronously with Run.
|
2018-06-29 21:53:31 +00:00
|
|
|
//
|
2019-02-21 23:37:22 +00:00
|
|
|
// This method is safe for calling concurrently with Run and does not modify
|
2018-06-29 21:53:31 +00:00
|
|
|
// the passed in allocation.
|
|
|
|
func (tr *TaskRunner) Update(update *structs.Allocation) {
|
2018-11-14 18:29:07 +00:00
|
|
|
task := update.LookupTask(tr.taskName)
|
|
|
|
if task == nil {
|
|
|
|
// This should not happen and likely indicates a bug in the
|
|
|
|
// server or client.
|
|
|
|
tr.logger.Error("allocation update is missing task; killing",
|
|
|
|
"group", update.TaskGroup)
|
|
|
|
te := structs.NewTaskEvent(structs.TaskKilled).
|
|
|
|
SetKillReason("update missing task").
|
|
|
|
SetFailsTask()
|
|
|
|
tr.Kill(context.Background(), te)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-01 18:03:52 +00:00
|
|
|
// Update tr.alloc
|
2018-11-14 18:29:07 +00:00
|
|
|
tr.setAlloc(update, task)
|
2018-08-01 18:03:52 +00:00
|
|
|
|
2018-10-16 03:38:12 +00:00
|
|
|
// Trigger update hooks if not terminal
|
|
|
|
if !update.TerminalStatus() {
|
|
|
|
tr.triggerUpdateHooks()
|
|
|
|
}
|
2018-08-01 18:03:52 +00:00
|
|
|
}
|
|
|
|
|
2019-04-29 17:35:15 +00:00
|
|
|
// SetNetworkIsolation is called by the PreRun allocation hook after configuring
|
|
|
|
// the network isolation for the allocation
|
|
|
|
func (tr *TaskRunner) SetNetworkIsolation(n *drivers.NetworkIsolationSpec) {
|
|
|
|
tr.networkIsolationLock.Lock()
|
|
|
|
tr.networkIsolationSpec = n
|
|
|
|
tr.networkIsolationLock.Unlock()
|
|
|
|
}
|
|
|
|
|
2018-08-01 18:03:52 +00:00
|
|
|
// triggerUpdate if there isn't already an update pending. Should be called
|
|
|
|
// instead of calling updateHooks directly to serialize runs of update hooks.
|
|
|
|
// TaskRunner state should be updated prior to triggering update hooks.
|
|
|
|
//
|
|
|
|
// Does not block.
|
|
|
|
func (tr *TaskRunner) triggerUpdateHooks() {
|
2018-06-29 21:53:31 +00:00
|
|
|
select {
|
2018-08-01 18:03:52 +00:00
|
|
|
case tr.triggerUpdateCh <- struct{}{}:
|
|
|
|
default:
|
|
|
|
// already an update hook pending
|
2018-06-29 21:53:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-14 18:29:07 +00:00
|
|
|
// Shutdown TaskRunner gracefully without affecting the state of the task.
|
|
|
|
// Shutdown blocks until the main Run loop exits.
|
|
|
|
func (tr *TaskRunner) Shutdown() {
|
|
|
|
tr.logger.Trace("shutting down")
|
2019-01-09 00:42:26 +00:00
|
|
|
tr.shutdownCtxCancel()
|
2018-11-14 18:29:07 +00:00
|
|
|
|
|
|
|
<-tr.WaitCh()
|
|
|
|
|
|
|
|
// Run shutdown hooks to cleanup
|
|
|
|
tr.shutdownHooks()
|
|
|
|
|
|
|
|
// Persist once more
|
|
|
|
tr.persistLocalState()
|
|
|
|
}
|
|
|
|
|
2018-09-15 00:08:26 +00:00
|
|
|
// LatestResourceUsage returns the last resource utilization datapoint
|
|
|
|
// collected. May return nil if the task is not running or no resource
|
|
|
|
// utilization has been collected yet.
|
|
|
|
func (tr *TaskRunner) LatestResourceUsage() *cstructs.TaskResourceUsage {
|
|
|
|
tr.resourceUsageLock.Lock()
|
|
|
|
ru := tr.resourceUsage
|
|
|
|
tr.resourceUsageLock.Unlock()
|
2018-11-15 15:13:14 +00:00
|
|
|
|
|
|
|
// Look up device statistics lazily when fetched, as currently we do not emit any stats for them yet
|
|
|
|
if ru != nil && tr.deviceStatsReporter != nil {
|
2019-01-17 21:52:51 +00:00
|
|
|
deviceResources := tr.taskResources.Devices
|
2018-11-15 15:13:14 +00:00
|
|
|
ru.ResourceUsage.DeviceStats = tr.deviceStatsReporter.LatestDeviceResourceStats(deviceResources)
|
|
|
|
}
|
2018-09-15 00:08:26 +00:00
|
|
|
return ru
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateStats updates and emits the latest stats from the driver.
|
|
|
|
func (tr *TaskRunner) UpdateStats(ru *cstructs.TaskResourceUsage) {
|
|
|
|
tr.resourceUsageLock.Lock()
|
|
|
|
tr.resourceUsage = ru
|
|
|
|
tr.resourceUsageLock.Unlock()
|
|
|
|
if ru != nil {
|
|
|
|
tr.emitStats(ru)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-16 14:06:30 +00:00
|
|
|
// TODO Remove Backwardscompat or use tr.Alloc()?
|
2018-09-15 00:08:26 +00:00
|
|
|
func (tr *TaskRunner) setGaugeForMemory(ru *cstructs.TaskResourceUsage) {
|
2019-03-29 18:10:11 +00:00
|
|
|
alloc := tr.Alloc()
|
|
|
|
var allocatedMem float32
|
2023-07-19 20:50:34 +00:00
|
|
|
var allocatedMemMax float32
|
2020-01-09 14:25:07 +00:00
|
|
|
if taskRes := alloc.AllocatedResources.Tasks[tr.taskName]; taskRes != nil {
|
|
|
|
// Convert to bytes to match other memory metrics
|
|
|
|
allocatedMem = float32(taskRes.Memory.MemoryMB) * 1024 * 1024
|
2023-07-19 20:50:34 +00:00
|
|
|
allocatedMemMax = float32(taskRes.Memory.MemoryMaxMB) * 1024 * 1024
|
2019-03-29 18:10:11 +00:00
|
|
|
}
|
|
|
|
|
2021-04-13 15:39:33 +00:00
|
|
|
ms := ru.ResourceUsage.MemoryStats
|
|
|
|
|
|
|
|
publishMetric := func(v uint64, reported, measured string) {
|
2022-09-21 19:53:25 +00:00
|
|
|
if v != 0 || slices.Contains(ms.Measured, measured) {
|
2021-04-13 15:39:33 +00:00
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "memory", reported},
|
|
|
|
float32(v), tr.baseLabels)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
publishMetric(ms.RSS, "rss", "RSS")
|
|
|
|
publishMetric(ms.Cache, "cache", "Cache")
|
|
|
|
publishMetric(ms.Swap, "swap", "Swap")
|
2022-01-10 20:35:19 +00:00
|
|
|
publishMetric(ms.MappedFile, "mapped_file", "Mapped File")
|
2021-04-13 15:39:33 +00:00
|
|
|
publishMetric(ms.Usage, "usage", "Usage")
|
|
|
|
publishMetric(ms.MaxUsage, "max_usage", "Max Usage")
|
|
|
|
publishMetric(ms.KernelUsage, "kernel_usage", "Kernel Usage")
|
|
|
|
publishMetric(ms.KernelMaxUsage, "kernel_max_usage", "Kernel Max Usage")
|
2020-10-13 19:56:54 +00:00
|
|
|
if allocatedMem > 0 {
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "memory", "allocated"},
|
|
|
|
allocatedMem, tr.baseLabels)
|
2018-09-15 00:08:26 +00:00
|
|
|
}
|
2023-07-19 20:50:34 +00:00
|
|
|
if allocatedMemMax > 0 {
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "memory", "max_allocated"},
|
|
|
|
allocatedMemMax, tr.baseLabels)
|
|
|
|
}
|
2018-09-15 00:08:26 +00:00
|
|
|
}
|
|
|
|
|
2022-08-16 14:06:30 +00:00
|
|
|
// TODO Remove Backwardscompat or use tr.Alloc()?
|
2018-09-15 00:08:26 +00:00
|
|
|
func (tr *TaskRunner) setGaugeForCPU(ru *cstructs.TaskResourceUsage) {
|
2019-12-09 20:40:22 +00:00
|
|
|
alloc := tr.Alloc()
|
|
|
|
var allocatedCPU float32
|
2020-01-09 14:25:07 +00:00
|
|
|
if taskRes := alloc.AllocatedResources.Tasks[tr.taskName]; taskRes != nil {
|
|
|
|
allocatedCPU = float32(taskRes.Cpu.CpuShares)
|
2019-12-09 20:40:22 +00:00
|
|
|
}
|
|
|
|
|
2020-10-13 19:56:54 +00:00
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "total_percent"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.Percent), tr.baseLabels)
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "system"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.SystemMode), tr.baseLabels)
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "user"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.UserMode), tr.baseLabels)
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "throttled_time"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.ThrottledTime), tr.baseLabels)
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "throttled_periods"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.ThrottledPeriods), tr.baseLabels)
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "total_ticks"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.TotalTicks), tr.baseLabels)
|
2023-07-05 14:28:55 +00:00
|
|
|
metrics.IncrCounterWithLabels([]string{"client", "allocs", "cpu", "total_ticks_count"},
|
|
|
|
float32(ru.ResourceUsage.CpuStats.TotalTicks), tr.baseLabels)
|
2020-10-13 19:56:54 +00:00
|
|
|
if allocatedCPU > 0 {
|
|
|
|
metrics.SetGaugeWithLabels([]string{"client", "allocs", "cpu", "allocated"},
|
|
|
|
allocatedCPU, tr.baseLabels)
|
2018-09-15 00:08:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// emitStats emits resource usage stats of tasks to remote metrics collector
|
|
|
|
// sinks
|
|
|
|
func (tr *TaskRunner) emitStats(ru *cstructs.TaskResourceUsage) {
|
|
|
|
if !tr.clientConfig.PublishAllocationMetrics {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if ru.ResourceUsage.MemoryStats != nil {
|
|
|
|
tr.setGaugeForMemory(ru)
|
client: Return empty values when host stats fail
Currently, there is an issue when running on Windows whereby under some
circumstances the Windows stats API's will begin to return errors (such
as internal timeouts) when a client is under high load, and potentially
other forms of resource contention / system states (and other unknown
cases).
When an error occurs during this collection, we then short circuit
further metrics emission from the client until the next interval.
This can be problematic if it happens for a sustained number of
intervals, as our metrics aggregator will begin to age out older
metrics, and we will eventually stop emitting various types of metrics
including `nomad.client.unallocated.*` metrics.
However, when metrics collection fails on Linux, gopsutil will in many cases
(e.g cpu.Times) silently return 0 values, rather than an error.
Here, we switch to returning empty metrics in these failures, and
logging the error at the source. This brings the behaviour into line
with Linux/Unix platforms, and although making aggregation a little
sadder on intermittent failures, will result in more desireable overall
behaviour of keeping metrics available for further investigation if
things look unusual.
2019-09-18 22:57:23 +00:00
|
|
|
} else {
|
|
|
|
tr.logger.Debug("Skipping memory stats for allocation", "reason", "MemoryStats is nil")
|
2018-09-15 00:08:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ru.ResourceUsage.CpuStats != nil {
|
|
|
|
tr.setGaugeForCPU(ru)
|
client: Return empty values when host stats fail
Currently, there is an issue when running on Windows whereby under some
circumstances the Windows stats API's will begin to return errors (such
as internal timeouts) when a client is under high load, and potentially
other forms of resource contention / system states (and other unknown
cases).
When an error occurs during this collection, we then short circuit
further metrics emission from the client until the next interval.
This can be problematic if it happens for a sustained number of
intervals, as our metrics aggregator will begin to age out older
metrics, and we will eventually stop emitting various types of metrics
including `nomad.client.unallocated.*` metrics.
However, when metrics collection fails on Linux, gopsutil will in many cases
(e.g cpu.Times) silently return 0 values, rather than an error.
Here, we switch to returning empty metrics in these failures, and
logging the error at the source. This brings the behaviour into line
with Linux/Unix platforms, and although making aggregation a little
sadder on intermittent failures, will result in more desireable overall
behaviour of keeping metrics available for further investigation if
things look unusual.
2019-09-18 22:57:23 +00:00
|
|
|
} else {
|
|
|
|
tr.logger.Debug("Skipping cpu stats for allocation", "reason", "CpuStats is nil")
|
2018-09-15 00:08:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-22 00:35:07 +00:00
|
|
|
// appendTaskEvent updates the task status by appending the new event.
|
test: port TestTaskRunner_CheckWatcher_Restart
Added ability to adjust the number of events the TaskRunner keeps as
there's no way to observe all events otherwise.
Task events differ slightly from 0.8 because 0.9 emits Terminated every
time a task exits instead of only when it exits on its own (not due to
restart or kill).
0.9 does not emit Killing/Killed for restarts like 0.8 which seems fine
as `Restart Signaled/Terminated/Restarting` is more descriptive.
Original v0.8 events emitted:
```
expected := []string{
"Received",
"Task Setup",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Restarting",
"Started",
"Restart Signaled",
"Killing",
"Killed",
"Not Restarting",
}
```
2019-01-18 16:30:44 +00:00
|
|
|
func appendTaskEvent(state *structs.TaskState, event *structs.TaskEvent, capacity int) {
|
2018-06-22 00:35:07 +00:00
|
|
|
if state.Events == nil {
|
2018-07-16 23:32:37 +00:00
|
|
|
state.Events = make([]*structs.TaskEvent, 1, capacity)
|
|
|
|
state.Events[0] = event
|
|
|
|
return
|
2018-06-22 00:35:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we hit capacity, then shift it.
|
|
|
|
if len(state.Events) == capacity {
|
|
|
|
old := state.Events
|
|
|
|
state.Events = make([]*structs.TaskEvent, 0, capacity)
|
|
|
|
state.Events = append(state.Events, old[1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
state.Events = append(state.Events, event)
|
|
|
|
}
|
2019-04-28 21:22:53 +00:00
|
|
|
|
|
|
|
func (tr *TaskRunner) TaskExecHandler() drivermanager.TaskExecHandler {
|
2019-08-02 02:07:41 +00:00
|
|
|
// Check it is running
|
|
|
|
handle := tr.getDriverHandle()
|
|
|
|
if handle == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return handle.ExecStreaming
|
2019-04-28 21:22:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (tr *TaskRunner) DriverCapabilities() (*drivers.Capabilities, error) {
|
|
|
|
return tr.driver.Capabilities()
|
|
|
|
}
|
2020-02-11 16:39:16 +00:00
|
|
|
|
2021-12-13 19:54:53 +00:00
|
|
|
// shutdownDelayCancel is used for testing only and cancels the
|
|
|
|
// shutdownDelayCtx
|
|
|
|
func (tr *TaskRunner) shutdownDelayCancel() {
|
|
|
|
tr.shutdownDelayCancelFn()
|
|
|
|
}
|