Repository navigation
fix(stats): populate allow/denylist counts in /api/stats at startup - #2113
Conversation
…fore first refresh The stats resolver learns allow/denylist entry counts only from the BlockingCacheGroupChanged event. Those events are emitted once while the lists load inside NewBlockingResolver, which runs before NewStatsResolver subscribes to the bus, so the stats resolver missed them and reported empty allowlist/denylist until the next periodic (or manual) list refresh. Re-emit the current per-group counts on ApplicationStarted, which fires after the whole resolver chain is wired and every subscriber is attached. The re-publish runs in a goroutine because EventBus.Publish holds a global lock while invoking handlers, so publishing from within a handler would deadlock.
There was a problem hiding this comment.
Pull request overview
This PR fixes an initialization-order issue where /api/stats could report empty lists.allowlist / lists.denylist counts until the first list refresh, by re-emitting the current per-group list counts after all resolvers have subscribed to the event bus.
Changes:
- Add
ListCache.PublishGroupCounts()to re-publishBlockingCacheGroupChangedevents for current group counts. - On
ApplicationStarted, publish deny/allowlist group counts (off the event-bus publish path) before initializing the FQDN IP cache. - Add a regression test ensuring late subscribers receive list counts on
ApplicationStarted.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| resolver/blocking_resolver.go | Re-emits list-count events on ApplicationStarted via a goroutine to avoid event-bus deadlock. |
| resolver/blocking_resolver_test.go | Adds regression coverage for “late subscriber” receiving counts on ApplicationStarted. |
| lists/list_cache.go | Introduces PublishGroupCounts() to publish current per-group element counts via the existing event path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2113 +/- ##
==========================================
+ Coverage 86.98% 87.01% +0.02%
==========================================
Files 122 122
Lines 9352 9358 +6
==========================================
+ Hits 8135 8143 +8
+ Misses 977 975 -2
Partials 240 240 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The re-publish runs in a goroutine, so the event-bus handler writes the counts map concurrently with the Eventually poll that reads it. Guard the map with a mutex and read a cloned snapshot, and drop the unused listType parameter (unparam). Addresses Copilot review feedback; passes go test -race and make lint.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.31.0` → `v0.32.0` | --- ### Release Notes <details> <summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary> ### [`v0.32.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.32.0) [Compare Source](0xERR0R/blocky@v0.31.0...v0.32.0) #### Changelog ##### Features - [`c851293`](0xERR0R/blocky@c851293): feat(blocking): log the matched rule in the block reason ([#​2091](0xERR0R/blocky#2091)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`87be127`](0xERR0R/blocky@87be127): feat(cache): Shard the result cache to remove the single-lock read ceiling ([#​2097](0xERR0R/blocky#2097)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`7c6da15`](0xERR0R/blocky@7c6da15): feat(config): structurally merge config folder files ([#​1827](0xERR0R/blocky#1827)) ([#​2112](0xERR0R/blocky#2112)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e7958e0`](0xERR0R/blocky@e7958e0): feat(lists): opt-in on-disk download cache with conditional revalidation ([#​2087](0xERR0R/blocky#2087)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`b82199b`](0xERR0R/blocky@b82199b): feat(querylog): ignore domains (exact/wildcard/regex) in query log ([#​2084](0xERR0R/blocky#2084)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`bee2d8b`](0xERR0R/blocky@bee2d8b): feat(resolver): UDP-first plain-DNS upstream and EDNS0 buffer floor ([#​2100](0xERR0R/blocky#2100)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`0b70e5c`](0xERR0R/blocky@0b70e5c): feat(resolver): add DNS rebinding protection ([#​2111](0xERR0R/blocky#2111)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`1b8e08a`](0xERR0R/blocky@1b8e08a): feat(resolver): pool DoT connections and enable TLS session resumption ([#​2098](0xERR0R/blocky#2098)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`4018d9e`](0xERR0R/blocky@4018d9e): feat: in-memory statistics subsystem with /api/stats REST endpoint ([#​2093](0xERR0R/blocky#2093)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`7abca44`](0xERR0R/blocky@7abca44): feat: support PROXY protocol on proxied DoT/DoH listeners ([#​2094](0xERR0R/blocky#2094)) ([@​kastakhov](https://github.com/kastakhov)) ##### Bug fixes - [`2496d12`](0xERR0R/blocky@2496d12): fix(dnssec): close DNSSEC validation bypass & cache-scope pollution (GHSA-x845-2f78-7v36) ([#​2119](0xERR0R/blocky#2119)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`06555e0`](0xERR0R/blocky@06555e0): fix(metrics): bound reason label cardinality for blocked responses ([#​2114](0xERR0R/blocky#2114)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`e0ea9b3`](0xERR0R/blocky@e0ea9b3): fix(resolver): eliminate recursive RLock deadlock in blocking group resolution ([#​2106](0xERR0R/blocky#2106)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`80f742e`](0xERR0R/blocky@80f742e): fix(server): answer browser CORS preflights for custom headers and Private Network Access ([#​2109](0xERR0R/blocky#2109)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`9702c8f`](0xERR0R/blocky@9702c8f): fix(stats): populate allow/denylist counts in /api/stats at startup ([#​2113](0xERR0R/blocky#2113)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Build and dependencies - [`821e378`](0xERR0R/blocky@821e378): build(deps): bump codecov/codecov-action from 6 to 7 ([#​2089](0xERR0R/blocky#2089)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`d63f84b`](0xERR0R/blocky@d63f84b): build(deps): bump github.com/0xERR0R/expiration-cache from 0.1.0 to 0.2.0 ([#​2095](0xERR0R/blocky#2095)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`81f2b2a`](0xERR0R/blocky@81f2b2a): build(deps): bump github.com/onsi/ginkgo/v2 from 2.29.0 to 2.30.0 ([#​2108](0xERR0R/blocky#2108)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`9c5abdf`](0xERR0R/blocky@9c5abdf): build(deps): bump github.com/onsi/ginkgo/v2 from 2.30.0 to 2.31.0 ([#​2116](0xERR0R/blocky#2116)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`6c0ec5f`](0xERR0R/blocky@6c0ec5f): build(deps): bump github.com/onsi/gomega from 1.41.0 to 1.42.0 ([#​2115](0xERR0R/blocky#2115)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`4beb9fe`](0xERR0R/blocky@4beb9fe): build(deps): bump github.com/quic-go/quic-go from 0.59.1 to 0.60.0 ([#​2090](0xERR0R/blocky#2090)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`8fe2202`](0xERR0R/blocky@8fe2202): build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0 ([#​2101](0xERR0R/blocky#2101)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`52fa6a4`](0xERR0R/blocky@52fa6a4): build(deps): bump golang.org/x/sys from 0.45.0 to 0.46.0 ([#​2096](0xERR0R/blocky#2096)) ([@​dependabot](https://github.com/dependabot)\[bot]) ##### Misc - [`1c9f717`](0xERR0R/blocky@1c9f717): ci(e2e): speed up e2e job via Ginkgo proc oversubscription ([#​2085](0xERR0R/blocky#2085)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`8d49922`](0xERR0R/blocky@8d49922): ci: cross-compile multi-arch docker image instead of QEMU emulation ([#​2081](0xERR0R/blocky#2081)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`355a9a1`](0xERR0R/blocky@355a9a1): docs(grafana): redesign Grafana dashboard and update integration guide ([#​2110](0xERR0R/blocky#2110)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`c791e80`](0xERR0R/blocky@c791e80): perf(e2e): probe blocky healthcheck at 250ms during start period ([#​2086](0xERR0R/blocky#2086)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`a0fb375`](0xERR0R/blocky@a0fb375): perf(lists): speed up blocklist loading (cache build + parsing) ([#​2083](0xERR0R/blocky#2083)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`316b073`](0xERR0R/blocky@316b073): perf(resolver): pre-classify client groups to cut per-query allocations ([#​2103](0xERR0R/blocky#2103)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`d502806`](0xERR0R/blocky@d502806): perf(server): let Truncate decide response compression ([#​2102](0xERR0R/blocky#2102)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`8b32512`](0xERR0R/blocky@8b32512): test(e2e): de-flake PROXY protocol and upstream init.strategy specs ([#​2099](0xERR0R/blocky#2099)) ([@​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:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIyMi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Summary
/api/statsreported emptylists.allowlist/lists.denylistuntil the first list refresh (manual or periodic).BlockingCacheGroupChangedevent. That event is emitted once while the lists load insideNewBlockingResolver(server.go:495) — beforeNewStatsResolversubscribes to the bus (server.go:527) — so the stats resolver never saw the initial counts. The cache-entries gauge isn't affected becauseCachingResultCacheChangedre-fires continuously as queries flow in; list counts only change on refresh.ApplicationStarted, which fires after the full resolver chain is wired and every subscriber is attached. Reuses the existing event path, so the metrics gauges (which use.Set()) stay consistent and idempotent.EventBus.Publishholds a global lock for the duration of handler invocation, so publishing synchronously from within a handler deadlocks. This matches how the existing handler already defersinitFQDNIPCache.Changes
lists/list_cache.go: newListCache.PublishGroupCounts()that re-emitsBlockingCacheGroupChangedwith each group's currentElementCount.resolver/blocking_resolver.go: the existingApplicationStartedhandler now calls it for both matchers (off the publish path).resolver/blocking_resolver_test.go: regression test — a subscriber that registers after the initial load receives the counts again onApplicationStarted.Test Plan
go test ./resolver/ ./lists/ ./stats/all green.go build ./...andgo vetclean./api/statsimmediately —lists.allowlist/denylistare populated without a manual refresh.