fix(server): gracefully shut down HTTP proxy to drain in-flight requests

On signal, main waited on wg for StartHTTPProxy's server.Listen, which never
returns because nothing called server.Shutdown(); it timed out after 10s and
process-exited, abruptly killing in-flight GraphQL requests.

Expose the proxy Fiber app and register an http-proxy Shutdown component so
server.Listen returns after Shutdown, letting connections drain. Guard the
shared app handle with a mutex (written by the StartHTTPProxy goroutine, read
by the shutdown component). Verified with build, vet, full test and full
race suite.
This commit is contained in:
2026-08-13 02:37:14 +01:00
parent d04ab329a1
commit db2216973e
3 changed files with 92 additions and 0 deletions
+4
View File
@@ -612,6 +612,10 @@ func main() {
return nil
})
// Register HTTP proxy so in-flight requests drain gracefully on shutdown
// instead of being cut off when the process exits.
shutdownManager.RegisterComponent("http-proxy", shutdownHTTPProxy)
// Cache shutdown is handled internally by the cache implementation
// Start monitoring server
+27
View File
@@ -1,8 +1,10 @@
package main
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/goccy/go-json"
@@ -36,6 +38,28 @@ type DependencyStatus struct {
}
// StartHTTPProxy initializes and starts the HTTP proxy server.
// httpProxyServer holds the live proxy Fiber app so the shutdown sequence can
// call Shutdown() on it, letting in-flight requests drain instead of being
// cut off by process exit. It is written by the StartHTTPProxy goroutine and
// read by the shutdown component, hence guarding with a mutex.
var (
httpProxyMu sync.RWMutex
httpProxyApp *fiber.App
)
// shutdownHTTPProxy gracefully stops the HTTP proxy server. It is invoked from
// the ShutdownManager during shutdown so server.Listen returns and the proxy
// can finish in-flight requests.
func shutdownHTTPProxy(_ context.Context) error {
httpProxyMu.RLock()
app := httpProxyApp
httpProxyMu.RUnlock()
if app != nil {
return app.Shutdown()
}
return nil
}
func StartHTTPProxy() error {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Starting the HTTP proxy",
@@ -51,6 +75,9 @@ func StartHTTPProxy() error {
}
server := fiber.New(serverConfig)
httpProxyMu.Lock()
httpProxyApp = server
httpProxyMu.Unlock()
server.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
+61
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
@@ -599,3 +600,63 @@ func TestStartHTTPProxy_StartsAndShutdown(t *testing.T) {
t.Errorf("want 200, got %d", httpResp.StatusCode)
}
}
// Test_shutdownHTTPProxy verifies the graceful-shutdown helper: it is a no-op
// when no server is running, and it stops a listening proxy server so that
// server.Listen returns (enabling a clean in-flight-request drain instead of
// process-exit cutoff).
func (suite *Tests) Test_shutdownHTTPProxy() {
// No server running: must be a safe no-op.
httpProxyMu.Lock()
original := httpProxyApp
httpProxyApp = nil
httpProxyMu.Unlock()
defer func() {
httpProxyMu.Lock()
httpProxyApp = original
httpProxyMu.Unlock()
}()
suite.NoError(shutdownHTTPProxy(context.Background()))
// With a listening server assigned, shutdown must stop it.
app := fiber.New()
app.Get("/ping", func(c fiber.Ctx) error { return c.SendStatus(200) })
ln, err := net.Listen("tcp", "127.0.0.1:0")
suite.Require().NoError(err)
listenDone := make(chan struct{})
go func() {
defer close(listenDone)
_ = app.Listener(ln)
}()
// Wait until the listener is actually serving (poll the port) before
// shutting down; otherwise Shutdown can race server startup and no-op.
addr := ln.Addr().String()
deadline := time.Now().Add(3 * time.Second)
for {
conn, err := net.Dial("tcp", addr)
if err == nil {
_ = conn.Close()
break
}
if time.Now().After(deadline) {
suite.Fail("proxy server never started listening on " + addr)
return
}
time.Sleep(10 * time.Millisecond)
}
httpProxyMu.Lock()
httpProxyApp = app
httpProxyMu.Unlock()
suite.NoError(shutdownHTTPProxy(context.Background()))
select {
case <-listenDone:
// Listen returned because we called Shutdown.
case <-time.After(3 * time.Second):
suite.Fail("proxy server did not shut down; in-flight requests would be cut off")
}
}