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,66 @@
|
||||
# 01 — Concurrent URL Health Checker
|
||||
|
||||
The single most frequently asked Go task in SRE interviews. It's a compressed
|
||||
version of your actual job: blackbox probing with bounded fan-out.
|
||||
|
||||
## The two idioms for bounding concurrency
|
||||
|
||||
**Semaphore** — a buffered channel where the *capacity* is the limit:
|
||||
|
||||
```go
|
||||
sem := make(chan struct{}, limit)
|
||||
sem <- struct{}{} // acquire (blocks when full)
|
||||
<-sem // release
|
||||
```
|
||||
|
||||
**Worker pool** — `limit` goroutines ranging over a jobs channel. Semaphore is
|
||||
less code when each job is independent; worker pool wins when workers carry
|
||||
state (a connection, a buffer). Be able to write both and *say why you chose one*.
|
||||
|
||||
## Why `results[i]` needs no mutex
|
||||
|
||||
Each goroutine writes to a distinct index. Different elements of a slice are
|
||||
distinct memory locations, so there's no data race — the race detector agrees.
|
||||
The `wg.Wait()` creates the happens-before edge that makes all those writes
|
||||
visible to the reader. This trick ("index-addressed results") deletes an entire
|
||||
class of locking from your solution. Interviewers notice.
|
||||
|
||||
## The body-drain ritual and why it exists
|
||||
|
||||
```go
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
```
|
||||
|
||||
`http.Transport` only returns a connection to the keep-alive pool if the body
|
||||
was fully read. Close-without-drain forces the connection to be torn down → new
|
||||
TCP+TLS handshake per request → your "health checker" DDoSes your own handshake
|
||||
path. Real incidents have been caused by exactly this.
|
||||
|
||||
## Things people get wrong here
|
||||
|
||||
- **`http.DefaultClient` has no timeout.** None. A hung server hangs your
|
||||
checker forever. Always construct your own client, or use
|
||||
`http.NewRequestWithContext`.
|
||||
- **A 500 response is not an `error`.** Transport errors (DNS, conn refused,
|
||||
timeout) populate `err`; HTTP status is data. Conflating them is a classic
|
||||
monitoring bug (your alerting says "down" when it's actually "up and angry").
|
||||
- **Goroutine leak on cancellation**: if you acquire the semaphore with a bare
|
||||
`sem <- struct{}{}` and ctx dies, goroutines queue up forever. Acquire inside
|
||||
a `select` with `ctx.Done()`.
|
||||
|
||||
## Level-up flex
|
||||
|
||||
`golang.org/x/sync/errgroup` collapses the whole choreography:
|
||||
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(limit)
|
||||
for i, u := range urls {
|
||||
g.Go(func() error { results[i] = check(ctx, u); return nil })
|
||||
}
|
||||
g.Wait()
|
||||
```
|
||||
|
||||
Mention it exists, then implement the raw version anyway — they want to see
|
||||
you handle the primitives.
|
||||
Reference in New Issue
Block a user