mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-16 06:07:54 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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{"*"},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user