mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
daemon: close EventsService on shutdown
On daemon shutdown, the HTTP server tries to gracefully shutdown for 5 seconds. If there's an open API connection to the '/events' endpoint, it fails to do so as nothing interrupts that connection, thus forcing the daemon to wait until that timeout is reached. Add a Close method to the EventsService, and call it during daemon shutdown. It'll close any events channel, signaling to the '/events' handler to return and close the connection. It now takes ~1s (or less) to shutdown the daemon when there's an active '/events' connection, instead of 5. Signed-off-by: Albin Kerouanton <albin.kerouanton@docker.com>
This commit is contained in:
@@ -1493,6 +1493,12 @@ func (daemon *Daemon) Shutdown(ctx context.Context) error {
|
||||
daemon.mdDB.Close()
|
||||
}
|
||||
|
||||
// At this point, everything has been shut down and no containers are
|
||||
// running anymore. If there are still some open connections to the
|
||||
// '/events' endpoint, closing the EventsService should tear them down
|
||||
// immediately.
|
||||
daemon.EventsService.Close()
|
||||
|
||||
return daemon.cleanupMounts(cfg)
|
||||
}
|
||||
|
||||
|
||||
@@ -151,3 +151,9 @@ func (e *Events) loadBufferedEvents(since, until time.Time, topic func(any) bool
|
||||
}
|
||||
return buffered
|
||||
}
|
||||
|
||||
// Close all the channels returned to event subscribers.
|
||||
func (e *Events) Close() error {
|
||||
e.pub.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -353,7 +353,12 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-l:
|
||||
case ev, ok := <-l:
|
||||
if !ok {
|
||||
log.G(ctx).Debug("event channel closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
jev, ok := ev.(events.Message)
|
||||
if !ok {
|
||||
log.G(ctx).Warnf("unexpected event message: %q", ev)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
@@ -518,3 +519,42 @@ func TestSwarmNoNftables(t *testing.T) {
|
||||
assert.Check(t, is.ErrorContains(err, "daemon exited during startup"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestDaemonShutsDownQuicklyDespiteEventsConnection checks whether the daemon
|
||||
// shuts down in less than 5 secs when there's an active API connection to the
|
||||
// '/events' endpoint.
|
||||
//
|
||||
// As this test verifies timing behavior, it may become flaky. Feel free to
|
||||
// delete it if it's too annoying.
|
||||
//
|
||||
// Regression test for https://github.com/moby/moby/issues/32357#issuecomment-3496848492
|
||||
func TestDaemonShutsDownQuicklyDespiteEventsConnection(t *testing.T) {
|
||||
skip.If(t, testEnv.IsRemoteDaemon, "cannot start daemon on remote test run")
|
||||
// The Engine is presumably working the same way on Windows and Linux.
|
||||
// Avoid running on Windows as it may be more flaky there.
|
||||
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
|
||||
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.StartSpan(baseContext, t)
|
||||
|
||||
d := daemon.New(t)
|
||||
defer d.Cleanup(t)
|
||||
|
||||
// This is a parallel test, disable iptables integration.
|
||||
d.StartWithBusybox(ctx, t, "--iptables=false", "--ip6tables=false")
|
||||
defer d.Stop(t)
|
||||
|
||||
apiClient := d.NewClientT(t)
|
||||
// Open a connection to the '/events' endpoint.
|
||||
apiClient.Events(ctx, client.EventsListOptions{})
|
||||
|
||||
// Kill the daemon
|
||||
t0 := time.Now()
|
||||
d.Stop(t)
|
||||
|
||||
dt := time.Since(t0)
|
||||
if dt.Seconds() > 5 {
|
||||
t.Error("the daemon took more than 5 secs to shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user