proxy: snapshot in-flight counts before sorting (least_conn selection O(n)) #124

Merged
benvin merged 1 commits from benvin/leastconn-snapshot-opt into master 2026-08-13 19:55:21 +10:00
Member

Why

least_conn mirror selection (baseURLAttemptOrder) scaled super-linearly. After rotating the pool by the round-robin cursor it sort.SliceStabled with a comparator that called inflightCounter on every comparison — and each call did a remoteName+"\x00"+url concat plus a sync.Map LoadOrStore with a speculative new(atomic.Int64). So each selection cost O(n·log n) map lookups + allocations, all on the cache-miss/upstream path.

How

Snapshot each mirror's in-flight count once, then sort the snapshot — O(n) map loads, zero comparator allocations.

  • Add read-only inflightCount(name, url) int64: plain sync.Map Load, returns 0 when the gauge is absent (no LoadOrStore, no speculative allocation).
  • least_conn branch builds a {url, count} snapshot via one inflightCount per rotated URL, sort.SliceStable by count ascending, then extracts the URLs.
  • beginAttempt/endAttempt keep the create-on-write inflightCounter path — they legitimately need to create the gauge.

Numbers (BenchmarkBaseURLAttemptOrder_LeastConn, Ryzen 7 4700U, best of 3)

pool before ns/op after ns/op before allocs after allocs before B/op after B/op
3 ~2516 ~1492 22 8 474 296
8 ~14647 ~3319 131–132 8 2688 568

Allocs are now constant regardless of pool size; pool-8 is ~4.8x faster with ~16x fewer allocations.

Behavior

Unchanged: least-loaded first, RR rotation as the stable tie-break, round_robin and single-URL paths untouched. Pure internal optimization — no API/schema/DB change. Added a multi-mirror tie-break test asserting all-equal load yields the RR rotation; make test (-race) green, vet/fmt clean.

## Why `least_conn` mirror selection (`baseURLAttemptOrder`) scaled super-linearly. After rotating the pool by the round-robin cursor it `sort.SliceStable`d with a comparator that called `inflightCounter` on **every comparison** — and each call did a `remoteName+"\x00"+url` concat plus a `sync.Map` `LoadOrStore` with a speculative `new(atomic.Int64)`. So each selection cost O(n·log n) map lookups + allocations, all on the cache-miss/upstream path. ## How Snapshot each mirror's in-flight count **once**, then sort the snapshot — O(n) map loads, zero comparator allocations. - Add read-only `inflightCount(name, url) int64`: plain `sync.Map` `Load`, returns 0 when the gauge is absent (no `LoadOrStore`, no speculative allocation). - `least_conn` branch builds a `{url, count}` snapshot via one `inflightCount` per rotated URL, `sort.SliceStable` by `count` ascending, then extracts the URLs. - `beginAttempt`/`endAttempt` keep the create-on-write `inflightCounter` path — they legitimately need to create the gauge. ## Numbers (`BenchmarkBaseURLAttemptOrder_LeastConn`, Ryzen 7 4700U, best of 3) | pool | before ns/op | after ns/op | before allocs | after allocs | before B/op | after B/op | |------|-------------:|------------:|--------------:|-------------:|------------:|-----------:| | 3 | ~2516 | ~1492 | 22 | 8 | 474 | 296 | | 8 | ~14647 | ~3319 | 131–132 | 8 | 2688 | 568 | Allocs are now **constant** regardless of pool size; pool-8 is ~4.8x faster with ~16x fewer allocations. ## Behavior Unchanged: least-loaded first, RR rotation as the stable tie-break, `round_robin` and single-URL paths untouched. Pure internal optimization — no API/schema/DB change. Added a multi-mirror tie-break test asserting all-equal load yields the RR rotation; `make test` (`-race`) green, vet/fmt clean.
unkin-agent added 1 commit 2026-08-13 17:47:38 +10:00
proxy: snapshot in-flight counts before sorting (least_conn selection O(n))
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
6ac1f4e5a4
least_conn mirror selection stable-sorted the RR-rotated pool with a
comparator that called inflightCounter on every comparison. Each call did a
remoteName+"\x00"+url concat plus a sync.Map LoadOrStore with a speculative
new(atomic.Int64), so selection cost O(n log n) map lookups and allocations
per request on the cache-miss path.

Snapshot each mirror's in-flight count exactly once, then sort the snapshot by
plain int, making selection O(n) map loads with zero comparator allocations:

- Add read-only inflightCount(name, url) int64: plain sync.Map Load, returns 0
  when the counter is absent (no LoadOrStore, no speculative allocation).
- least_conn branch builds a {url, count} snapshot via one inflightCount per
  rotated URL, sort.SliceStable by count ascending, then extracts the URLs.
- beginAttempt/endAttempt keep the create-on-write inflightCounter path; they
  legitimately need to create the gauge.

Behavior is unchanged: least-loaded first, RR rotation as the stable tie-break,
round_robin and single-URL paths untouched. Added a multi-mirror tie-break test
asserting all-equal load yields the RR rotation.
benvin merged commit 734195e54e into master 2026-08-13 19:55:21 +10:00
benvin deleted branch benvin/leastconn-snapshot-opt 2026-08-13 19:55:21 +10:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkin/artifactapi#124