internal/resolver tests query live nameservers and fail nondeterministically #93
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The
internal/resolvertests onmainquery live nameservers (ns1.google.cometc.) and fail nondeterministically — a different subset each run — even in environments where directdig @ns1.google.comworks. This makesmake check/ CI flaky.Definition of done rewritten 2026-08-10 to match sneak's ruling (#93 (comment)). The original DoD proposed a
networkbuild tag or a-shortskip; both are now explicitly off the table. His words:> make the cap 60s in both and never use mocking, always use live resolvers and assume the build and run environments have full unmodified unrestricted internet access. it is ok if they fail due to a bad build environment that alters dns packets.
Definition of done
TESTING.md: the "within the 30-second target" wording becomes 60 seconds.REPO_POLICIES.md: themake testceiling of 20 seconds becomes 60 seconds, and the-timeoutbackstop is raised consistently with it. See the caveat below before editing this file.script/test: thego test -timeoutvalue must not be lower than the new cap, or the cap is unreachable in practice.-short, nonetworkbuild tag, no build-tag-gated tests. Live resolvers always, in every package.make checkgreen, and green across repeated consecutive runs.Scoped to the above only — it does NOT resolve the
feature/resolverreconciliation or the DNSSEC work in #59.Caveat on
REPO_POLICIES.mdThat file is org-canonical and vendored from
sneak/prompts. Changing this repo's copy in isolation creates drift. Whether 60s is the new org-wide cap or an approved dnswatcher divergence is raised at sneak/prompts#41. That question must not block this work; land the edit here in a way that matches whichever answer comes back.The finishing commit's title must end with
(closes #93).Reframing this issue under project policy before any implementation happens.
Policy: DNS is never mocked in dnswatcher
Per sneak (2026-08-07): DNS is never mocked in this project. Ever. No mock resolvers, no fake DNS servers, no stubbed lookups — not in tests, not anywhere. dnswatcher's entire purpose is correct behavior against the real DNS, and tests exercise real resolution against live nameservers by design. Flaky live tests are a robustness problem, never a justification for mocks. Any future worker picking this up: do not propose or implement mocked/hermetic DNS tests here — the change will be rejected. (This also means the "hermetic mocked tests" on
origin/feature/resolvermentioned inTODO.mdcannot land as-is.) This policy is being documented in the README via #94.Where that leaves this issue
The current definition of done proposes guarding the live tests behind an opt-in (
networkbuild tag or-shortskip). That is not mocking — the tests still hit real DNS when they run — but it changes what runs by default:make checkwould no longer exercise resolution at all unless opted in. That trade-off is an owner decision, so laying out the options:make checkkeeps exercising real DNS; flakiness is engineered out rather than routed around.make testbudget inREPO_POLICIES.md); an opt-in tier (build tag) runs the exhaustive/strict variants that are too slow or too environment-sensitive for every run.make checkbecomes offline-deterministic; live tests run only on demand. Fastest and most deterministic default, but the default check then proves nothing about the one thing dnswatcher exists to do.Recommendation
Option 1, falling back to option 2 only if robustness work can't keep the default suite reliably inside the 20-second
make testbudget. The repo's ethos is that testing against the real DNS is the test suite's value — a greenmainshould mean the resolver actually resolves. The observed nondeterminism (different subset failing each run againstns1.google.cometc.) is exactly the failure mode that retry-with-backoff plus multi-nameserver quorum is designed to absorb. Notably, today's fullmake checkrun in a clean environment passed all 39 resolver tests live, so the baseline is closer to "occasionally flaky" than "unusably broken" — robustness looks achievable.@sneak: please pick an option (1/2/3). Once decided, the DoD here should be rewritten to match, and implementation can proceed. Assigning to you for the decision.
[manager] Not working this issue — the approach decision is parked with @sneak. Adding one finding from the 1.0 backlog audit that bears directly on that decision, because it was not visible when this issue was written.
This issue's definition of done is now in direct conflict with
TESTING.mdonmain.Item 1 of the DoD here proposes guarding the live tests behind "a
networkbuild tag, or atesting.Short()skip with-shortwired intoscript/test".TESTING.md(added since, and reinforced by PR #97) says the opposite in as many words:> - Do not add
-shortflags to skip slow tests> - Do not increase
-timeoutto hide hanging queries>
> Flaky failures from transient network issues are acceptable and should be investigated as potential resolver bugs, not papered over with mocks or skip flags.
So both of this issue's suggested mechanisms are explicitly forbidden by the repo's own testing policy. Implementing this as written would require either amending
TESTING.mdor overriding it — which is exactly why it is your call, not mine.Two further data points from the audit:
The suite is currently green and fast.
make checkonmain(9347a28) passed in 7.8 seconds total, with lint clean. The resolver's live-DNS tests were not the bottleneck in this environment. Whatever the flakiness is, it is not reproducing here right now — which argues for the "investigate as a resolver bug" path over the gating path.There is a second, quieter cost.
docker buildrunsmake check, so the live-DNS coupling means the Docker build requires outbound UDP:53 from the build environment. In a network-restricted CI runner, essentially every resolver test would have to exhaust its retries against unreachable root servers before failing. Worst case that is bounded by a per-test 60s context (resolver_test.go:35-44) but plausibly exceeds the 30sgo test -timeout. If the flakiness you saw was in CI rather than locally, egress restrictions are a likelier root cause than resolver bugs — and that would point at a third option neither this issue norTESTING.mdcurrently considers: fix the build environment's DNS egress, and change no test code at all.Also worth noting:
TESTING.mdline 21 refers to keeping the suite "within the 30-second target", whileREPO_POLICIES.mdsets themake testceiling at 20 seconds (with a 30s timeout as the backstop). Those two numbers should be reconciled whenever this issue is resolved.No action taken. Flagging only so the decision is made against current facts.
make the cap 60s in both and never use mocking, always use live resolvers and assume
the build and run environments have full unmodified unrestricted internet access. it is ok if they fail due to a bad build environment that alters dns packets.
[dispatcher] DECIDED by sneak 2026-08-09. His words verbatim:
So, unambiguously:
-short, nonetworkbuild tag. The two mechanisms this issue's original DoD proposed are both off the table, which resolves the conflict withTESTING.mdinTESTING.md's favour.TESTING.md's target andREPO_POLICIES.md's ceiling, which currently disagree at 30s and 20s. Reconcile both to 60s.Rewrite the DoD to match and implement. The robustness work (retries, multi-nameserver quorum, sensible timeouts) is the sanctioned way to address flakiness, now with a 60s budget to work inside rather than 20s.
One thing needing care, flagged rather than assumed:
REPO_POLICIES.mdis the org-canonical file vendored fromsneak/prompts. Changing this repo's copy to 60s in isolation creates drift from canonical. I have raised that upstream at sneak/prompts#41 to establish whether 60s is the new org-wide cap or an approved dnswatcher divergence — do not let that block the test work here, but land theREPO_POLICIES.mdedit in a way that matches whichever answer comes back.[worker] Implementation plan for #93, per the rewritten DoD. Working on
next, single commit ending(closes #93).Baseline measured first
On
next(cc86473),make testruns the whole suite in 4.2 s,internal/resolverin 2.0 s, and I got 6 consecutive clean runs with zero failures. So the nondeterminism is not reproducing in this environment right now; the robustness work below targets the mechanisms that produce it rather than a failure I can currently trigger.Where the nondeterminism actually comes from
Three structural properties of the current live tests, all test-side:
t.Parallel(), this host has 48 CPUs, sogo testwill run essentially all of them at once. Every one of them starts an independent iterative resolution, andqueryServerswalksrootServerList()in fixed order — so all ~35 resolutions fire their first query at198.41.0.4within milliseconds of each other. Root servers rate-limit. That is a very good fit for the reported symptom of a different subset failing each run.TestQueryAllNameservers_AllReturnOKandTestQueryAllNameservers_NXDomainFromAllNSrequire everygoogle.comnameserver to answer. With 4 nameservers that is 4 independent chances to fail per run, and no tolerance for one being slow.What I will do
Docs / caps (DoD 1-3):
TESTING.md: "within the 30-second target" becomes 60. One-line surgical edit only, so it merges cleanly with #97.REPO_POLICIES.md:make testceiling 20 s becomes 60 s, and the documented-timeoutbackstop is raised with it.script/test:-timeout 30sraised to the same new backstop value.-timeout 120s. It must strictly exceed the 60 s cap or the cap is unreachable; it must also exceed the per-test live context (60 s) so a hung test is reported by its own context rather than by a whole-suite panic. 2x the cap gives room for-raceoverhead. Stated in the PR body.REPO_POLICIES.mdnow diverges from canonical pending sneak/prompts#41.Robustness (DoD 4), all in
internal/resolvertest code:internal/resolver/livedns_test.goholding the live-DNS helpers, soresolver_test.goitself takes only small call-site edits and stays mergeable with #97.t.Parallel(); only their network work is throttled. This is the direct fix for the burst-at-one-root problem, and the 60 s budget is what makes it affordable.google.com, or records for a name that must not exist) still fails the test. That keeps the assertions meaningful while absorbing one slow server.QueryNameserverrather than NS discovery. This is a live query executed once and shared, not a recorded fixture; NS discovery itself remains directly exercised by its own live tests.Explicitly not doing: no mocks, fakes or stubs; no
-short; no build tags; no skips; no environment-tolerance for restricted egress; no padding of timeouts in place of real work.Production code (DoD 7): no changes. Note that the single most effective lever against root-server rate limiting would be randomising the order in which
queryServerstriesrootServerList(), which lives in production code — out of scope here, and I will flag it in the PR body as an observation rather than acting on it.Verification (DoD 8):
make check(Docker lint included) run repeatedly, not once; I will report the exact number of consecutive runs and any flake I see, including flakes I cannot reproduce.[worker] Implemented and pushed as
9cb2c2bonnext, accumulated into #136. Full write-up is in that PR body; summary and verification here.What was built
No mocks, fakes, stubs, recorded fixtures,
-short, build tags or skips, and no environment-tolerance for restricted egress. Production resolver behaviour is unchanged.New
internal/resolver/livedns_test.gocarries all the live-DNS machinery, soresolver_test.goitself only changes at call sites:queryServerswalksrootServerList()in fixed order, all aimed their first query at198.41.0.4. Root servers rate-limit that, which matches the reported symptom of a different arbitrary subset failing each run. Tests keept.Parallel(); only their network work is throttled.TestQueryAllNameservers_AllReturnOKand_NXDomainFromAllNSno longer require every nameserver to answer; a strict majority suffices. A silent nameserver is tolerated, a wrong one is not: no server may report NXDOMAIN forgoogle.com, and none may answer OK for a name that must not exist. The retry predicate also fires when fewer than a quorum answered at all.New
internal/resolver/livedns_harness_test.gotests that machinery directly — quorum arithmetic, status counting, the concurrency bound, per-attempt deadlines, and recovery from a transient failure. It performs no DNS resolution of any kind.Caps and backstop
Landed against the org-wide ruling at sneak/prompts#41 (comment), which arrived while this was in flight and supersedes the caveat in the DoD above: 60s hard cap, 20s target, overage between the two filed as an improvement bug, 90s
-timeoutbackstop.script/test:-timeout 30sbecomes-timeout 90s. The old value would have killed a 60s-capped suite at half its allowance, making the cap unreachable. Nothing else in the repo carried the old30s.REPO_POLICIES.md: not hand-edited. Copied byte-for-byte fromprompts/REPO_POLICIES.mdonsneak/promptsbranchorg-wide-60s-test-cap(52b5192), verified withcmpandsha256sum(bcf11c31...). This is the new canonical text, not a divergence. Re-vendoring the whole file also picks up drift this copy had fallen behind on (thelast_modifieddate and the canonical golangci-lint v2.12.2 pin paragraph).TESTING.md: "within the 30-second target" becomes 60, as a single-line edit so it merges cleanly when #97 lands.Verification
Everything through
make/script/entrypoints; lint in Docker.Ten consecutive
make checkruns, all exit 0, zero failures, and zero(cached)lines — Go's test cache reportsok pkg (cached)without executing anything, which would prove nothing about nondeterminism, so every run was forced to actually execute and each log checked. Wall times 26-47s (dominated by the Docker lint stage, which was confirmed executing rather than cached at 39.7s).make testalone: 3.6-4.1s over three timed uncached runs.internal/resolvermoved from 2.0s to ~2.9s, which is the concurrency gate's cost. Inside the 20s target, so no improvement bug is owed under the new two-tier rule.What those runs do not prove, stated plainly. Live DNS was healthy the entire time: no live-DNS retry fired even once across the ten runs, and no flake was observed before or after the change either — six pre-change baseline runs on
cc86473were also clean. So the runs show the change is not itself flaky and does not slow the suite; they do not demonstrate recovery from a real DNS failure, because none occurred. The retry path is instead proven byTestRetryLiveRecoversFromTransientFailure, which is the sole source of the singleretrying in 500msline in each log.One thing left alone, deliberately
The largest remaining lever against root-server rate limiting would be to stop
queryServerstryingrootServerList()in the same fixed order, spreading load across all thirteen roots instead of concentrating ona.root-servers.net. That is production code and item 7 of the DoD scopes this work as test-side, so I did not touch it. Flagging it as a question rather than filing it as a defect, since fixed-order root selection may well be a deliberate simplicity choice. @sneak, say the word if you want it as a separate issue.[manager] Landed on
next(9cb2c2b+ rework87bce43), carried by #136. First review failed, rework passed a fresh review.Worth recording, since it nearly shipped: the quorum change tolerated
nodataas a wrong answer, so one bad nameserver in four passed. Fixed as a closed allowlist, verified by injection.Two boundaries this unit deliberately does not cover, both tracked: test caching means a repeat green proves nothing (#139), and no live-DNS retry ever fired in ~20 runs, so real-failure recovery is evidenced only synthetically.
make testuncached is 5.96s, inside the 20s target, so no improvement issue is owed.error(and therefore SERVFAIL) is sanctioned in the quorum tests by design — a minority of actively SERVFAILing nameservers is tolerated.