check / check (push) Waiting to run
Shutdown passes its context to every stage: the streamer, the four handlers, the WHOIS fetcher, the database maintainer and the HTTP server each stop waiting when it ends. A handler then skips its last write and logs how many updates it did not write, and the HTTP server closes the connections still open. The database is checkpointed and closed last, and left open once the deadline has passed, since a write may still hold it. The stop hook gives Shutdown 8 seconds and fx 9, inside the 10 seconds upaas and docker stop allow, so a stop signal exits 0. Model: opus-5-5
93 lines
2.2 KiB
Go
93 lines
2.2 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
)
|
|
|
|
// TestStopClosesTheConnectionOfARequestStillRunning serves a request whose
|
|
// handler does not return until the test ends, and stops the server with a
|
|
// deadline that passes while the request runs. The client must then see its
|
|
// connection closed. Before the fix Stop returned at the deadline and left the
|
|
// connection open until the handler returned.
|
|
func TestStopClosesTheConnectionOfARequestStillRunning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
stopDeadline = 100 * time.Millisecond
|
|
// waitTimeout is how long the test waits for the request to reach
|
|
// the handler, and then for the client to see its connection closed.
|
|
waitTimeout = 2 * time.Second
|
|
// readHeaderTimeout bounds the test server's read of the request
|
|
// header, as every http.Server must.
|
|
readHeaderTimeout = time.Second
|
|
)
|
|
|
|
release := make(chan struct{})
|
|
defer close(release)
|
|
|
|
started := make(chan struct{})
|
|
|
|
var listenConfig net.ListenConfig
|
|
|
|
listener, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to open the test listener: %v", err)
|
|
}
|
|
|
|
s := &Server{logger: logger.New(), srv: &http.Server{
|
|
Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
|
close(started)
|
|
<-release
|
|
}),
|
|
ReadHeaderTimeout: readHeaderTimeout,
|
|
}}
|
|
|
|
go func() { _ = s.srv.Serve(listener) }()
|
|
|
|
answered := make(chan error, 1)
|
|
|
|
go func() {
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
|
|
"http://"+listener.Addr().String()+"/", nil)
|
|
if err != nil {
|
|
answered <- err
|
|
|
|
return
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err == nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
|
|
answered <- err
|
|
}()
|
|
|
|
select {
|
|
case <-started:
|
|
case <-time.After(waitTimeout):
|
|
t.Fatal("the request did not reach the handler")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), stopDeadline)
|
|
defer cancel()
|
|
|
|
_ = s.Stop(ctx)
|
|
|
|
select {
|
|
case err := <-answered:
|
|
if err == nil {
|
|
t.Error("the request still running at the deadline was answered")
|
|
}
|
|
case <-time.After(waitTimeout):
|
|
t.Fatal("the connection of the request still running at the " +
|
|
"deadline was open after Stop returned")
|
|
}
|
|
}
|