Why Context Cancel Is Not Enough
In go-ethereum v1.10.21, rpc.DialContext says the context only controls initial connection establishment. It does not close the resulting client.
For non-HTTP clients, initClient starts dispatch/read goroutines:
if !isHTTP {
go c.dispatch(conn)
}
dispatch starts the read loop:
go c.read(codec)
The intended shutdown path is Client.Close():
func (c *Client) Close() {
if c.isHTTP {
return
}
select {
case c.close <- struct{}{}:
<-c.didClose
case <-c.didClose:
}
}
So defer cancel() only cancels the timeout context. It does not signal c.close, does not wait on c.didClose, and does not terminate those goroutines.
Proper Fix
Close immediately after a successful dial:
c, err := rpc.DialContext(timeout, url)
if err != nil {
return fmt.Errorf("failed to connect to endpoint: %w", err)
}
defer c.Close()
Same pattern in getFinality:
c, err := rpc.DialContext(timeout, w.url)
if err != nil {
return false, false, fmt.Errorf("failed to connect to endpoint: %w", err)
}
defer c.Close()
That ensures every return path after successful dial closes the temporary client, including error returns from the RPC call or response parsing.
While you're here, you may want to look into testing
rpc.DialContextas well. GPT is telling me the following. Take it or leave it -- also feel free to capture in a GitHub Issue and resolve separately. I just wanted to note it here in case the memory leaks are interfering with your work. If so, you could look into DialContext as well to help get unstuck.Originally posted by @johnsaigle in #4852 (review)