Skip to content

fix(stats): populate allow/denylist counts in /api/stats at startup - #2113

Merged
0xERR0R merged 3 commits into
mainfrom
fix/stats-empty-list-counts-at-startup
Jun 12, 2026
Merged

0xERR0R merged 3 commits into
mainfrom
fix/stats-empty-list-counts-at-startup

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • /api/stats reported empty lists.allowlist / lists.denylist until the first list refresh (manual or periodic).
  • Root cause: the stats resolver populates those counts only from the BlockingCacheGroupChanged event. That event is emitted once while the lists load inside NewBlockingResolver (server.go:495) — before NewStatsResolver subscribes to the bus (server.go:527) — so the stats resolver never saw the initial counts. The cache-entries gauge isn't affected because CachingResultCacheChanged re-fires continuously as queries flow in; list counts only change on refresh.
  • Fix: re-emit the current per-group counts on 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.
  • The re-publish runs in a goroutine: EventBus.Publish holds a global lock for the duration of handler invocation, so publishing synchronously from within a handler deadlocks. This matches how the existing handler already defers initFQDNIPCache.

Changes

  • lists/list_cache.go: new ListCache.PublishGroupCounts() that re-emits BlockingCacheGroupChanged with each group's current ElementCount.
  • resolver/blocking_resolver.go: the existing ApplicationStarted handler 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 on ApplicationStarted.

Test Plan

  • New regression test fails before the fix, passes after.
  • go test ./resolver/ ./lists/ ./stats/ all green.
  • go build ./... and go vet clean.
  • Manual: start blocky with lists configured, call /api/stats immediately — lists.allowlist/denylist are populated without a manual refresh.

…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.
Copilot AI review requested due to automatic review settings June 12, 2026 20:57
@0xERR0R 0xERR0R added the 🐞 bug Something isn't working label Jun 12, 2026
@0xERR0R 0xERR0R added this to the v0.32.0 milestone Jun 12, 2026

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.

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-publish BlockingCacheGroupChanged events 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.

Comment thread resolver/blocking_resolver_test.go
@codecov

