Skip to content

perf(resolver): allocation-free resolver selection in ParallelBestResolver - #2150

Merged
0xERR0R merged 2 commits into
mainfrom
perf/parallel-best-resolver-selection
Jun 30, 2026
Merged

0xERR0R merged 2 commits into
mainfrom
perf/parallel-best-resolver-selection

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jun 30, 2026

Copy link
Copy Markdown
Owner

What

ParallelBestResolver is the default upstream strategy. On every cache-miss request it rebuilt a weightedrand.Chooser — and via pickRandom, it did so twice per request. Each build allocated a choices slice, sorted it, and allocated a totals slice: ~10 allocations / 328 B per request in the common 2-upstream case.

This replaces the Chooser with single-pass weighted reservoir sampling (k=1): each resolver is kept with probability weight / runningTotal, which leaves every resolver selected with probability proportional to its weight once the loop completes. Same weighting semantics (a resolver that errored recently gets a reduced weight, recovering to full weight an hour after its last error), same statistical distribution, but zero allocations in one O(n) pass.

Additionally:

  • Skip context.WithCancel for the random strategy, which queries a single resolver per attempt and therefore has no losing race to abort.
  • Drops the github.com/mroth/weightedrand/v2 dependency (no longer used anywhere).

This addresses Tier 4a of the hot-path performance findings (upstream / cache-miss path).

Benchmarks

benchstat (n=6), per-request resolver-selection cost:

benchmark before after
pickRandom/upstreams=2 (default parallel_best) 717ns, 328 B, 10 allocs 205ns, 0 B, 0 allocs (−71%)
weightedRandom/upstreams=2 (random strategy) 451ns, 200 B, 6 allocs 125ns, 0 B, 0 allocs (−72%)
pickRandom/upstreams=5 1336ns, 544 B, 12 allocs 585ns, 0 B, 0 allocs (−56%)
weightedRandom/upstreams=5 755ns, 280 B, 6 allocs 370ns, 0 B, 0 allocs (−51%)

