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
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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?")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user