Files
moby/daemon/stop.go
Sebastiaan van Stijn 062082ec9b daemon: containerStop: fix ordering of "stop" and "die" events
Commit 8e6cd44ce4 added synchronisation to
wait for the container's status to be updated in memory. However, since
952902efbc, a defer was used to produce
the container's "stop" event.

As a result of the sychronisation that was added, the "die" event would
now be produced before the "stop" event.

This patch moves the locking inside the defer to restore the previous
behavior.

Unfortunately the order of events is still not guaranteed, because events
are emited from multiple goroutines that don't have synchronisation between
them; this is something to look at for follow ups. This patch keeps the status
quo and should preserve the old behavior, which was "more" correct in most
cases.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-18 20:42:52 +02:00

130 lines
4.2 KiB
Go

package daemon
import (
"context"
"time"
"github.com/containerd/log"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/container"
"github.com/docker/docker/errdefs"
"github.com/moby/sys/signal"
"github.com/pkg/errors"
)
// ContainerStop looks for the given container and stops it.
// In case the container fails to stop gracefully within a time duration
// specified by the timeout argument, in seconds, it is forcefully
// terminated (killed).
//
// If the timeout is nil, the container's StopTimeout value is used, if set,
// otherwise the engine default. A negative timeout value can be specified,
// meaning no timeout, i.e. no forceful termination is performed.
func (daemon *Daemon) ContainerStop(ctx context.Context, name string, options containertypes.StopOptions) error {
ctr, err := daemon.GetContainer(name)
if err != nil {
return err
}
if !ctr.IsRunning() {
// This is not an actual error, but produces a 304 "not modified"
// when returned through the API to indicates the container is
// already in the desired state. It's implemented as an error
// to make the code calling this function terminate early (as
// no further processing is needed).
return errdefs.NotModified(errors.New("container is already stopped"))
}
err = daemon.containerStop(ctx, ctr, options)
if err != nil {
return errdefs.System(errors.Wrapf(err, "cannot stop container: %s", name))
}
return nil
}
// containerStop sends a stop signal, waits, sends a kill signal. It uses
// a [context.WithoutCancel], so cancelling the context does not cancel
// the request to stop the container.
func (daemon *Daemon) containerStop(ctx context.Context, ctr *container.Container, options containertypes.StopOptions) (retErr error) {
// Cancelling the request should not cancel the stop.
ctx = context.WithoutCancel(ctx)
if !ctr.IsRunning() {
return nil
}
var (
stopSignal = ctr.StopSignal()
stopTimeout = ctr.StopTimeout()
)
if options.Signal != "" {
sig, err := signal.ParseSignal(options.Signal)
if err != nil {
return errdefs.InvalidParameter(err)
}
stopSignal = sig
}
if options.Timeout != nil {
stopTimeout = *options.Timeout
}
var wait time.Duration
if stopTimeout >= 0 {
wait = time.Duration(stopTimeout) * time.Second
}
defer func() {
if retErr == nil {
daemon.LogContainerEvent(ctr, events.ActionStop)
// Ensure container status changes are committed by handler of container exit before returning control to the caller
ctr.Lock()
defer ctr.Unlock()
}
}()
// 1. Send a stop signal
err := daemon.killPossiblyDeadProcess(ctr, stopSignal)
if err != nil {
wait = 2 * time.Second
}
var subCtx context.Context
var cancel context.CancelFunc
if stopTimeout >= 0 {
subCtx, cancel = context.WithTimeout(ctx, wait)
} else {
subCtx, cancel = context.WithCancel(ctx)
}
defer cancel()
if status := <-ctr.Wait(subCtx, containertypes.WaitConditionNotRunning); status.Err() == nil {
// container did exit, so ignore any previous errors and return
return nil
}
if err != nil {
// the container has still not exited, and the kill function errored, so log the error here:
log.G(ctx).WithError(err).WithField("container", ctr.ID).Errorf("Error sending stop (signal %d) to container", stopSignal)
}
if stopTimeout < 0 {
// if the client requested that we never kill / wait forever, but container.Wait was still
// interrupted (parent context cancelled, for example), we should propagate the signal failure
return err
}
log.G(ctx).WithField("container", ctr.ID).Infof("Container failed to exit within %s of signal %d - using the force", wait, stopSignal)
// Stop either failed or container didn't exit, so fallback to kill.
if err := daemon.Kill(ctr); err != nil {
// got a kill error, but give container 2 more seconds to exit just in case
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
status := <-ctr.Wait(subCtx, containertypes.WaitConditionNotRunning)
if status.Err() != nil {
log.G(ctx).WithError(err).WithField("container", ctr.ID).Errorf("error killing container: %v", status.Err())
return err
}
// container did exit, so ignore previous errors and continue
}
return nil
}