Skip to content

Swartzn/fix/remote node connection handler - #333

Open
swartzn wants to merge 3 commits into
mainfrom
swartzn/fix/remote-node-connection-handler
Open

swartzn wants to merge 3 commits into
mainfrom
swartzn/fix/remote-node-connection-handler

Conversation

@swartzn

@swartzn swartzn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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,

  1. Redefine retry in the grpcClientHandler interface's connect function to isErrTransient. 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.

    • To see this implementation, just look at the first commit.
  2. Remove retry/isErrTransient altogether from the grpcClientHandler interface's connect function. This unifies the errors so that they are all retried under the same exponential backoff mechanism.

    • To see this implementation, look at all the changes together.

What does this PR do / why do we need it?

Required for all PRs.

  • Fixes the remote worker node shutdown.
  • Aligns grpcClientHandler connect semantics with the expected behavior.

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:

  • Documentation:
    • Does developer documentation (code comments, readme, etc.) need to be added or updated?
    • Does the user documentation need to be expanded or updated for this change?
  • Testing:
    • Does this functionality require changing or adding new unit tests?
    • Does this functionality require changing or adding new integration tests?
  • Git Hygiene:

For more details refer to the Go coding standards and the pull request process.

@swartzn
swartzn requested a review from a team as a code owner June 11, 2026 12:51
@swartzn swartzn mentioned this pull request Jun 11, 2026
9 tasks
@iamjoemccormick iamjoemccormick added the bug Something isn't working label Jun 16, 2026
@swartzn
swartzn force-pushed the swartzn/fix/remote-node-connection-handler branch from b533c69 to 2e9785c Compare June 22, 2026 11:28
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.
@swartzn
swartzn force-pushed the swartzn/fix/remote-node-connection-handler branch from 2e9785c to 850a4c5 Compare July 14, 2026 20:34
@swartzn
swartzn force-pushed the swartzn/fix/remote-node-connection-handler branch from 850a4c5 to dea0057 Compare July 14, 2026 20:52

@iamjoemccormick iamjoemccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: there are some stale comments referencing "fatal" errors we could cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants