feat: release v0.27 unified HTTP limits and safe request shutdown

This commit is contained in:
2026-09-29 23:24:19 +08:00
parent 859f1851b1
commit 3b00bb55d1
42 changed files with 1191 additions and 70 deletions

View File

@@ -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{}{}: