Keep working out the home page cards after startup (closes #211)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Server.Start ran the loop that works out the home page's cards on the context it was given. fx ends that context once start-up is done, so the cards were worked out once and never again. The loop now runs on a context that only Stop ends, as the other background parts do. Stop waits for the loop to return, until its own context ends, before it stops the HTTP server. A new test starts a server in a synctest bubble and ends its context at once. On the bubble's fake clock, the cards must be worked out again a minute later, and not after Stop. Model: opus-5-5
This commit was merged in pull request #212.
This commit is contained in:
@@ -35,6 +35,9 @@ tested.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-27: the server works out the home page's cards every minute until it
|
||||||
|
stops. It used to work them out only once, at startup: the end of the context
|
||||||
|
that `Start` is given stopped the refreshes (closes #211)
|
||||||
- 2026-10-27: README "Rationale" says why RouteWatch exists: to answer our other
|
- 2026-10-27: README "Rationale" says why RouteWatch exists: to answer our other
|
||||||
services' questions about IP addresses, from its own routing table and the
|
services' questions about IP addresses, from its own routing table and the
|
||||||
WHOIS details it keeps. It still says what RIS Live does not give (closes
|
WHOIS details it keeps. It still says what RIS Live does not give (closes
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||||
@@ -341,6 +342,66 @@ func TestStartedServerWorksOutTheCards(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStartedServerWorksOutTheCardsUntilStop starts a server with a context
|
||||||
|
// that ends as soon as Start returns. It runs in a synctest bubble, whose clock
|
||||||
|
// is fake: time.Sleep there moves the clock on and returns at once. The server
|
||||||
|
// must work the cards out again a minute later, and no more after Stop. Each
|
||||||
|
// time it works them out, it keeps a new copy. Before the fix the end of
|
||||||
|
// Start's context stopped the goroutine that works them out.
|
||||||
|
func TestStartedServerWorksOutTheCardsUntilStop(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// The server is made outside the bubble. The streamer's metrics start a
|
||||||
|
// goroutine that never returns, and a bubble waits for each of its
|
||||||
|
// goroutines to return.
|
||||||
|
s := newServerWithTable(t, newCardsTable())
|
||||||
|
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
|
||||||
|
err := s.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Start: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// The bubble's clock moves only while each of its goroutines waits
|
||||||
|
// on the bubble, and the goroutine that serves HTTP waits on the
|
||||||
|
// network. The test sends no request, so it closes the HTTP server.
|
||||||
|
_ = s.srv.Close()
|
||||||
|
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
started := s.cards.Load()
|
||||||
|
if started == nil {
|
||||||
|
t.Fatal("the server did not work out the cards when it started")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(homeCardsRefreshInterval)
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
if s.cards.Load() == started {
|
||||||
|
t.Error("the server did not work out the cards again a minute " +
|
||||||
|
"after its start context ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Stop(t.Context())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Stop: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped := s.cards.Load()
|
||||||
|
|
||||||
|
time.Sleep(homeCardsRefreshInterval)
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
if s.cards.Load() != stopped {
|
||||||
|
t.Error("the server worked out the cards again after Stop")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// waitUntil fails the test unless done reports true within maxWait.
|
// waitUntil fails the test unless done reports true within maxWait.
|
||||||
func waitUntil(t *testing.T, maxWait time.Duration, done func() bool) {
|
func waitUntil(t *testing.T, maxWait time.Duration, done func() bool) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ type Server struct {
|
|||||||
// cards is the copy of the home page's cards that the pages serve, nil
|
// cards is the copy of the home page's cards that the pages serve, nil
|
||||||
// until the server first works them out
|
// until the server first works them out
|
||||||
cards atomic.Pointer[homeCards]
|
cards atomic.Pointer[homeCards]
|
||||||
|
|
||||||
|
// stopCards ends the goroutine that works out the cards, and
|
||||||
|
// cardsStopped is closed once it has returned. Both are nil until Start
|
||||||
|
// starts the goroutine.
|
||||||
|
stopCards context.CancelFunc
|
||||||
|
cardsStopped chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new HTTP server that reads routes from table, the routing
|
// New creates a new HTTP server that reads routes from table, the routing
|
||||||
@@ -111,7 +117,8 @@ func New(
|
|||||||
// Start listens on the configured port and serves HTTP in the background.
|
// Start listens on the configured port and serves HTTP in the background.
|
||||||
// It returns an error if it cannot listen, for example because the port is
|
// It returns an error if it cannot listen, for example because the port is
|
||||||
// already in use. ctx bounds opening the port, not serving. Once listening,
|
// already in use. ctx bounds opening the port, not serving. Once listening,
|
||||||
// the server works out the home page's cards every minute until ctx ends.
|
// the server works out the home page's cards every minute until Stop: the end
|
||||||
|
// of ctx does not stop it.
|
||||||
func (s *Server) Start(ctx context.Context) error {
|
func (s *Server) Start(ctx context.Context) error {
|
||||||
const (
|
const (
|
||||||
network = "tcp"
|
network = "tcp"
|
||||||
@@ -148,16 +155,22 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
ctx, s.stopCards = context.WithCancel(context.WithoutCancel(ctx))
|
||||||
|
s.cardsStopped = make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(s.cardsStopped)
|
||||||
|
|
||||||
s.keepHomeCardsFresh(ctx, homeCardsRefreshInterval)
|
s.keepHomeCardsFresh(ctx, homeCardsRefreshInterval)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the HTTP server. It waits, until ctx ends, for the requests in
|
// Stop stops the goroutine that works out the home page's cards, and waits
|
||||||
// progress to finish, and then closes the connections of those still running
|
// until it has returned or ctx ends. It then stops the HTTP server. It waits,
|
||||||
// and returns ctx's error.
|
// 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 {
|
func (s *Server) Stop(ctx context.Context) error {
|
||||||
if s.srv == nil {
|
if s.srv == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -165,6 +178,16 @@ func (s *Server) Stop(ctx context.Context) error {
|
|||||||
|
|
||||||
s.logger.Info("Stopping HTTP server")
|
s.logger.Info("Stopping HTTP server")
|
||||||
|
|
||||||
|
// Start returns before it starts the goroutine when it cannot listen.
|
||||||
|
if s.stopCards != nil {
|
||||||
|
s.stopCards()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.cardsStopped:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := s.srv.Shutdown(ctx)
|
err := s.srv.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Join(err, s.srv.Close())
|
return errors.Join(err, s.srv.Close())
|
||||||
|
|||||||
Reference in New Issue
Block a user