Files
http-latency-prober/docs/plans/2026-07-01-01-23-concurrency.md
Jan Novak 9a69ba946f 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>
2026-07-01 01:33:03 +02:00

141 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: Concurrency (Go, Step 7)
## Context
Large probe runs are slow. The example matrices in `docs/custom-usage-examples.md`
are 15 URLs × 10 samples = 150 sequential HTTP requests, dominated by summed
network round-trips, so a single run takes minutes. Execution is currently
strictly sequential: `run()` loops over URLs and, for each, `runSamples()` loops
`-n` times calling `probe.Measure()`.
We want to cut wall-clock time by probing multiple **URLs** in parallel, while
keeping measurement honest. Concurrency and latency accuracy are in tension:
firing many requests at once makes them contend for the local NIC/CPU/resolver
and inflates timings. The decision (confirmed with the user) is to parallelize
**only across URLs** — the samples of a single URL stay sequential so each URL's
min/avg/max remains a clean, self-consistent measurement.
Confirmed decisions:
- **Granularity:** across URLs only; samples sequential per URL. Unit of work =
one URL with its full `-n` sample set.
- **Default concurrency:** auto when `-c` is not given → `min(numURLs, 8)`.
`-c 1` forces fully sequential (most accurate); `-c N` sets an explicit cap.
- **Output:** buffered and printed in original input order (deterministic;
identical layout to today, for both text and JSON).
## Files to modify
### `go/main.go` — core change
1. **New flag** (mirror the `-n`/`--count` pattern at lines 7879):
```go
conc := fs.Int("concurrency", 0, "max URLs probed in parallel (0 = auto)")
fs.IntVar(conc, "c", 0, "max URLs probed in parallel (shorthand)")
```
Add `-c, --concurrency` to `usageText` (around lines 2227).
2. **Resolve effective concurrency** after the empty-URL check (line 96):
```go
const defaultMaxConc = 8
workers := *conc
if workers <= 0 {
workers = min(len(urls), defaultMaxConc) // auto
}
workers = min(workers, len(urls)) // never exceed work units
if workers < 1 {
workers = 1
}
```
(Go 1.26 has builtin `min`.)
3. **Split `run()` into measure phase (concurrent) + render phase (sequential).**
Introduce a small result holder:
```go
type urlResult struct {
url string
succeeded, failed []probe.Result
}
```
Measure phase — bounded worker pool via a semaphore channel + `sync.WaitGroup`,
each goroutine writing only its own preallocated slot (no shared mutable state,
so no mutex / data race):
```go
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{rawURL, s, f}
}(i, rawURL)
}
wg.Wait()
```
Render phase — the existing per-URL loop body (lines 102127), unchanged in
behaviour, now reading from `results[i]` instead of calling `runSamples`
inline. `worstCode` accumulation, JSON entry building, and ordered text
printing all stay identical. JSON encode block (129136) and `return worstCode`
unchanged.
`runSamples`, `printURL`, `buildJSONEntry`, and all output helpers stay as-is.
Concurrency safety: `probe.Measure` uses `http.DefaultClient`, which is safe for
concurrent use; distinct URLs hit distinct hosts so the shared transport pool is
not a correctness concern. Output order is deterministic because rendering reads
the indexed `results` slice after `wg.Wait()`.
### `go/run_test.go` — add coverage
- `TestRunConcurrentOrder`: spin up several `httptest` servers (reuse existing
`statusSrv` helper), pass them with `-c` larger than 1, and assert the printed
blocks appear in **input order** and the exit code matches the sequential run.
- Add a mixed success/failure case under high `-c` to confirm `worstCode`
aggregation is unaffected by parallelism.
### `Makefile` + `docs/usage/makefile.md`
- Add `go-test-race`: `go -C $(GO_DIR) test -race ./...` (with `##` help comment,
added to `.PHONY`). Document it in the Go targets table. Leave `check` as-is
(race run kept as an explicit opt-in target).
### Docs & conventions
- `docs/usage/step-7-concurrency.md` (new): explain the `-c`/`--concurrency`
flag, the auto default (`min(numURLs, 8)`), the across-URLs-only model, the
accuracy trade-off (use `-c 1` for the most precise numbers), and a worked
example with before/after timing.
- `README.md`: add `-c, --concurrency` to the Usage flags block; add **Step 7 —
Concurrency** row (✅) to the Go roadmap table.
- `CHANGELOG.md`: new top entry, `2026-07-01 01:23 — Concurrency (Go, Step 7)`.
- `docs/custom-usage-examples.md`: note near the top that runs now parallelize
across URLs by default and that `-c 1` restores serial timing; revise the
Example 2/3/9 runtime estimates to reflect the speedup.
- `docs/plans/2026-07-01-01-23-concurrency.md`: copy of this plan (per CLAUDE.md).
## Verification
```sh
make build
# Auto concurrency (defaults to min(numURLs,8)) — should be much faster than before:
time ./go/latprobe -n 5 https://www.google.com https://www.bbc.co.uk \
https://www.lemonde.fr https://www.spiegel.de https://www.abc.net.au
# Forced serial for comparison / accuracy:
time ./go/latprobe -c 1 -n 5 https://www.google.com https://www.bbc.co.uk \
https://www.lemonde.fr https://www.spiegel.de https://www.abc.net.au
# Output order is deterministic regardless of -c:
./go/latprobe -c 8 https://www.google.com https://www.bbc.co.uk https://www.abc.net.au
# JSON array order also matches input order:
./go/latprobe --json -c 8 https://www.google.com https://www.bbc.co.uk | jq '.[].url'
make test # full suite incl. new ordering tests
make go-test-race # data-race detector must report clean
make check # fmt + vet + test gate
```
Confirm: identical output (modulo timing numbers) between `-c 1` and default for
the same URL set; exit codes unchanged; `-race` clean.