mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-21 06:37:21 +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
|
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
|
// Cache shutdown is handled internally by the cache implementation
|
||||||
|
|
||||||
// Start monitoring server
|
// Start monitoring server
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
@@ -36,6 +38,28 @@ type DependencyStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartHTTPProxy initializes and starts the HTTP proxy server.
|
// 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 {
|
func StartHTTPProxy() error {
|
||||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||||
Message: "Starting the HTTP proxy",
|
Message: "Starting the HTTP proxy",
|
||||||
@@ -51,6 +75,9 @@ func StartHTTPProxy() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server := fiber.New(serverConfig)
|
server := fiber.New(serverConfig)
|
||||||
|
httpProxyMu.Lock()
|
||||||
|
httpProxyApp = server
|
||||||
|
httpProxyMu.Unlock()
|
||||||
|
|
||||||
server.Use(cors.New(cors.Config{
|
server.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"*"},
|
AllowOrigins: []string{"*"},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -599,3 +600,63 @@ func TestStartHTTPProxy_StartsAndShutdown(t *testing.T) {
|
|||||||
t.Errorf("want 200, got %d", httpResp.StatusCode)
|
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