Skip to content

feat: reconnect worker to coordinator after heartbeat timeout#2408

Open
pirm-in wants to merge 2 commits into
dagucloud:mainfrom
pirm-in:feat/worker-coordinator-reconnect
Open

feat: reconnect worker to coordinator after heartbeat timeout#2408
pirm-in wants to merge 2 commits into
dagucloud:mainfrom
pirm-in:feat/worker-coordinator-reconnect

Conversation

@pirm-in

@pirm-in pirm-in commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Workers can get stuck talking to a dead cached coordinator connection when a
coordinator restarts or moves to a new address under the same ID. This drops
and re-dials the cached gRPC client on transient failures, and bounds the
worker heartbeat with a configurable timeout so a stalled coordinator cannot
block the heartbeat loop.

Changes

  • Reset (close + drop) the cached coordinator client on transient gRPC errors (Unavailable / deadline) so the next call re-dials
  • Add configurable HeartbeatTimeout (default 10s) bounding the worker heartbeat
  • Health check reuses the already-resolved client instead of re-resolving it

Related Issues

none

Checklist

  • Code follows the project style guidelines
  • Self-review of the code has been performed
  • Tests have been added or updated as needed
  • Documentation has been updated as needed (N/A - config not user-facing)
  • Changes have been tested locally (make test, make lint)

Summary by cubic

Workers now recover from coordinator restarts/moves by dropping stale cached gRPC clients on transient errors and re-dialing on the next call. This now also covers stream failover and workspace-bundle paths, and the heartbeat is bounded by a configurable timeout (default 10s).

  • Bug Fixes

    • Evict the cached coordinator client on transient gRPC failures (Unavailable, deadline) and only remove it if it matches the current cached instance.
    • On health-check failure during stream failover and workspace-bundle operations, reset the stale client via a shared checkHealthy and retry with a fresh connection; health checks reuse the resolved client.
  • New Features

    • Added HeartbeatTimeout to coordinator.Config; applied per heartbeat (default 10s).

Written for commit 80e93f8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Improvements

    • Improved coordinator recovery after transient failures and expired deadlines.
    • Prevented concurrent operations from discarding newer coordinator connections.
    • Added configurable heartbeat timeouts, defaulting to 10 seconds.
    • Improved failover for heartbeats, stream connections, and workspace uploads.
  • Bug Fixes

    • Heartbeats now reconnect successfully after coordinator deadline failures.
    • Stale or unhealthy connections are safely removed before retrying.

Cache the worker's coordinator gRPC clients until a call fails with a
transient error (Unavailable or a deadline). On such failures the cached
client is closed and dropped so the next call re-dials, which lets a
worker recover when a coordinator moves to a new address under the same
ID. Add a configurable HeartbeatTimeout (default 10s) bounding the worker
heartbeat so a stalled coordinator cannot block the heartbeat loop.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The coordinator client now applies configurable heartbeat timeouts, performs health checks against cached client instances, and conditionally evicts stale clients during transient failures. Stream and workspace-bundle failover paths use the updated checks, with recovery and timeout behavior covered by tests.

Changes

Coordinator client resilience

Layer / File(s) Summary
Heartbeat timeout configuration
internal/service/coordinator/config.go, internal/service/coordinator/config_test.go, internal/service/coordinator/client.go, internal/service/coordinator/client_test.go
Adds HeartbeatTimeout with a 10-second default and validation fallback, applies it to heartbeat contexts, and tests timeout cancellation.
Instance-aware client recovery
internal/service/coordinator/client.go
Health checks now accept resolved client instances, while transient and stale errors conditionally remove only the matching cached client.
Failover health-check integration
internal/service/coordinator/client.go, internal/service/coordinator/client_test.go
Stream opening and workspace-bundle uploads use cached clients for health checks, and heartbeat recovery across coordinator endpoints is tested.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HeartbeatClient
  participant CoordinatorA
  participant CoordinatorB
  HeartbeatClient->>CoordinatorA: Send heartbeat
  CoordinatorA-->>HeartbeatClient: Deadline exceeded
  HeartbeatClient->>CoordinatorB: Retry heartbeat
  CoordinatorB-->>HeartbeatClient: Successful heartbeat
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: reconnecting the worker to the coordinator after heartbeat timeout.
Description check ✅ Passed The PR description follows the template with Summary, Changes, Related Issues, and Checklist sections filled in.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/service/coordinator/client.go`:
- Around line 965-968: The health-check failure branches in attemptCall’s
failover paths must evict reset-eligible cached clients before continuing. In
internal/service/coordinator/client.go at lines 965-968 and 1127-1130, call
resetClientOnTransientError(member, memberClient, err) after recording the
failure and before continue; update both sites consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 66955373-d5da-4c67-bb6a-cff7fc04d4dd

📥 Commits

Reviewing files that changed from the base of the PR and between 5f36316 and 3a569d1.

📒 Files selected for processing (4)
  • internal/service/coordinator/client.go
  • internal/service/coordinator/client_test.go
  • internal/service/coordinator/config.go
  • internal/service/coordinator/config_test.go

Comment thread internal/service/coordinator/client.go Outdated
Comment on lines 965 to 968
if err := cli.isHealthy(ctx, memberClient); err != nil {
cli.recordFailure(err)
lastErr = err
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Evict reset-eligible clients after health-check failures.

attemptCall resets the exact cached instance, but these failover paths retain it after isHealthy fails. A stale connection therefore remains cached and is reused on later stream or bundle operations.

  • internal/service/coordinator/client.go#L965-L968: call cli.resetClientOnTransientError(member, memberClient, err) before continuing.
  • internal/service/coordinator/client.go#L1127-L1130: call cli.resetClientOnTransientError(member, memberClient, err) before continuing.
📍 Affects 1 file
  • internal/service/coordinator/client.go#L965-L968 (this comment)
  • internal/service/coordinator/client.go#L1127-L1130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/coordinator/client.go` around lines 965 - 968, The
health-check failure branches in attemptCall’s failover paths must evict
reset-eligible cached clients before continuing. In
internal/service/coordinator/client.go at lines 965-968 and 1127-1130, call
resetClientOnTransientError(member, memberClient, err) after recording the
failure and before continue; update both sites consistently.

Extend the transient-error client reset to the stream-failover and
workspace-bundle paths so a coordinator that moved address under the same
ID is evicted there too, not only on the dispatch path. Fold the
health-check and reset-on-failure into a single checkHealthy helper so
every member-loop site shares one policy, and cover the stream and bundle
failover paths with a regression test.

@yohamta0 yohamta0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See inline comment.

// checkHealthy verifies the coordinator is serving and evicts the cached
// client when the failure indicates a stale connection.
func (cli *clientImpl) checkHealthy(ctx context.Context, member exec.HostInfo, client *client) error {
if err := cli.isHealthy(ctx, client); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we bail out before the health check when ctx is already done?

Heartbeat shares one timeout across all coordinator attempts. If one coordinator uses the full timeout, the next healthy coordinator gets DeadlineExceeded immediately and this code closes its cached ClientConn, canceling any active log or artifact stream. I reproduced this with two coordinators.

Suggested change
if err := cli.isHealthy(ctx, client); err != nil {
if err := ctx.Err(); err != nil {
return err
}
if err := cli.isHealthy(ctx, client); err != nil {

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants