Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testClient disables keep-alives: the default transport may speculatively
|
|
// dial a second connection and park it in StateNew without ever sending a
|
|
// request — and Server.Shutdown waits for StateNew conns, deadlocking the
|
|
// drain. A fun wart, but not the one this exercise is about.
|
|
var testClient = &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
|
|
|
|
func freeAddr(t *testing.T) string {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addr := l.Addr().String()
|
|
l.Close()
|
|
return addr
|
|
}
|
|
|
|
func waitReady(t *testing.T, url string) {
|
|
t.Helper()
|
|
for i := 0; i < 100; i++ {
|
|
if resp, err := testClient.Get(url); err == nil {
|
|
resp.Body.Close()
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("server never became ready")
|
|
}
|
|
|
|
func TestServerHasTimeouts(t *testing.T) {
|
|
srv := NewServer(":0", http.NewServeMux())
|
|
if srv.ReadTimeout == 0 || srv.WriteTimeout == 0 || srv.IdleTimeout == 0 {
|
|
t.Fatalf("server must set Read/Write/Idle timeouts explicitly (got %v/%v/%v) — zero means unlimited",
|
|
srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout)
|
|
}
|
|
if srv.Addr != ":0" || srv.Handler == nil {
|
|
t.Fatal("Addr/Handler not wired through")
|
|
}
|
|
}
|
|
|
|
func TestServesAndStopsOnCancel(t *testing.T) {
|
|
addr := freeAddr(t)
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- Run(ctx, NewServer(addr, mux), 5*time.Second) }()
|
|
|
|
waitReady(t, "http://"+addr+"/ok")
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("clean shutdown should return nil, got %v (leaking http.ErrServerClosed?)", err)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Run did not return after ctx cancel")
|
|
}
|
|
}
|
|
|
|
func TestInFlightRequestCompletes(t *testing.T) {
|
|
addr := freeAddr(t)
|
|
started := make(chan struct{})
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
|
|
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
|
|
close(started)
|
|
time.Sleep(500 * time.Millisecond)
|
|
io.WriteString(w, "slow-done")
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
runDone := make(chan error, 1)
|
|
go func() { runDone <- Run(ctx, NewServer(addr, mux), 5*time.Second) }()
|
|
waitReady(t, "http://"+addr+"/ok")
|
|
|
|
reqDone := make(chan string, 1)
|
|
go func() {
|
|
resp, err := testClient.Get("http://" + addr + "/slow")
|
|
if err != nil {
|
|
reqDone <- "error: " + err.Error()
|
|
return
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
reqDone <- string(b)
|
|
}()
|
|
|
|
<-started // request is in flight
|
|
cancel() // SIGTERM arrives mid-request
|
|
|
|
if body := <-reqDone; body != "slow-done" {
|
|
t.Fatalf("in-flight request was killed instead of drained: got %q", body)
|
|
}
|
|
if err := <-runDone; err != nil {
|
|
t.Fatalf("drain within deadline should return nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDrainDeadlineExceeded(t *testing.T) {
|
|
addr := freeAddr(t)
|
|
started := make(chan struct{})
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
|
|
mux.HandleFunc("/hang", func(w http.ResponseWriter, r *http.Request) {
|
|
close(started)
|
|
time.Sleep(5 * time.Second)
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
runDone := make(chan error, 1)
|
|
go func() { runDone <- Run(ctx, NewServer(addr, mux), 200*time.Millisecond) }()
|
|
waitReady(t, "http://"+addr+"/ok")
|
|
|
|
go func() { testClient.Get("http://" + addr + "/hang") }()
|
|
<-started
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-runDone:
|
|
if err == nil {
|
|
t.Fatal("want error when drain deadline exceeded, got nil")
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Run hung past the drain deadline — Shutdown ctx wired up wrong?")
|
|
}
|
|
}
|