daemon: ignore duplicate task exit events in daemon state

Handle duplicate TaskExit events by checking container state and timestamps
while holding the container lock. Ignore late duplicates when restarting or
when a newer task is already running.

Signed-off-by: Wei Fu <fuweid89@gmail.com>
This commit is contained in:
Wei Fu
2026-01-25 17:46:44 -05:00
parent f89429f18b
commit d5e23b680c
2 changed files with 59 additions and 1 deletions

View File

@@ -299,7 +299,11 @@ func (s *State) SetRestarting(exitStatus *ExitStatus) {
s.Restarting = true
s.Paused = false
s.Pid = 0
s.FinishedAt = time.Now().UTC()
finishedAt := exitStatus.ExitedAt
if finishedAt.IsZero() {
finishedAt = time.Now().UTC()
}
s.FinishedAt = finishedAt
s.ExitCode = exitStatus.ExitCode
s.notifyAndClear(&s.stopWaiters)

View File

@@ -48,6 +48,13 @@ func (daemon *Daemon) handleContainerExit(c *container.Container, e *libcontaine
return nil
}
// Ignore duplicate exit event that may arrive after the first one.
// See moby/moby#46212.
if daemon.shouldIgnoreExitEventWithLock(c, e) {
c.Unlock()
return nil
}
cfg := daemon.config()
// Health checks will be automatically restarted if/when the
@@ -339,3 +346,50 @@ func (daemon *Daemon) autoRemove(cfg *config.Config, c *container.Container) {
log.G(context.TODO()).WithFields(log.Fields{"error": err, "container": c.ID}).Error("error removing container")
}
}
func (daemon *Daemon) shouldIgnoreExitEventWithLock(c *container.Container, e *libcontainerdtypes.EventInfo) (ret bool) {
if e == nil {
return false
}
curState := c.State.State()
defer func() {
if ret {
log.G(context.TODO()).
WithFields(log.Fields{
"container": c.ID,
"state": c.State.String(),
"exitCode": e.ExitCode,
"exitedAt": e.ExitedAt,
}).Info("ignoring duplicate container exit event")
}
}()
switch curState {
case containertypes.StateRemoving,
containertypes.StateExited,
containertypes.StateDead:
return true
case containertypes.StateRunning:
// If the container is running, but the exit event is from
// before it was started, ignore it. This can happen when a
// duplicate exit arrives while the restart path holds the
// container lock; by the time we process it, a new task is
// already running, so the exit belongs to the previous task.
return !e.ExitedAt.IsZero() && e.ExitedAt.Before(c.StartedAt)
case containertypes.StateRestarting:
// The restart path acquires and holds the container lock before
// processing; on failure it transitions the container to exited,
// and on success it transitions to running. Therefore, any exit
// event observed while still restarting is a late duplicate from
// the previous task and should be ignored.
return !e.ExitedAt.IsZero() && e.ExitedAt.After(c.FinishedAt)
default:
return false
}
}