Conversation
b533c69 to
2e9785c
Compare
Fix remote worker handler's shutdown process. Fix non-transient connection errors so that it retries after a fixed period of time. Change grpcClientHandler connect semantics from retry to isErrTransient to match expected behavior.
2e9785c to
850a4c5
Compare
β¦ntHandler connect return signature
850a4c5 to
dea0057
Compare
iamjoemccormick
left a comment
There was a problem hiding this comment.
Overall LGTM, this is also much easier to follow than what I wrote originally! Only one blocker, which maybe you already considered and isn't worth addressing.
| } | ||
| return n.nodeCtx.Err() | ||
| case <-time.After(retryDelay): | ||
| if err := n.connect(config, wrUpdates, requiredFeatures); err != nil { |
There was a problem hiding this comment.
todo: initially Claude flagged this as "gRPC ClientConn leaked on every failed reconnect attempt" but I pushed back because I'd never observed this happen before, and I'd expect the documentation on grpc.NewClient to explicitly call out the need to disconnect if it didn't somehow handle cleaning up connections. Below is Claude's revised findings. I still think this is worth addressing, but unless someone drops the MaxReconnectBackOff we're unlikely to accrue a meaningful number of "live orphaned connections".
gRPC ClientConn leaked on every failed reconnect attempt beegfs-go/rst/remote/internal/worker/worker.go:206 (with beesync.go:45)
BeeSyncNode.connect assigns a live *grpc.ClientConn to n.conn at beesync.go:45, then can still fail at a later step β address-discovery heartbeat (:69/:73), capability check (:88/:90), config rejected (:109), or bulk-update rejected (:120). The new waitUntilConnected retries connect() in a tight loop at worker.go:206 and never calls disconnect() between attempts β it only returns on success or context-cancel, so Handle's setOffline()/disconnect() at the bottom of the outer loop is never reached during a persistent-rejection cycle. The next iteration overwrites n.conn via beegrpc.NewClientConn, orphaning the prior connection plus its resolver/balancer goroutines and socket.
This is a genuine regression for the previously-"fatal" paths: in the old code, connect returned retry=false for config-rejected / bulk-rejected / address-discovery failures, which caused connectLoop to return and Handle to run n.disconnect() before the next attempt. (The transient RPC-error paths already leaked in the old code via continue-without-disconnect; this diff extends the leak to the previously-fatal paths.)
Failure scenario: a sync node is dial-reachable but persistently rejects config or fails capability negotiation (version skew / misconfiguration). Each retry leaks one ClientConn, accumulating unbounded β roughly one per MaxReconnectBackOff seconds (default 60s) once backoff saturates β for as long as the node stays misconfigured.
Minimal fix β disconnect before backing off in the error branch of waitUntilConnected:
if err := n.connect(config, wrUpdates, requiredFeatures); err != nil {
if derr := n.disconnect(); derr != nil {
n.log.Debug("error disconnecting before reconnect", zap.Error(derr))
}
// ...existing backoff...
}(disconnect() is already nil-safe on n.conn β see beesync.go:132.)
Assisted-by: Claude:claude-opus-4-8
There was a problem hiding this comment.
nitpick: there are some stale comments referencing "fatal" errors we could cleanup.
This patch does solidifies the worker connect/connection handler behavior. The connection handler should only stop trying to reconnect to the worker when remote itself is shutting down. So I propose two alternatives with a preference for the second,
Redefine
retryin thegrpcClientHandlerinterface'sconnectfunction toisErrTransient. This keeps the beesync connection code the same but realigns the interface with the expected behavior. This allows us to cleanly adjust remote's worker connection handler so that there are two error types: 1) transient errors (rpc/network) and 2) non-transient errors (version/config). Keeping these error types separate allows for a fixed retry delay for the non-transient errors.Remove
retry/isErrTransientaltogether from thegrpcClientHandlerinterface'sconnectfunction. This unifies the errors so that they are all retried under the same exponential backoff mechanism.What does this PR do / why do we need it?
Required for all PRs.
Related Issue(s)
Required when applicable.
Where should the reviewer(s) start reviewing this?
Only required for larger PRs when this may not be immediately obvious.
Are there any specific topics we should discuss before merging?
Not required.
What are the next steps after this PR?
Not required.
Checklist before merging:
Required for all PRs.
When creating a PR these are items to keep in mind that cannot be checked by GitHub actions:
For more details refer to the Go coding standards and the pull request process.