diff --git a/01-url-health-checker/main.go b/01-url-health-checker/main.go index 1302ba6..8928126 100644 --- a/01-url-health-checker/main.go +++ b/01-url-health-checker/main.go @@ -19,6 +19,10 @@ package main import ( "context" "fmt" + "io" + "io/ioutil" + "net/http" + "sync" "time" ) @@ -33,20 +37,138 @@ type Result struct { // TODO: implement me. func CheckAll(ctx context.Context, urls []string, limit int) []Result { results := make([]Result, len(urls)) - // TODO: // - 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) }