don't cancel container stop when cancelling context
- relates to https://github.com/moby/moby/pull/43206
- fixes https://github.com/moby/moby/issues/45731


Commit 90de570cfa5f6de0b02bdf2e794627fbf5b0dfc8 passed through the request context to daemon.ContainerStop(). As a result, cancelling the context would cancel the "graceful" stop of the container, and would proceed with forcefully killing the container.

This patch partially reverts the changes from 90de570cfa5f6de0b02bdf2e794627fbf5b0dfc8 and breaks the context to prevent cancelling the context from cancelling the stop.

<!--
Please make sure you've read and understood our contributing guidelines;
https://github.com/moby/moby/blob/master/CONTRIBUTING.md

** Make sure all your commits include a signature generated with `git commit -s` **

For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/

If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"

Please provide the following information:
-->

**- What I did**

**- How I did it**

**- How to verify it**

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


daemon/stop.go
package daemon // import "github.com/docker/docker/daemon"

import (
	"context"
	"time"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	"github.com/docker/docker/errdefs"
	"github.com/moby/sys/signal"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// 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() {
		return containerNotModifiedError{}
	}
	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.
func (daemon *Daemon) containerStop(ctx context.Context, ctr *container.Container, options containertypes.StopOptions) (retErr error) {
	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, "stop")
		}
	}()

	// 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, container.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:
		logrus.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
	}

	logrus.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, container.WaitConditionNotRunning)
		if status.Err() != nil {
			logrus.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
}

package daemon // import "github.com/docker/docker/daemon"

import (
	"context"
	"time"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	"github.com/docker/docker/errdefs"
	"github.com/moby/sys/signal"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// 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() {
		return containerNotModifiedError{}
	}
	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.
func (daemon *Daemon) containerStop(_ context.Context, ctr *container.Container, options containertypes.StopOptions) (retErr error) {
	// Deliberately using a local context here, because cancelling the
	// request should not cancel the stop.
	//
	// TODO(thaJeztah): pass context, and use context.WithoutCancel() once available: https://github.com/golang/go/issues/40221
	ctx := context.Background()

	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, "stop")
		}
	}()

	// 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, container.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:
		logrus.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
	}

	logrus.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, container.WaitConditionNotRunning)
		if status.Err() != nil {
			logrus.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
}

Been going back-and-forth whether to remove the context.Context here, but decided to go for the most minimal changes to fix the regression to reduce code-churn (and we may want to have the context available here in future for other purposes).