Add URL-level concurrency (-c flag, Step 7)
Probe multiple URLs in parallel with a bounded worker pool; the N samples of each URL remain sequential to preserve accurate min/avg/max statistics. Default auto-concurrency is min(numURLs, 8); -c 1 restores serial mode. Output is always buffered and printed in original input order. Verified clean with go test -race. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
48
go/main.go
48
go/main.go
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"latprobe/internal/probe"
|
||||
@@ -20,11 +21,12 @@ Usage:
|
||||
latprobe [flags] <url> [url ...]
|
||||
|
||||
Flags:
|
||||
-n, --count int Number of requests per URL (default 1)
|
||||
--timeout duration Request timeout, e.g. 10s, 500ms (default 10s)
|
||||
--fail Exit non-zero on HTTP status >= 400 (exit code 6)
|
||||
--json Output results as JSON instead of text
|
||||
-h, --help Show this help
|
||||
-n, --count int Number of requests per URL (default 1)
|
||||
-c, --concurrency int Max URLs probed in parallel, 0 = auto (default min(numURLs,8))
|
||||
--timeout duration Request timeout, e.g. 10s, 500ms (default 10s)
|
||||
--fail Exit non-zero on HTTP status >= 400 (exit code 6)
|
||||
--json Output results as JSON instead of text
|
||||
-h, --help Show this help
|
||||
|
||||
Exit codes:
|
||||
0 All probes succeeded
|
||||
@@ -77,6 +79,8 @@ func run(args []string, stdout, stderr io.Writer) int {
|
||||
|
||||
count := fs.Int("count", 1, "number of requests per URL")
|
||||
fs.IntVar(count, "n", 1, "number of requests per URL (shorthand)")
|
||||
conc := fs.Int("concurrency", 0, "max URLs probed in parallel (0 = auto)")
|
||||
fs.IntVar(conc, "c", 0, "max URLs probed in parallel (shorthand)")
|
||||
timeout := fs.Duration("timeout", 10*time.Second, "request timeout per sample")
|
||||
fail := fs.Bool("fail", false, "exit non-zero on HTTP status >= 400")
|
||||
jsonOut := fs.Bool("json", false, "output results as JSON instead of text")
|
||||
@@ -96,11 +100,43 @@ func run(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
opts := probe.Options{Timeout: *timeout}
|
||||
|
||||
// Resolve effective worker count.
|
||||
const defaultMaxConc = 8
|
||||
workers := *conc
|
||||
if workers <= 0 {
|
||||
workers = min(len(urls), defaultMaxConc) // auto
|
||||
}
|
||||
workers = min(workers, len(urls)) // never more goroutines than work units
|
||||
workers = max(workers, 1)
|
||||
|
||||
// ── Measure phase (concurrent) ────────────────────────────────────────────
|
||||
// Each goroutine writes only its own indexed slot — no shared mutable state.
|
||||
type urlResult struct {
|
||||
succeeded, failed []probe.Result
|
||||
}
|
||||
results := make([]urlResult, len(urls))
|
||||
sem := make(chan struct{}, workers)
|
||||
var wg sync.WaitGroup
|
||||
for i, rawURL := range urls {
|
||||
wg.Add(1)
|
||||
go func(i int, rawURL string) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
s, f := runSamples(rawURL, *count, opts)
|
||||
results[i] = urlResult{s, f}
|
||||
}(i, rawURL)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// ── Render phase (sequential, input order) ────────────────────────────────
|
||||
worstCode := exitOK
|
||||
var jsonEntries []jsonEntry
|
||||
|
||||
for i, rawURL := range urls {
|
||||
succeeded, failed := runSamples(rawURL, *count, opts)
|
||||
succeeded := results[i].succeeded
|
||||
failed := results[i].failed
|
||||
|
||||
for _, r := range failed {
|
||||
if c := failPhaseCode(r.FailPhase); c > worstCode {
|
||||
|
||||
Reference in New Issue
Block a user