Files
kacerrandClaude Fable 5 7219eb5a23 Initial commit: Go SRE interview exercises
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
2026-08-28 22:18:09 +02:00

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?")
}
}