codecov Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.50000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.01%. Comparing base (4018d9e) to head (bc6b2e6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lists/list_cache.go 0.00% 3 Missing ⚠️
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.
📢 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.

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.
@0xERR0R
0xERR0R enabled auto-merge (squash) June 12, 2026 21:07
@0xERR0R
0xERR0R merged commit 9702c8f into main Jun 12, 2026
21 of 22 checks passed
@0xERR0R
0xERR0R deleted the fix/stats-empty-list-counts-at-startup branch June 12, 2026 21:10
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jun 20, 2026
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 ([#&#8203;2091](0xERR0R/blocky#2091)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`87be127`](0xERR0R/blocky@87be127): feat(cache): Shard the result cache to remove the single-lock read ceiling ([#&#8203;2097](0xERR0R/blocky#2097)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`7c6da15`](0xERR0R/blocky@7c6da15): feat(config): structurally merge config folder files ([#&#8203;1827](0xERR0R/blocky#1827)) ([#&#8203;2112](0xERR0R/blocky#2112)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e7958e0`](0xERR0R/blocky@e7958e0): feat(lists): opt-in on-disk download cache with conditional revalidation ([#&#8203;2087](0xERR0R/blocky#2087)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`b82199b`](0xERR0R/blocky@b82199b): feat(querylog): ignore domains (exact/wildcard/regex) in query log ([#&#8203;2084](0xERR0R/blocky#2084)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`bee2d8b`](0xERR0R/blocky@bee2d8b): feat(resolver): UDP-first plain-DNS upstream and EDNS0 buffer floor ([#&#8203;2100](0xERR0R/blocky#2100)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`0b70e5c`](0xERR0R/blocky@0b70e5c): feat(resolver): add DNS rebinding protection ([#&#8203;2111](0xERR0R/blocky#2111)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`1b8e08a`](0xERR0R/blocky@1b8e08a): feat(resolver): pool DoT connections and enable TLS session resumption ([#&#8203;2098](0xERR0R/blocky#2098)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`4018d9e`](0xERR0R/blocky@4018d9e): feat: in-memory statistics subsystem with /api/stats REST endpoint ([#&#8203;2093](0xERR0R/blocky#2093)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`7abca44`](0xERR0R/blocky@7abca44): feat: support PROXY protocol on proxied DoT/DoH listeners ([#&#8203;2094](0xERR0R/blocky#2094)) ([@&#8203;kastakhov](https://github.com/kastakhov))

##### Bug fixes

- [`2496d12`](0xERR0R/blocky@2496d12): fix(dnssec): close DNSSEC validation bypass & cache-scope pollution (GHSA-x845-2f78-7v36) ([#&#8203;2119](0xERR0R/blocky#2119)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`06555e0`](0xERR0R/blocky@06555e0): fix(metrics): bound reason label cardinality for blocked responses ([#&#8203;2114](0xERR0R/blocky#2114)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e0ea9b3`](0xERR0R/blocky@e0ea9b3): fix(resolver): eliminate recursive RLock deadlock in blocking group resolution ([#&#8203;2106](0xERR0R/blocky#2106)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`80f742e`](0xERR0R/blocky@80f742e): fix(server): answer browser CORS preflights for custom headers and Private Network Access ([#&#8203;2109](0xERR0R/blocky#2109)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`9702c8f`](0xERR0R/blocky@9702c8f): fix(stats): populate allow/denylist counts in /api/stats at startup ([#&#8203;2113](0xERR0R/blocky#2113)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Build and dependencies

- [`821e378`](0xERR0R/blocky@821e378): build(deps): bump codecov/codecov-action from 6 to 7 ([#&#8203;2089](0xERR0R/blocky#2089)) ([@&#8203;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 ([#&#8203;2095](0xERR0R/blocky#2095)) ([@&#8203;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 ([#&#8203;2108](0xERR0R/blocky#2108)) ([@&#8203;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 ([#&#8203;2116](0xERR0R/blocky#2116)) ([@&#8203;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 ([#&#8203;2115](0xERR0R/blocky#2115)) ([@&#8203;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 ([#&#8203;2090](0xERR0R/blocky#2090)) ([@&#8203;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 ([#&#8203;2101](0xERR0R/blocky#2101)) ([@&#8203;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 ([#&#8203;2096](0xERR0R/blocky#2096)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])

##### Misc

- [`1c9f717`](0xERR0R/blocky@1c9f717): ci(e2e): speed up e2e job via Ginkgo proc oversubscription ([#&#8203;2085](0xERR0R/blocky#2085)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`8d49922`](0xERR0R/blocky@8d49922): ci: cross-compile multi-arch docker image instead of QEMU emulation ([#&#8203;2081](0xERR0R/blocky#2081)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`355a9a1`](0xERR0R/blocky@355a9a1): docs(grafana): redesign Grafana dashboard and update integration guide ([#&#8203;2110](0xERR0R/blocky#2110)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`c791e80`](0xERR0R/blocky@c791e80): perf(e2e): probe blocky healthcheck at 250ms during start period ([#&#8203;2086](0xERR0R/blocky#2086)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`a0fb375`](0xERR0R/blocky@a0fb375): perf(lists): speed up blocklist loading (cache build + parsing) ([#&#8203;2083](0xERR0R/blocky#2083)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`316b073`](0xERR0R/blocky@316b073): perf(resolver): pre-classify client groups to cut per-query allocations ([#&#8203;2103](0xERR0R/blocky#2103)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`d502806`](0xERR0R/blocky@d502806): perf(server): let Truncate decide response compression ([#&#8203;2102](0xERR0R/blocky#2102)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`8b32512`](0xERR0R/blocky@8b32512): test(e2e): de-flake PROXY protocol and upstream init.strategy specs ([#&#8203;2099](0xERR0R/blocky#2099)) ([@&#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:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIyMi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants