feat: release v0.27 unified HTTP limits and safe request shutdown
This commit is contained in:
@@ -247,3 +247,29 @@ func TestCanceledServeContextWaitsForShutdownDeadline(t *testing.T) {
|
||||
}
|
||||
waitSignal(t, canceled)
|
||||
}
|
||||
|
||||
func TestShutdownWaitsForHandlerAfterClientDisconnect(t *testing.T) {
|
||||
started, canceled, release, finished := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
|
||||
s := New(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
defer close(finished)
|
||||
close(started)
|
||||
<-r.Context().Done()
|
||||
close(canceled)
|
||||
<-release
|
||||
}))
|
||||
defer func() { close(release); waitSignal(t, finished) }()
|
||||
address, done := launchServer(t, s, context.Background())
|
||||
p := newFramePeer(t, address)
|
||||
p.get(t, 1, "/hold")
|
||||
waitSignal(t, started)
|
||||
p.Close()
|
||||
waitSignal(t, canceled)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := s.Shutdown(ctx); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("disconnected handler reported as drained: %v", err)
|
||||
}
|
||||
// Serve can finish its bounded shutdown, but successful Shutdown must never
|
||||
// imply that a disconnected, still-running handler has completed.
|
||||
_ = serveResult(t, done)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ type Server struct {
|
||||
activeConns atomic.Int64
|
||||
activeRequests atomic.Int64
|
||||
wg sync.WaitGroup
|
||||
requestWG sync.WaitGroup
|
||||
}
|
||||
|
||||
// Stats is a point-in-time view of HTTP/2 transport admission and lifecycle.
|
||||
@@ -390,7 +391,8 @@ func (s *Server) tlsHandshakeTimeout() time.Duration {
|
||||
return defaultTLSHandshake
|
||||
}
|
||||
|
||||
// Shutdown stops accepting connections and waits for active HTTP/2 sessions.
|
||||
// Shutdown stops accepting connections/requests and waits for both HTTP/2
|
||||
// sessions and admitted handlers, even when the client has already disconnected.
|
||||
// If the context expires, remaining connections are closed so handlers can
|
||||
// observe cancellation through their request contexts.
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
@@ -436,6 +438,7 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.wg.Wait()
|
||||
s.requestWG.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
@@ -476,6 +479,17 @@ type requestGate struct {
|
||||
}
|
||||
|
||||
func (g *requestGate) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
// Synchronize admission with Shutdown before it starts waiting. Connection
|
||||
// completion alone does not join x/net/http2's separate handler goroutines.
|
||||
g.server.mu.Lock()
|
||||
if g.server.closing {
|
||||
g.server.mu.Unlock()
|
||||
http.Error(writer, "server shutting down", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
g.server.requestWG.Add(1)
|
||||
g.server.mu.Unlock()
|
||||
defer g.server.requestWG.Done()
|
||||
if g.slots != nil {
|
||||
select {
|
||||
case g.slots <- struct{}{}:
|
||||
|
||||
Reference in New Issue
Block a user