Tests

  • Existing statistical distribution tests (equal-weight → uniform; recent-error → reduced weight) pass unchanged — they are the correctness guard for the new sampler.
  • New BenchmarkParallelBestResolverSelection added (per the tier's "add a bench if missing" guidance).
  • go test ./resolver/ (full package) and go test -race on the parallel/random paths pass.
  • make lint clean.

Not included

  • Tier-4a item 2 (pooling the requestResponse channel) is deliberately skipped: a losing goroutine still sends into the buffered channel after Resolve returns, so recycling it would risk a stale response landing in a concurrent request's channel. Making it safe would require draining the slow loser before returning, negating the "fastest wins" benefit.

…lelBestResolver

The default upstream strategy rebuilt a weightedrand.Chooser on every
cache-miss request — twice, via pickRandom. Each build allocated a
choices slice, sorted it, and allocated a totals slice, for ~10 allocs
(328 B) per request in the common 2-upstream case.

Replace the Chooser with single-pass weighted reservoir sampling (k=1):
each resolver is kept with probability weight/runningTotal, leaving every
resolver selected proportional to its weight once the loop completes.
Same weighting (recent errors reduce a resolver's weight, recovering over
an hour), same distribution, but zero allocations and one O(n) pass.

Also skip context.WithCancel for the random strategy, which queries a
single resolver per attempt and so has no losing race to abort.

This drops the github.com/mroth/weightedrand/v2 dependency.

benchstat (n=6), selection cost per request:

  pickRandom/upstreams=2      717ns 328B 10 allocs -> 205ns 0B 0 allocs
  weightedRandom/upstreams=2  451ns 200B  6 allocs -> 125ns 0B 0 allocs
  pickRandom/upstreams=5     1336ns 544B 12 allocs -> 585ns 0B 0 allocs
  weightedRandom/upstreams=5  755ns 280B  6 allocs -> 370ns 0B 0 allocs
Copilot AI review requested due to automatic review settings June 30, 2026 07:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Jun 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.90%. Comparing base (223df0c) to head (73413cc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2150      +/-   ##
==========================================
+ Coverage   87.86%   87.90%   +0.04%     
==========================================
  Files         124      124              
  Lines        9813     9816       +3     
==========================================
+ Hits         8622     8629       +7     
+ Misses        926      924       -2     
+ Partials      265      263       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@0xERR0R 0xERR0R changed the title perf(resolver): Tier 4a — allocation-free resolver selection in ParallelBestResolver perf(resolver): allocation-free resolver selection in ParallelBestResolver Jun 30, 2026
@0xERR0R 0xERR0R added the 🧰 technical debts Technical debts, refactoring label Jun 30, 2026
@0xERR0R 0xERR0R added this to the v0.33.0 milestone Jun 30, 2026
@0xERR0R
0xERR0R enabled auto-merge (squash) June 30, 2026 07:47
@0xERR0R
0xERR0R merged commit b73422e into main Jun 30, 2026
22 checks passed
@0xERR0R
0xERR0R deleted the perf/parallel-best-resolver-selection branch June 30, 2026 07:52
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jul 2, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.32.1` → `v0.33.0` |

---

### Release Notes

<details>
<summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary>

### [`v0.33.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.33.0)

[Compare Source](0xERR0R/blocky@v0.32.1...v0.33.0)

#### Changelog

##### Features

- [`552eda5`](0xERR0R/blocky@552eda5): feat(cli): add 'blocky stats' command ([#&#8203;2138](0xERR0R/blocky#2138)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e9deb53`](0xERR0R/blocky@e9deb53): feat(queryLog): add dnstap as a query log destination ([#&#8203;2144](0xERR0R/blocky#2144)) ([@&#8203;rtmongold](https://github.com/rtmongold))

##### Bug fixes

- [`a191ad2`](0xERR0R/blocky@a191ad2): fix(dnssec): propagate Indeterminate (not Bogus) for unreachable chain of trust ([#&#8203;2120](0xERR0R/blocky#2120)) ([#&#8203;2139](0xERR0R/blocky#2139)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`4b524e8`](0xERR0R/blocky@4b524e8): fix(ecs): apply useAsClient above cache and client-name lookup ([#&#8203;2140](0xERR0R/blocky#2140)) ([#&#8203;2141](0xERR0R/blocky#2141)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Build and dependencies

- [`afea452`](0xERR0R/blocky@afea452): build(deps): bump github.com/breml/rootcerts from 0.3.5 to 0.3.6 ([#&#8203;2133](0xERR0R/blocky#2133)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`247a3c1`](0xERR0R/blocky@247a3c1): build(deps): bump github.com/jedib0t/go-pretty/v6 from 6.6.7 to 6.8.1 ([#&#8203;2145](0xERR0R/blocky#2145)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`2aa728d`](0xERR0R/blocky@2aa728d): build(deps): bump github.com/oapi-codegen/runtime from 1.4.1 to 1.4.2 ([#&#8203;2134](0xERR0R/blocky#2134)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`3f3f7d9`](0xERR0R/blocky@3f3f7d9): build(deps): bump github.com/onsi/ginkgo/v2 from 2.31.0 to 2.32.0 ([#&#8203;2135](0xERR0R/blocky#2135)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`2d0e6f2`](0xERR0R/blocky@2d0e6f2): build(deps): bump github.com/onsi/gomega from 1.42.0 to 1.42.1 ([#&#8203;2137](0xERR0R/blocky#2137)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`61d9a98`](0xERR0R/blocky@61d9a98): build(deps): bump github.com/testcontainers/testcontainers-go/modules/mariadb from 0.42.0 to 0.43.0 ([#&#8203;2131](0xERR0R/blocky#2131)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`cf865a6`](0xERR0R/blocky@cf865a6): build(deps): bump github.com/testcontainers/testcontainers-go/modules/postgres from 0.42.0 to 0.43.0 ([#&#8203;2130](0xERR0R/blocky#2130)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`e3c30dc`](0xERR0R/blocky@e3c30dc): build(deps): bump github.com/testcontainers/testcontainers-go/modules/redis from 0.42.0 to 0.43.0 ([#&#8203;2129](0xERR0R/blocky#2129)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`a58f1c8`](0xERR0R/blocky@a58f1c8): build(deps): bump google.golang.org/protobuf from 1.36.10 to 1.36.11 ([#&#8203;2148](0xERR0R/blocky#2148)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`6df83cd`](0xERR0R/blocky@6df83cd): build(deps): bump gorm.io/gorm from 1.31.1 to 1.31.2 ([#&#8203;2147](0xERR0R/blocky#2147)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])

##### Misc

- [`6da7ce1`](0xERR0R/blocky@6da7ce1): perf(blocking): Tier 2 — lock-free grouped cache, cheapest-first lookup, client-name indexing ([#&#8203;2146](0xERR0R/blocky#2146)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`302ca65`](0xERR0R/blocky@302ca65): perf(log): cut per-request logger allocations on the resolver hot path ([#&#8203;2142](0xERR0R/blocky#2142)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`223df0c`](0xERR0R/blocky@223df0c): perf(queryLog): skip per-request LogEntry build when off or ignored ([#&#8203;2149](0xERR0R/blocky#2149)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`b73422e`](0xERR0R/blocky@b73422e): perf(resolver): allocation-free resolver selection in ParallelBestResolver ([#&#8203;2150](0xERR0R/blocky#2150)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

***

#### ❤️ Support Blocky

Blocky is free and open source, developed in my spare time — no telemetry, no ads, no hidden
filtering. If it's useful to you, please consider supporting its continued development:

[GitHub Sponsors](https://github.com/sponsors/0xERR0R) ·
[thanks.dev](https://thanks.dev/u/gh/0xERR0R) ·
[Liberapay](https://liberapay.com/spx01) ·
[Ko-fi](https://ko-fi.com/0xerr0r) ·
[PayPal](https://paypal.me/spx01)

Thank you to everyone supporting Blocky! 🙏

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDYuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI0Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧰 technical debts Technical debts, refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants