diff --git a/cmd/tapir/shutdown_test.go b/cmd/tapir/shutdown_test.go index be808d3..458d266 100644 --- a/cmd/tapir/shutdown_test.go +++ b/cmd/tapir/shutdown_test.go @@ -2,6 +2,8 @@ package main import ( "context" + "net/http" + "net/http/httptest" "os" "syscall" "testing" @@ -28,3 +30,64 @@ func TestShutdownContext_IsCancelledBySIGTERMAndSIGINT(t *testing.T) { }) } } + +// serveHTTP must not return while a request is in flight or the worker is still recording +// its job: ListenAndServe returns ErrServerClosed the moment Shutdown starts, so returning +// from it let the process exit before either finished (#50). +func TestServeHTTP_ReturnsOnlyAfterTheInFlightRequestAndTheWorkerHaveFinished(t *testing.T) { + release := make(chan struct{}) + entered := make(chan struct{}) + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + close(entered) + <-release + _, _ = w.Write([]byte("ok")) + })) + srv.Start() + defer srv.Close() + ctx, cancel := context.WithCancel(context.Background()) + workerDone := make(chan struct{}) + returned := make(chan error, 1) + go func() { returned <- serveHTTP(ctx, srv.Config, srv.Listener, nil, workerDone, 5*time.Second) }() + + go func() { _, _ = http.Get(srv.URL) }() + <-entered + cancel() + + assertPending := func(why string) { + t.Helper() + select { + case err := <-returned: + t.Fatalf("serveHTTP returned (%v) %s", err, why) + case <-time.After(150 * time.Millisecond): + } + } + assertPending("while a request was still in flight") + close(release) + assertPending("before the worker finished") + close(workerDone) + select { + case err := <-returned: + if err != nil { + t.Fatalf("serveHTTP: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("serveHTTP did not return once the request and the worker had finished") + } +} + +func TestServeHTTP_GivesUpOnAWorkerThatNeverFinishesAfterTheGrace(t *testing.T) { + srv := httptest.NewUnstartedServer(http.NotFoundHandler()) + srv.Start() + defer srv.Close() + ctx, cancel := context.WithCancel(context.Background()) + returned := make(chan error, 1) + go func() { returned <- serveHTTP(ctx, srv.Config, srv.Listener, nil, make(chan struct{}), 200*time.Millisecond) }() + + cancel() + + select { + case <-returned: + case <-time.After(3 * time.Second): + t.Fatal("a stuck worker must not hold the process past the grace period") + } +}