offload dns resolution off the green worker#598
Merged
Merged
Conversation
getaddrinfo is synchronous and there is nothing to poll, so a lookup made from a green task held the worker os thread for its whole duration and every other task pinned to that worker stalled behind it. sockets already yield through the epoll reactor, which left dns as the one blocking step on the common client path: every grpc, http, and database dial starts with one. the lookup now goes to a small pool of ordinary blocking threads (four at most, started on demand) while the calling task parks, woken by the pool thread through the same park/wake handshake netpoll uses for socket readiness. the os-thread backend is untouched — resolve() checks the backend and calls to_socket_addrs inline there, where blocking a thread is what a thread is for — and a numeric address skips the pool entirely, since parsing one never blocks. failure behaviour is unchanged: a resolver error and an empty answer both come back as no addresses, which green_connect turns into 0 and pith_dns_resolve into null, exactly as before. a panic inside a lookup is contained and reported as a failed lookup rather than stranding the parked task. if no pool thread can be started at all, the caller falls back to resolving inline.
covers the path the resolver-pool change touches from pith: a successful lookup, a dial whose lookup succeeds and whose connect is refused, and a loopback round trip. it is hosts-file and loopback only, so it needs no network and produces the same output on both backends — which is the point, since the green run of this case goes through the pool and the os-thread run resolves inline.
both the backend-choice section and the rough-edges list called out getaddrinfo as the blocking step in front of a dial. describe the resolver pool instead, and pick a different example for the native call preemption cannot interrupt.
kacy
added a commit
that referenced
this pull request
Jul 27, 2026
* make the green backend the default on linux green is faster on every shape this repo measures and the last blocker (dns holding a worker) landed in #598, so a spawned task now runs as a coroutine on the worker pool unless you say otherwise. the default stays os threads everywhere else. the reactor is epoll and eventfd, so macos and the bsds compile netpoll_fallback, which has no reactor at all: a green task waiting on a socket there would hold its worker for the whole wait. green is still reachable with PITH_GREEN=1 on those platforms, just not the default. PITH_GREEN now reads in both directions. 1/on/true force green, 0/off/false force os threads, and anything else takes the platform default. the off side is new and is the escape hatch for anyone the flip hurts, so it is trimmed and matched case-insensitively. an unrecognized value falls to the platform default rather than to os threads, so the only values that move the backend are the ones spelled out in the match and a typo behaves exactly like an unset variable. parsing moved into a pure backend_from_env so it can be tested without fighting the OnceLock that backend() caches into. * pin both backends explicitly in the differential tests with green as the linux default, a comparison whose reference side just says `pith run` is green against green and passes for free. every green-* target now runs its os-thread side under PITH_GREEN=0. the same reasoning applies to coverage. `make test` on a linux box now exercises only green, so the PITH_GREEN=0 path across the corpus had no gate at all. verify-osthread-corpus mirrors verify-green-corpus for the opt-out and ci runs both. memcheck grew a small second list for the cases whose subject is the os-thread task machinery itself, so the flip does not quietly stop testing the code they were written for. * describe green as the default rather than the experiment docs/concurrency.md drops the "off by default and still experimental" framing. the backend-choice section now says green on linux, os threads elsewhere, and PITH_GREEN=0 as the opt-out, and it keeps the caveats that came with the flip rather than burying them: file i/o has no yield point so a file read holds its worker and everything pinned to it; preemption is a build-time opt-in where the kernel gave os threads preemption for free; and task placement is first-resume luck. docs/limitations.md had a list of what it would take to make green the default. most of that is history now, so it reads as the caveats of the current default instead. the performance scoreboard labelled its concurrency rows "(green)"; the os-thread rows are the ones that need a label now, so they carry PITH_GREEN=0. the same for the bench README table and the examples and module headers that showed PITH_GREEN=1 as the way to opt in. * accept no/yes for the backend switch, and fix two stale strings three small corrections on top of the flip. `PITH_GREEN=no` fell to the platform default, which on linux is green — so someone reaching for the escape hatch and writing the most natural word for "off" got exactly what they were trying to turn off. no/n now mean os threads and yes/y mean green, which keeps the rule (only spelled-out values move the backend) while making the direction that costs something hard to miss. the green corpus target claimed it "matches os-thread output". it compares against a fixed expected file, which is the stronger check and the reason it catches a drift both backends share. now that there is a sibling target that really does run os threads, the old wording is worth correcting. and the corpus is 261 cases, not 260, since the dns case joined it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
getaddrinfois synchronous and there is nothing to poll, so a name lookup made from a green task held the worker os thread for its whole duration. every other task pinned to that worker stalled behind it. sockets already yield through the epoll reactor, which left dns as the one blocking step on the common client path, since every grpc, http, and database dial starts with one.the lookup now goes to a small pool of ordinary blocking threads while the calling task parks. a pool thread wakes it when the answer is ready, through the same park/wake handshake
netpolluses for socket readiness, so a wake that lands before the task finishes suspending is recorded as pending rather than lost. the pool is capped at four threads and grows only when nothing is idle, so a program that resolves one name at a time runs one thread and a program that never resolves under green starts none.the os-thread backend is untouched.
resolvechecks the backend and callsto_socket_addrsinline there, where blocking a thread is what a thread is for and a pool would only add a handoff. a numeric address also skips the pool, becauseto_socket_addrsparses one in place with no syscall to block in and dialing a literal ip is common.failure behaviour is unchanged. a resolver error and an empty answer both come back as no addresses, which
green_connectturns into0andpith_dns_resolveinto null, exactly as before. a panic inside a lookup is caught and reported as a failed lookup rather than stranding the parked task, and if no pool thread can be started at all the caller resolves inline rather than parking on an answer that will never come.the measurement
a program with two green tasks under
PITH_GREEN_WORKERS=1: one resolves 40 uncached names, the other runs a fixed cpu loop and reports when it finished. on one worker the cpu task can only overlap the lookups if the resolving task gives the worker back.seven interleaved pairs, swapping only
target/release/libpith_runtime.abetween arms so the driver binary is identical:the cpu task's own runtime, measured alone, is 413-418 ms. before the change it always finished last and always at roughly its own time plus the whole dns batch; after it finishes at its own time while the resolver is still working (the resolver finished at 903-1010 ms in those same runs). so the worker is genuinely free during a lookup rather than the lookups merely getting faster.
what was tested
cargo build --releaseat the repo root andcargo test --release -p pith-runtime, including six new unit tests for the pool: it answers a lookup, reports failure as no addresses, never exceeds its thread cap under a burst of jobs, stays inline outside a green task, short-circuits literal addresses to the same answerto_socket_addrsgives, and still rejects an out-of-range port through the literal pathmake test: 99/99 examples, 261/261 native regressions, 261/261 self-hosted regressions, plus the invalid, parity, cli, and ir-contract checksPITH_GREEN=1 make verify-green-corpus: 261/261 at the default worker count and again pinned to one worker (260 before this branch; the new case brings it to 261)make docsite-checktests/cases/test_dns_resolve_and_dial.pithprints identical output under os threads, green at one worker, and green at the default worker count