Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
176 lines
6.2 KiB
Go
176 lines
6.2 KiB
Go
// Exercise 01 — Concurrent URL Health Checker
|
|
//
|
|
// ASSIGNMENT
|
|
// ----------
|
|
// Implement CheckAll so that it:
|
|
//
|
|
// 1. Checks every URL in `urls` with an HTTP GET.
|
|
// 2. Runs AT MOST `limit` checks concurrently (bounded concurrency).
|
|
// 3. Uses a per-request timeout of 5s AND respects the global `ctx`
|
|
// (if ctx is cancelled, pending/unstarted checks must return ctx.Err()).
|
|
// 4. Returns one Result per URL, at the SAME index as the input URL.
|
|
// 5. Reuses a single http.Client and properly drains+closes response bodies
|
|
// so connections are reused.
|
|
//
|
|
// Run the tests: go test -race ./01-url-health-checker/
|
|
// Run the demo: go run ./01-url-health-checker/
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Result struct {
|
|
URL string
|
|
Status int
|
|
Latency time.Duration
|
|
Err error
|
|
}
|
|
|
|
// CheckAll checks all URLs with at most `limit` in flight at once.
|
|
// TODO: implement me.
|
|
func CheckAll(ctx context.Context, urls []string, limit int) []Result {
|
|
results := make([]Result, len(urls))
|
|
// - semaphore (buffered channel) or worker pool for the concurrency limit
|
|
sem := make(chan struct{}, limit)
|
|
// - sync.WaitGroup to wait for completion
|
|
var wg sync.WaitGroup
|
|
// - http.NewRequestWithContext for cancellation
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
for i, u := range urls {
|
|
wg.Add(1)
|
|
go func(i int, u string) {
|
|
defer wg.Done()
|
|
// wait for semaphore or ctx cancellation
|
|
select {
|
|
case sem <- struct{}{}:
|
|
defer func() { <-sem }()
|
|
case <-ctx.Done():
|
|
results[i] = Result{URL: u, Err: ctx.Err()}
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
results[i] = Result{URL: u, Err: err}
|
|
return
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
results[i] = Result{URL: u, Err: err, Latency: time.Since(start)}
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
io.Copy(ioutil.Discard, resp.Body)
|
|
results[i] = Result{URL: u, Err: err, Status: resp.StatusCode, Latency: time.Since(start)}
|
|
}(i, u)
|
|
}
|
|
|
|
// - write results[i] from goroutine i (why is this safe without a mutex?)
|
|
_ = ctx
|
|
_ = limit
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
func main() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
// Pick ONE of the url sets below (uncomment it, comment the others).
|
|
// Tip: run with a bigger `limit` and compare total wall-clock time.
|
|
|
|
// Option A — the original mix: OK, HTTP 500, DNS failure.
|
|
urls := []string{"https://example.com", "https://httpbin.org/status/500", "https://nope.invalid"}
|
|
|
|
//region Not-selected-options
|
|
|
|
// Option B — all succeed (2xx). Good for measuring how `limit` affects
|
|
// total time: with limit=1 it's the sum of latencies, with limit=8 ~max.
|
|
// urls := []string{
|
|
// "https://example.com",
|
|
// "https://www.google.com",
|
|
// "https://www.cloudflare.com",
|
|
// "https://github.com",
|
|
// "https://go.dev",
|
|
// "https://pkg.go.dev",
|
|
// "https://httpbin.org/status/200",
|
|
// "https://httpbin.org/get",
|
|
// }
|
|
|
|
// Option C — HTTP-level failures (connection succeeds, status is bad).
|
|
// Err should be nil for these; the caller decides what a "healthy" status is.
|
|
// urls := []string{
|
|
// "https://httpbin.org/status/200", // control
|
|
// "https://httpbin.org/status/301", // redirect — client follows it by default, so you'll see the final status
|
|
// "https://httpbin.org/status/401",
|
|
// "https://httpbin.org/status/403",
|
|
// "https://httpbin.org/status/404",
|
|
// "https://httpbin.org/status/429",
|
|
// "https://httpbin.org/status/500",
|
|
// "https://httpbin.org/status/502",
|
|
// "https://httpbin.org/status/503",
|
|
// "https://example.com/definitely-not-here", // real site, 404
|
|
// }
|
|
|
|
// Option D — network/transport failures (Err != nil, Status == 0).
|
|
// Each fails for a DIFFERENT reason — print r.Err and read the message.
|
|
// urls := []string{
|
|
// "https://example.com", // control
|
|
// "https://nope.invalid", // DNS: .invalid TLD never resolves
|
|
// "https://this-host-does-not-exist-12345.com", // DNS: NXDOMAIN
|
|
// "http://localhost:1", // TCP: connection refused (nothing listens on port 1)
|
|
// "http://10.255.255.1", // TCP: unroutable address → connect timeout (hits the 5s per-request timeout)
|
|
// "https://expired.badssl.com", // TLS: expired certificate
|
|
// "https://self-signed.badssl.com", // TLS: self-signed certificate
|
|
// "https://wrong.host.badssl.com", // TLS: hostname mismatch
|
|
// "https://untrusted-root.badssl.com", // TLS: unknown CA
|
|
// "ftp://example.com", // unsupported protocol scheme
|
|
// "not a url at all", // request creation fails (invalid URL)
|
|
// }
|
|
|
|
// Option E — slow responses: exercises the 5s per-request timeout AND the
|
|
// global ctx. /delay/N sleeps N seconds server-side before replying.
|
|
// With limit=2 and a 10s ctx, watch which ones finish, which time out
|
|
// per-request, and which never start and return ctx.Err().
|
|
// urls := []string{
|
|
// "https://httpbin.org/delay/1", // OK
|
|
// "https://httpbin.org/delay/3", // OK
|
|
// "https://httpbin.org/delay/8", // exceeds 5s per-request timeout
|
|
// "https://httpbin.org/delay/8", // exceeds 5s per-request timeout
|
|
// "https://httpbin.org/delay/1", // may hit global ctx deadline before it even starts
|
|
// "https://httpbin.org/delay/1",
|
|
// "https://httpbin.org/delay/1",
|
|
// }
|
|
//endregion
|
|
|
|
// Option F — everything at once, larger list. Set limit to 1, 4, 16 and
|
|
// compare; check that the order of results still matches the input order.
|
|
urls = []string{
|
|
"https://example.com",
|
|
"https://go.dev",
|
|
"https://httpbin.org/status/200",
|
|
"https://httpbin.org/status/404",
|
|
"https://httpbin.org/status/500",
|
|
"https://httpbin.org/status/503",
|
|
"https://httpbin.org/delay/2",
|
|
"https://httpbin.org/delay/7",
|
|
"https://nope.invalid",
|
|
"http://localhost:1",
|
|
"https://expired.badssl.com",
|
|
"https://self-signed.badssl.com",
|
|
"https://www.cloudflare.com",
|
|
"https://github.com",
|
|
}
|
|
for _, r := range CheckAll(ctx, urls, 2) {
|
|
fmt.Printf("%-40s status=%d latency=%v err=%v\n", r.URL, r.Status, r.Latency, r.Err)
|
|
}
|
|
}
|