01-url-health-checker: add URL option sets, start CheckAll implementation

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 23:49:38 +02:00
co-authored by Claude Fable 5
parent 9df64837dd
commit 8dd8381b8c
+123 -1
View File
@@ -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)
}