Merge pull request #50908 from ndeloof/restarting

Add `restarting` attribute on `die` events
This commit is contained in:
Paweł Gronowski authored and GitHub committed 2026-10-29 17:30:14 +01:00
commit cecd7514ba
4 files changed
+162 -1

No files matched your search

+7
View File
@@ -15,6 +15,13 @@ keywords: "API, Docker, rcli, REST, documentation"
## v1.57 API changes
* `GET /events`: `die` events for containers now include a `restarting`
attribute, set to `"true"` when the container is going to be restarted,
either by its restart policy or by a manual restart in progress. The
attribute is omitted when the container is not going to be restarted,
including when the daemon is shutting down. It announces the restart but
does not guarantee it: the restart can still be cancelled (for example by
stopping the container) or fail to start the container again.
* The API specification now uses OpenAPI 3.2.0 instead of Swagger 2.0.
A generated Swagger 2.0 version is still available for existing tooling.
+7
View File
@@ -120,6 +120,13 @@ func (daemon *Daemon) handleContainerExit(c *container.Container, e *libcontaine
"exitCode": strconv.Itoa(ctrExitStatus.ExitCode),
"execDuration": strconv.Itoa(int(execDuration.Seconds())),
}
if restart || (c.HasBeenManuallyRestarted && !daemonShutdown) {
// The container will be restarted, either by the restart policy or
// by a manual "container restart" in progress (which doesn't
// complete if the daemon is shutting down).
attributes["restarting"] = "true"
}
daemon.Cleanup(context.TODO(), c)
if restart {
+12 -1
View File
@@ -32,7 +32,7 @@ func (daemon *Daemon) ContainerRestart(ctx context.Context, name string, options
// container. When stopping, wait for the given duration in seconds to
// gracefully stop, before forcefully terminating the container. If
// given a negative duration, wait forever for a graceful stop.
func (daemon *Daemon) containerRestart(ctx context.Context, daemonCfg *configStore, container *container.Container, options backend.ContainerStopOptions) error {
func (daemon *Daemon) containerRestart(ctx context.Context, daemonCfg *configStore, container *container.Container, options backend.ContainerStopOptions) (retErr error) {
// Restarting is expected to be an atomic operation, and cancelling
// the request should not cancel the stop -> start sequence.
ctx = context.WithoutCancel(ctx)
@@ -60,6 +60,17 @@ func (daemon *Daemon) containerRestart(ctx context.Context, daemonCfg *configSto
container.HasBeenManuallyRestarted = true
container.Unlock()
// The flag is only cleared once the container is started again. If
// the restart fails, make sure it doesn't linger: otherwise a later,
// unrelated exit would be reported as "going to restart".
defer func() {
if retErr != nil {
container.Lock()
container.HasBeenManuallyRestarted = false
container.Unlock()
}
}()
err := daemon.containerStop(ctx, container, options)
if err != nil {
return err
+136
View File
@@ -564,3 +564,139 @@ func testContainerRestartWithCancelledRequest(ctx context.Context, t *testing.T,
assert.NilError(t, err)
assert.Check(t, is.Equal(inspect.Container.State.Status, container.StateRunning))
}
// TestContainerDieEventRestartingAttribute verifies that "die" events tell
// event consumers whether the container is going to be restarted, either by
// its restart policy or by a manual restart, so that they don't have to
// inspect the container to find out.
func TestContainerDieEventRestartingAttribute(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "test requires a Linux shell")
ctx := setupTest(t)
apiClient := testEnv.APIClient()
// newContainer creates a container, and returns a function to wait for
// its events. Events are subscribed to before the container is started,
// so none of them can be missed.
newContainer := func(ctx context.Context, t *testing.T, ops ...func(*testContainer.TestContainerConfig)) (string, *containerEvents) {
t.Helper()
cID := testContainer.Create(ctx, t, apiClient, ops...)
t.Cleanup(func() {
apiClient.ContainerRemove(context.WithoutCancel(ctx), cID, client.ContainerRemoveOptions{Force: true})
})
evs := listenContainerEvents(ctx, t, apiClient, cID)
_, err := apiClient.ContainerStart(ctx, cID, client.ContainerStartOptions{})
assert.NilError(t, err)
return cID, evs
}
exitWithFailure := testContainer.WithCmd("sh", "-c", "exit 1")
stopTimeout := 1
t.Run("restart-policy", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
_, evs := newContainer(ctx, t, exitWithFailure, testContainer.WithRestartPolicy(container.RestartPolicyAlways))
die := evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], "true"))
evs.next(t, events.ActionStart)
})
t.Run("restart-policy-max-retries-reached", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
_, evs := newContainer(ctx, t, exitWithFailure, func(c *testContainer.TestContainerConfig) {
c.HostConfig.RestartPolicy = container.RestartPolicy{
Name: container.RestartPolicyOnFailure,
MaximumRetryCount: 1,
}
})
// The first exit is followed by the one allowed retry.
die := evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], "true"))
evs.next(t, events.ActionStart)
// The second one isn't: no more retries.
die = evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], ""))
})
t.Run("no-restart-policy", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
_, evs := newContainer(ctx, t, exitWithFailure)
die := evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], ""))
})
t.Run("stopped-with-restart-policy", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
cID, evs := newContainer(ctx, t, testContainer.WithRestartPolicy(container.RestartPolicyAlways))
poll.WaitOn(t, testContainer.IsInState(ctx, apiClient, cID, container.StateRunning), poll.WithTimeout(30*time.Second))
_, err := apiClient.ContainerStop(ctx, cID, client.ContainerStopOptions{Timeout: &stopTimeout})
assert.NilError(t, err)
die := evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], ""))
})
t.Run("manual-restart", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
cID, evs := newContainer(ctx, t)
poll.WaitOn(t, testContainer.IsInState(ctx, apiClient, cID, container.StateRunning), poll.WithTimeout(30*time.Second))
_, err := apiClient.ContainerRestart(ctx, cID, client.ContainerRestartOptions{Timeout: &stopTimeout})
assert.NilError(t, err)
die := evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], "true"))
evs.next(t, events.ActionStart)
evs.next(t, events.ActionRestart)
// The container isn't flagged as restarting once it was restarted.
_, err = apiClient.ContainerStop(ctx, cID, client.ContainerStopOptions{Timeout: &stopTimeout})
assert.NilError(t, err)
die = evs.next(t, events.ActionDie)
assert.Check(t, is.Equal(die.Actor.Attributes["restarting"], ""))
})
}
// containerEvents delivers the events of a single container.
type containerEvents struct {
messages <-chan events.Message
errs <-chan error
}
// listenContainerEvents starts listening for the events of a container, until
// the test completes.
func listenContainerEvents(ctx context.Context, t *testing.T, apiClient client.APIClient, cID string) *containerEvents {
t.Helper()
ctx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
// Events are replayed from now (the container is yet to be started), so
// that a container exiting right away doesn't beat the subscription.
result := apiClient.Events(ctx, client.EventsListOptions{
Since: strconv.FormatInt(time.Now().Unix(), 10),
Filters: make(client.Filters).Add("container", cID),
})
return &containerEvents{messages: result.Messages, errs: result.Err}
}
// next waits for the next event with the given action, skipping any other one.
func (e *containerEvents) next(t *testing.T, action events.Action) events.Message {
t.Helper()
timeout := time.After(30 * time.Second)
for {
select {
case m := <-e.messages:
if m.Action == action {
return m
}
case err := <-e.errs:
t.Fatalf("error while waiting for %q event: %v", action, err)
case <-timeout:
t.Fatalf("timeout waiting for %q event", action)
}
}
}