-
Notifications
You must be signed in to change notification settings - Fork 5.8k
fix: detect externally stopped and removed containers in up monitor #13990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -159,6 +159,13 @@ func (c *monitor) Start(ctx context.Context) error { | |||||
| c.onContainerStart(event, ctr, containers, restarting) | ||||||
| case events.ActionRestart: | ||||||
| c.onContainerRestart(event, ctr) | ||||||
| case events.ActionStop: | ||||||
| err := c.onContainerStop(ctx, ctr, containers, restarting) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| case events.ActionDestroy: | ||||||
| c.onContainerDestroy(ctr, containers, restarting) | ||||||
| case events.ActionDie: | ||||||
| err := c.onContainerDie(ctx, event, ctr, containers, restarting) | ||||||
| if err != nil { | ||||||
|
|
@@ -238,17 +245,11 @@ func (c *monitor) onContainerRestart(event events.Message, ctr *api.ContainerSum | |||||
|
|
||||||
| func (c *monitor) onContainerDie(ctx context.Context, event events.Message, ctr *api.ContainerSummary, containers, restarting utils.Set[string]) error { | ||||||
| logrus.Debugf("container %s exited with code %d", ctr.Name, ctr.ExitCode) | ||||||
| inspect, err := c.apiClient.ContainerInspect(ctx, event.Actor.ID, client.ContainerInspectOptions{}) | ||||||
| if errdefs.IsNotFound(err) { | ||||||
| // Source is already removed | ||||||
| } else if err != nil { | ||||||
| willRestart, err := c.isRestarting(ctx, ctr.ID) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| if inspect.Container.State != nil && (inspect.Container.State.Restarting || inspect.Container.State.Running) { | ||||||
| // State.Restarting is set by engine when container is configured to restart on exit | ||||||
| // on ContainerRestart it doesn't (see https://github.com/moby/moby/issues/45538) | ||||||
| // container state still is reported as "running" | ||||||
| if willRestart { | ||||||
| logrus.Debugf("container %s is restarting", ctr.Name) | ||||||
| restarting.Add(ctr.ID) | ||||||
| c.notify(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited, func(e *api.ContainerEvent) { | ||||||
|
|
@@ -262,6 +263,55 @@ func (c *monitor) onContainerDie(ctx context.Context, event events.Message, ctr | |||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| // onContainerStop handles a stop event with no following start: the container won't | ||||||
| // come back, either because it has no restart policy, or because an external | ||||||
| // `stop`/`down` canceled the restart loop of a container in backoff | ||||||
| // (https://github.com/docker/compose/issues/13985). The event alone can't tell us: | ||||||
| // during a ContainerRestart (watch sync+restart, https://github.com/docker/compose/issues/13161) | ||||||
| // the engine also emits `stop` before `start`. | ||||||
| func (c *monitor) onContainerStop(ctx context.Context, ctr *api.ContainerSummary, containers, restarting utils.Set[string]) error { | ||||||
| willRestart, err := c.isRestarting(ctx, ctr.ID) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| if willRestart { | ||||||
| logrus.Debugf("container %s stopped, restart in progress", ctr.Name) | ||||||
| restarting.Add(ctr.ID) | ||||||
| } else { | ||||||
| // definitive stop: the exit was already reported to listeners by the | ||||||
| // preceding die event, just stop tracking the container | ||||||
| logrus.Debugf("container %s stopped", ctr.Name) | ||||||
| restarting.Remove(ctr.ID) | ||||||
| containers.Remove(ctr.ID) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] Final event consumers receive after external stop-of-backoff has In the
The only The test Fix: pass the stop-event timestamp into // in the else branch of onContainerStop:
c.notify(newContainerEvent(stopTimeNano, ctr, api.ContainerEventExited))
restarting.Remove(ctr.ID)
containers.Remove(ctr.ID)This also means the test assertion should flip to
|
||||||
| } | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| // onContainerDestroy handles a container removed by an external `docker compose down`: | ||||||
| // terminal state, there is nothing left to inspect. | ||||||
| func (c *monitor) onContainerDestroy(ctr *api.ContainerSummary, containers, restarting utils.Set[string]) { | ||||||
| logrus.Debugf("container %s destroyed", ctr.Name) | ||||||
| restarting.Remove(ctr.ID) | ||||||
| containers.Remove(ctr.ID) | ||||||
| } | ||||||
|
|
||||||
| // isRestarting tells whether a container which just stopped is expected to come back. | ||||||
| // State.Restarting is set by the engine when the container is configured to restart on | ||||||
| // exit, but not on a ContainerRestart, where state still is reported as "running" | ||||||
| // (see https://github.com/moby/moby/issues/45538). A container already removed won't | ||||||
| // come back. | ||||||
| func (c *monitor) isRestarting(ctx context.Context, containerID string) (bool, error) { | ||||||
| inspect, err := c.apiClient.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) | ||||||
| if errdefs.IsNotFound(err) { | ||||||
| return false, nil | ||||||
| } | ||||||
| if err != nil { | ||||||
| return false, err | ||||||
| } | ||||||
| state := inspect.Container.State | ||||||
| return state != nil && (state.Restarting || state.Running), nil | ||||||
| } | ||||||
|
|
||||||
| func newContainerEvent(timeNano int64, ctr *api.ContainerSummary, eventType int, opts ...func(e *api.ContainerEvent)) api.ContainerEvent { | ||||||
| name := ctr.Name | ||||||
| defaultName := getDefaultContainerName(ctr.Project, ctr.Labels[api.ServiceLabel], ctr.Labels[api.ContainerNumberLabel]) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| services: | ||
| app: | ||
| image: alpine | ||
| restart: unless-stopped | ||
| command: sh -c "exit 1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low]
onContainerDie's non-restart path leaves a stalerestartingentry whenonContainerStopran firstThe new
onContainerStophandler (added by this PR) can add a container torestartingwhenisRestartingreturnstrue(i.e.,State.Running == true, mid-exit during a normaldocker stop). If thisstopevent arrives before the correspondingdieevent:stop→onContainerStop:Running=trueseen →restarting.Add(ctr.ID)die→onContainerDie:Exitedseen →willRestart=false→ callscontainers.Remove(ctr.ID)but NOTrestarting.Remove(ctr.ID)The container is removed from
containers(so the monitor eventually terminates), but therestartingset retains a stale entry. In the current session this is harmless — once the container leavescontainers, no futurestartevent for it is expected. However if the container is externally re-created with the same ID before the monitor session ends,onContainerStartwould seerestarting.Has(ctr.ID) == trueand emitContainerEventStarted{Restarting: true}incorrectly (suggesting it is recovering from a monitored crash rather than being a fresh start).Fix: add
restarting.Remove(ctr.ID)toonContainerDie's non-restart branch (alongside the existingcontainers.Remove):This is a defensive cleanup that makes the two paths (
stop-first anddie-first) symmetric.