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:
2026-08-28 22:18:09 +02:00
co-authored by Claude Fable 5
commit 7219eb5a23
43 changed files with 2865 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# 08 — Graceful Shutdown
Pure SRE bread and butter: every rolling deploy, node drain, and spot
eviction ends with this code path. Interviewers love it because it chains
signals, contexts, and HTTP internals into one small program.
## The fresh-context trick (the whole exercise in one line)
```go
<-ctx.Done() // SIGTERM arrived; ctx is DEAD
shutCtx, cancel := context.WithTimeout(context.Background(), drain)
srv.Shutdown(shutCtx)
```
`Shutdown`'s context bounds *how long draining may take*. Pass the
already-cancelled signal ctx and Shutdown gives up instantly — you built
"graceful" shutdown that guillotines every in-flight request. It has to derive
from `Background()`. This inverted-context moment is the most commonly botched
line in the whole genre; the `TestDrainDeadlineExceeded`/`TestInFlight` pair
exists to catch both failure modes.
## `http.ErrServerClosed` is a success
`ListenAndServe` *always* returns non-nil. After `Shutdown` it returns
`http.ErrServerClosed`, which means "you asked me to stop and I did." Filter
it with `errors.Is`. Logging it as fatal is the "error: success" log line
every fleet has somewhere.
Also: send it through a **buffered** channel (`make(chan error, 1)`) — if you
exit `Run` via the ctx path first, an unbuffered send would strand the
ListenAndServe goroutine forever. Tiny buffer, whole leak class gone.
## What Shutdown actually does
1. Closes listeners → new connects get refused (RST).
2. Closes *idle* keep-alive connections.
3. Waits for active requests to finish, polling, until its ctx expires.
4. Returns `ctx.Err()` if the drain deadline passed with requests still going.
Note what it does **not** do: notify your load balancer. Which brings us to…
## The Kubernetes choreography (senior talking points)
SIGTERM and "stop receiving traffic" are **not ordered** the way you'd hope.
Kubelet sends SIGTERM *and* starts removing the pod from Endpoints
concurrently — traffic can arrive *after* SIGTERM for however long it takes
every kube-proxy and LB in the fleet to converge. Hence the cargo-cult that
is actually correct:
- **`preStop: sleep 5-15s`** — not superstition; it's buying time for
endpoint propagation *before* the process even sees SIGTERM.
- Fail readiness on SIGTERM, keep serving while draining.
- `terminationGracePeriodSeconds` (default 30s) must exceed
preStop + drainTimeout, or SIGKILL wins and no code of yours runs at all.
- If you're PID 1 in a container without an init, you get no default signal
handlers — `signal.NotifyContext` is doing real work there. (Related deep
cut: PID 1 doesn't reap zombies unless you make it — `tini` exists for a
reason.)
## `signal.NotifyContext` fine print
Second signal = default action (instant death). That's a *feature*: hung
drain + impatient operator's second Ctrl+C = immediate exit. Calling the
returned `stop()` after `ctx.Done()` restores default handling explicitly for
the same effect. Mentioning "first SIGTERM drains, second kills" is a nice
operational garnish.
+69
View File
@@ -0,0 +1,69 @@
// Exercise 08 — Graceful HTTP Server Shutdown
//
// ASSIGNMENT
// ----------
// Implement NewServer and Run:
//
// 1. NewServer(addr, handler) returns an *http.Server with EXPLICIT
// ReadTimeout, WriteTimeout and IdleTimeout (pick sane values; the
// zero-value server has none, which is the bug).
// 2. Run(ctx, srv, drainTimeout):
// a. Starts the server (ListenAndServe) in a goroutine.
// b. Blocks until ctx is cancelled (that's your SIGTERM signal).
// c. Then calls srv.Shutdown with a FRESH context carrying
// drainTimeout — NOT the already-cancelled ctx (why? see README).
// d. In-flight requests must be allowed to complete (up to drainTimeout);
// new connections are refused during drain.
// e. Returns nil on clean drain; returns an error if the drain deadline
// was exceeded or ListenAndServe failed for a real reason.
// (http.ErrServerClosed is NOT a real reason.)
//
// Run the tests: go test -race ./08-graceful-shutdown/
// Run the demo: go run ./08-graceful-shutdown/ (then Ctrl+C)
package main
import (
"context"
"fmt"
"net/http"
"os/signal"
"syscall"
"time"
)
// NewServer builds an http.Server with production timeouts.
// TODO: implement me.
func NewServer(addr string, h http.Handler) *http.Server {
return &http.Server{}
}
// Run serves until ctx is cancelled, then drains gracefully.
// TODO: implement me.
func Run(ctx context.Context, srv *http.Server, drainTimeout time.Duration) error {
// TODO:
// - errCh := make(chan error, 1); go srv.ListenAndServe() into it
// (why buffered? so the goroutine never leaks if we exit via ctx)
// - select on ctx.Done() vs errCh
// - on ctx.Done(): context.WithTimeout(context.Background(), drainTimeout)
// and srv.Shutdown(shutCtx)
// - filter http.ErrServerClosed with errors.Is
return nil
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
mux := http.NewServeMux()
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
fmt.Fprintln(w, "done")
})
srv := NewServer(":8080", mux)
fmt.Println("serving on :8080 — curl localhost:8080/slow, then Ctrl+C mid-request")
if err := Run(ctx, srv, 10*time.Second); err != nil {
fmt.Println("shutdown error:", err)
}
fmt.Println("bye")
}
+140
View File
@@ -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?")
}
}