package main import ( "context" "errors" "net" "net/http" "sync/atomic" "testing" "time" ) func TestShutdownKeepsDependenciesUntilHTTPReturns(t *testing.T) { entered := make(chan struct{}) release := make(chan struct{}) workerStopped := make(chan struct{}) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { close(entered); <-release; w.WriteHeader(200) })} ready := new(atomic.Bool) ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- serve(ctx, listener, server, ready, []component{{"worker", func(ctx context.Context) error { <-ctx.Done(); close(workerStopped); return nil }}}, component{"events", func(ctx context.Context) error { <-ctx.Done() select { case <-workerStopped: return nil default: return errors.New("consumer stopped before worker") } }}, time.Second) }() response := make(chan error, 1) go func() { r, e := http.Get("http://" + listener.Addr().String()) if e == nil { r.Body.Close() } response <- e }() select { case <-entered: case <-time.After(time.Second): t.Fatal("HTTP not entered") } cancel() select { case <-workerStopped: t.Fatal("worker stopped during HTTP transaction") case <-time.After(20 * time.Millisecond): } if ready.Load() { t.Fatal("readiness remained true during shutdown") } close(release) if err = <-response; err != nil { t.Fatal(err) } select { case err = <-done: if err != nil { t.Fatal(err) } case <-time.After(3 * time.Second): t.Fatal("shutdown blocked") } } func TestBackgroundFailureStopsServiceAndPeers(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() boom := errors.New("fatal worker") stopped := make(chan struct{}) result := serve(context.Background(), listener, &http.Server{}, new(atomic.Bool), []component{{"worker", func(context.Context) error { return boom }}}, component{"consumer", func(ctx context.Context) error { <-ctx.Done(); close(stopped); return nil }}, time.Second) if !errors.Is(result, boom) { t.Fatal(result) } select { case <-stopped: default: t.Fatal("consumer leaked") } }