161 lines
3.6 KiB
Go
161 lines
3.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
func TestServerShutdownDrainsInFlightRequestsAndRejectsNewRequests(t *testing.T) {
|
|
app := fiber.New(fiber.Config{
|
|
DisableStartupMessage: true,
|
|
})
|
|
UseDefaultMiddlewares(app, testHTTPConfig())
|
|
|
|
slowStarted := make(chan struct{})
|
|
app.Get("/slow", func(c *fiber.Ctx) error {
|
|
select {
|
|
case <-slowStarted:
|
|
default:
|
|
close(slowStarted)
|
|
}
|
|
|
|
select {
|
|
case <-time.After(300 * time.Millisecond):
|
|
case <-c.UserContext().Done():
|
|
return c.Status(http.StatusInternalServerError).SendString("user context canceled")
|
|
}
|
|
|
|
return c.SendString("slow done")
|
|
})
|
|
app.Get("/fast", func(c *fiber.Ctx) error {
|
|
return c.SendString("fast done")
|
|
})
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
|
|
serveErrCh := make(chan error, 1)
|
|
go func() {
|
|
serveErrCh <- app.Listener(ln)
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
_ = app.Shutdown()
|
|
})
|
|
|
|
baseURL := "http://" + ln.Addr().String()
|
|
fastClient := newTestHTTPClient()
|
|
slowClient := newTestHTTPClient()
|
|
defer fastClient.CloseIdleConnections()
|
|
defer slowClient.CloseIdleConnections()
|
|
|
|
if err := waitForHTTP200(fastClient, baseURL+"/fast", time.Second); err != nil {
|
|
t.Fatalf("wait for server ready: %v", err)
|
|
}
|
|
|
|
slowRespCh := make(chan httpCallResult, 1)
|
|
go func() {
|
|
resp, err := slowClient.Get(baseURL + "/slow")
|
|
if err != nil {
|
|
slowRespCh <- httpCallResult{err: err}
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, readErr := io.ReadAll(resp.Body)
|
|
slowRespCh <- httpCallResult{
|
|
statusCode: resp.StatusCode,
|
|
body: string(body),
|
|
err: readErr,
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-slowStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("slow request did not start")
|
|
}
|
|
|
|
srv := NewServer(app, WithShutdownTimeout(2*time.Second))
|
|
shutdownErrCh := make(chan error, 1)
|
|
go func() {
|
|
shutdownErrCh <- srv.Shutdown(context.Background())
|
|
}()
|
|
|
|
if err := waitForRequestRejection(fastClient, baseURL+"/fast", time.Second); err == nil {
|
|
t.Fatal("expected new requests to be rejected during shutdown")
|
|
}
|
|
|
|
slowResult := <-slowRespCh
|
|
if slowResult.err != nil {
|
|
t.Fatalf("slow request failed: %v", slowResult.err)
|
|
}
|
|
if slowResult.statusCode != http.StatusOK {
|
|
t.Fatalf("expected slow request 200, got %d with body %q", slowResult.statusCode, slowResult.body)
|
|
}
|
|
if slowResult.body != "slow done" {
|
|
t.Fatalf("expected slow request body %q, got %q", "slow done", slowResult.body)
|
|
}
|
|
|
|
if err := <-shutdownErrCh; err != nil {
|
|
t.Fatalf("shutdown failed: %v", err)
|
|
}
|
|
if err := <-serveErrCh; err != nil {
|
|
t.Fatalf("listener exited with error: %v", err)
|
|
}
|
|
}
|
|
|
|
type httpCallResult struct {
|
|
statusCode int
|
|
body string
|
|
err error
|
|
}
|
|
|
|
func newTestHTTPClient() *http.Client {
|
|
return &http.Client{
|
|
Timeout: time.Second,
|
|
Transport: &http.Transport{
|
|
DisableKeepAlives: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func waitForHTTP200(client *http.Client, url string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
resp, err := client.Get(url)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
return errors.New("server did not become ready")
|
|
}
|
|
|
|
func waitForRequestRejection(client *http.Client, url string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
var lastErr error
|
|
for time.Now().Before(deadline) {
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lastErr = errors.New("request still accepted")
|
|
resp.Body.Close()
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
return lastErr
|
|
}
|