Bound each shutdown stage by the stop deadline (closes #100)
check / check (push) Waiting to run
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
This commit is contained in:
@@ -109,7 +109,9 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the HTTP server
|
||||
// Stop stops the HTTP server. It waits, until ctx ends, for the requests in
|
||||
// progress to finish, and then closes the connections of those still running
|
||||
// and returns ctx's error.
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
if s.srv == nil {
|
||||
return nil
|
||||
@@ -117,7 +119,12 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
|
||||
s.logger.Info("Stopping HTTP server")
|
||||
|
||||
return s.srv.Shutdown(ctx)
|
||||
err := s.srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return errors.Join(err, s.srv.Close())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user