indexer: reuse one discovery client per URL (#197) - #202
Conversation
FileLocationCache.getFileLocation built a ZgsClient for every candidate URL on every lookup that reached discovery, inside a loop over locations and ports. Each client carries its own HTTP transport, and Close does not release its connections - measured at two descriptors per client, the same whether the client succeeded or failed and whether or not it was closed. So the descriptor count grew with traffic. Report finding 25 proposed closing the client on the shard-config error path. That fixes nothing measurable: 50 successful, explicitly closed clients leak exactly as much as 50 unclosed failures. The error path was never the problem. Key clients by URL and keep them, so the cost is proportional to the number of distinct nodes seen rather than to the number of lookups. The network bounds the former. Deliberately no eviction: an evicted client's connections would not be reclaimed either, so eviction would restore the growth rather than cap it. Close now shuts down every cached client. The defer inside the loop is gone too - it held every probed client until the enclosing call returned, which for a multi-port sweep meant all of them at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing - not worth it. The measurement stands: two descriptors per ZgsClient, released neither by Close nor by anything else reachable from here, and discovery constructs one per candidate URL on every lookup that reaches it. But I never showed that adds up to an operational problem. Reaching discovery at all requires the trusted set not to cover the file's shards, and I have no measurement of how often that happens in practice - so the growth rate is unknown, and against a typical descriptor limit it may never matter. The fix also carried its own cost I underweighted: clients cached for the process lifetime with deliberately no eviction, which trades a slow descriptor leak for holding a client per node ever seen. That is defensible only if the leak is real in practice, which is exactly what is unestablished. |
Fixes #197. Report finding 25 — where the reported fix turned out to be inert and the real problem was one layer over.
What the report proposed, and why it doesn't work
Finding 25 says
NewZgsClientleaks its RPC client when the initial shard-config lookup fails, and suggestsclient.Close()before the error return. I implemented that, measured it, and it changes nothing:Close()Close()dTwo per client, regardless of outcome or cleanup.
Close()releases no connections for an HTTP client, andproviders.Optionoffers no way to supply a shared one — so the error path was never the issue.The real problem
getFileLocationbuilds a client per candidate URL, on every lookup that reaches discovery, inside a loop over locations × ports:So descriptor use grew with traffic, not with the number of nodes. In a long-running indexer that's unbounded from ordinary requests. The
deferinside the loop compounded it, holding every probed client until the enclosing call returned.Change
Key clients by URL and keep them. The cost becomes proportional to the number of distinct nodes seen, which the network bounds, instead of to the number of lookups.
Deliberately no eviction. An evicted client's connections wouldn't be reclaimed either —
Closedoesn't do that — so an LRU would restore the unbounded growth rather than cap it, just more slowly.Close()on the cache now shuts down every cached client.A URL whose shard-config lookup fails is not cached, so a transient failure doesn't poison it permanently; there's a test.
Testing
..._ReusesOneClientPerURL..._DistinctURLsGetDistinctClients..._FailedClientIsNotCachedDescriptor counts come from
/proc/self/fdor/dev/fd, skipping where neither is enumerable.go build,go vet,gofmtandgo test -count=1 ./...(14 packages) all pass.This change is