Skip to content

Flaky under parallel make: OAuthTransportTest#test_https_proxy_refused_without_p_use_ssl_support does live DNS inside a 1s bound #720

Description

@jeremy

What happens

ruby/test/basecamp/oauth_transport_test.rb:839
OAuthTransportTest#test_https_proxy_refused_without_p_use_ssl_support — was
observed failing during a parallel make run while the #669/#674 build-hygiene
wave was being proven (2026-08-12, macOS, several concurrent agent workspaces on
one machine). It passes on a quiet machine and it passed on every serial run
since. Filing it rather than fixing it, because it is out of scope for the PR
that hit it and it should not be lost.

I have not reproduced it deterministically. What follows is a mechanism read of
the source, which is the actionable part.

Why it is timing-dependent

The test asserts the fetcher refuses an https:// proxy when the bundled
net-http lacks Net::HTTP.new's eighth p_use_ssl parameter:

error = assert_raises(Basecamp::Oauth::OauthError) do
  Basecamp::Oauth::Fetcher.stream_http(:get, "https://old-net-http.test/token", timeout: 1)
end
assert_equal "validation", error.type

That OauthError is raised at ruby/lib/basecamp/oauth/fetcher.rb:371. Reaching
it requires getting past line 363 first:

proxy_uri = Timeout.timeout(timeout, Net::OpenTimeout) { uri.find_proxy }

URI::Generic#find_proxy resolves the target hostname — IPSocket.getaddress
to evaluate its loopback rule. The fetcher's own comment above that line says so
and is deliberate about it ("a blocking DNS call that must sit inside the
advertised bound like every other network step"). So the test performs a live
DNS lookup for old-net-http.test inside a 1-second bound
, before the
assertion's subject is ever evaluated.

.test is RFC 6761 reserved, so this is normally a fast NXDOMAIN — but it is
still a real query to whatever resolver the machine has. Blow the second and the
Timeout fires instead, surfacing as Faraday::TimeoutError rather than
Basecamp::Oauth::OauthError, and assert_raises fails. Under make -j the
same machine is running npm, uv, bundler, Gradle and Go concurrently, which is
exactly when a resolver round-trip stops being free.

That predicts a specific failure signature. Worth confirming against the real
failure output before acting:

Expected Basecamp::Oauth::OauthError, got #<Faraday::TimeoutError>

If the observed failure was a different error or a different assertion, this
analysis is wrong and the finding still stands on the observation.

Notes for whoever picks it up

  • The neighbouring test at oauth_transport_test.rb:800 already stubs
    IPSocket.getaddress (there, to simulate a stall). This test does not, so it
    is the only one in the file whose pass/fail depends on the machine's resolver.
  • The timeout: 1 is doing double duty: the test's subject is a validation
    refusal that needs no timeout at all, but the value also bounds a DNS call the
    test does not care about.
  • Directions, not a prescription — which is right depends on what the test is
    meant to pin: stub the resolution the way its neighbour does; raise the bound;
    or reorder so the capability check precedes proxy resolution (that last one
    changes shipped behaviour and would need its own argument).
  • ruby/test is not parallelized (no parallelize_me!), so this is not
    intra-suite interference. The concurrency is make -j across toolchains,
    competing for the machine — not for Ruby's process.

Scope

Split off from the PR closing #669 and #674. That PR closes #674 against its
Gradle half only (serializing the Gradle-backed check-targets); this is the
other thing make -j surfaced, and it is unrelated to Gradle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions