chore: initial commit of Go API gateway
Baseline for the Kong replacement on api.riotpiao.com. Brings the working tree under version control for the first time: gateway source, the task board that drives the agent runs, test fixtures, and K8s manifests. Anchor the gateway ignore rule to the repo root. Unanchored, "gateway" also matched the cmd/gateway/ source directory, so the program entrypoint was excluded from every commit. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Riotpiaole/homelab-frontend/internal/server"
|
||||
)
|
||||
|
||||
// TestGracefulShutdown verifies that:
|
||||
// - A request in-flight when shutdown starts receives its full, uncorrupted response body
|
||||
// - A request arriving after shutdown starts is refused on a new connection
|
||||
// - The shutdown completes with exit code 0 (no timeout)
|
||||
func TestGracefulShutdown(t *testing.T) {
|
||||
// Create a handler that responds slowly
|
||||
const responseBody = "slow response body content"
|
||||
const sleepDuration = 500 * time.Millisecond
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Simulate a slow LLM response
|
||||
time.Sleep(sleepDuration)
|
||||
fmt.Fprint(w, responseBody)
|
||||
})
|
||||
|
||||
// Create server with a shutdown timeout longer than the sleep
|
||||
srv := server.New("127.0.0.1:0", 5*time.Second, handler)
|
||||
|
||||
// Start server in a goroutine
|
||||
var listenErr error
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
listenErr = srv.ListenAndServe()
|
||||
// http.ErrServerClosed is expected after shutdown
|
||||
if listenErr != nil && listenErr != http.ErrServerClosed {
|
||||
t.Logf("unexpected listen error: %v", listenErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Give server time to start listening
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Issue a slow request in a goroutine
|
||||
var responseBody_got string
|
||||
var requestErr error
|
||||
var requestWg sync.WaitGroup
|
||||
requestWg.Add(1)
|
||||
go func() {
|
||||
defer requestWg.Done()
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s/", srv.Addr()))
|
||||
if err != nil {
|
||||
requestErr = err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
requestErr = err
|
||||
return
|
||||
}
|
||||
responseBody_got = string(body)
|
||||
}()
|
||||
|
||||
// Give the request time to reach the handler
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Now initiate shutdown while request is in-flight
|
||||
shutdownErr := srv.Shutdown(context.Background())
|
||||
|
||||
// Wait for the in-flight request to complete
|
||||
requestWg.Wait()
|
||||
|
||||
// Verify the in-flight request completed successfully
|
||||
if requestErr != nil {
|
||||
t.Fatalf("in-flight request failed: %v", requestErr)
|
||||
}
|
||||
if responseBody_got != responseBody {
|
||||
t.Fatalf("in-flight request got wrong body: %q (expected %q)", responseBody_got, responseBody)
|
||||
}
|
||||
|
||||
// Verify shutdown succeeded (no timeout)
|
||||
if shutdownErr != nil {
|
||||
t.Fatalf("shutdown failed: %v", shutdownErr)
|
||||
}
|
||||
|
||||
// Verify in-flight requests were allowed to complete
|
||||
wg.Wait()
|
||||
|
||||
// Now verify that a new request is refused after shutdown
|
||||
_, err := net.Dial("tcp", srv.Addr())
|
||||
if err == nil {
|
||||
// Connection succeeded when it should have failed
|
||||
t.Fatalf("new connection accepted after shutdown (should have been refused)")
|
||||
}
|
||||
// If we get here, the connection was properly refused, which is what we want
|
||||
}
|
||||
Reference in New Issue
Block a user