Skip to content

fix(gravity): bound tunnel sends and isolate stalled endpoints - #319

Merged
jhaynie merged 2 commits into
mainfrom
fix/gravity-bounded-tunnel-send
Jul 27, 2026
Merged

fix(gravity): bound tunnel sends and isolate stalled endpoints#319
jhaynie merged 2 commits into
mainfrom
fix/gravity-bounded-tunnel-send

Conversation

@jhaynie

@jhaynie jhaynie commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • bound every Gravity tunnel packet and keepalive send to 5 seconds
  • quarantine and reconnect only the affected Gravity endpoint when a send stalls
  • serialize sends through a persistent per-stream worker so one blocked gRPC Send cannot indefinitely freeze Hadron TUN packet processing or create per-packet goroutines
  • copy packet ownership before potentially outliving the caller
  • cancel stream/client contexts before waiting for shutdown handlers

RCA

US Central repeatedly entered a state where Hadron control sessions, resource sync, readiness, and PSC health remained green while deployment VIP probes timed out and Ions suppressed routes. The failing VIPs moved among Hadrons and Ions, and restarting a Hadron process temporarily restored traffic.

Hadron's outbound dataplane synchronously called the bidirectional gRPC stream's Send. That API has no per-call context. If HTTP/2 flow control or a half-open stream wedged the call, the shared outbound TUN processing path could stop making progress indefinitely while control-plane health continued to look healthy. Keepalive sends detected a block but previously only logged it; they did not quarantine or reconnect the endpoint.

This change gives packet and keepalive sends one bounded path. When its deadline expires, it marks the stream and endpoint unhealthy, cancels that endpoint's streams through the existing endpoint disconnect path, and reconnects only that endpoint. Healthy sibling endpoints continue serving.

Tests

  • go test -race ./gravity -count=1
  • go test ./...
  • git diff --check

Focused regression coverage verifies:

  • a permanently blocked send returns within the configured deadline
  • only the affected endpoint reconnects
  • a sibling endpoint continues carrying traffic
  • a blocked keepalive triggers endpoint recovery
  • stream cancellation releases the blocked send worker (leak guard)
  • shutdown cancels I/O before waiting for handlers

Rollout / canary plan

  1. Review and merge this PR.
  2. Release a new go-common version separately.
  3. Bump Hadron only to that release on a focused infra branch.
  4. Build from source and canary one Hadron, preferably hadron-pmxhdgcl.
  5. Require full regional convergence, zero route issues, healthy PSC, and repeated per-Ion pinned checks for customer health, infra monitor, and edge.agentcompany.com.
  6. Confirm timeout/reconnect telemetry is endpoint-scoped and sibling Ions remain available.
  7. Soak, then roll the remaining Hadrons one at a time with convergence gates.

Operational note

This PR does not tag or release go-common, update Hadron dependencies, or deploy any production component.

Summary by CodeRabbit

  • Reliability
    • Tunnel packet and keepalive sends now time out instead of becoming indefinitely blocked.
    • Affected endpoints are automatically quarantined and reconnected without disrupting healthy endpoints.
    • Shutdown now cancels active tunnel operations promptly, preventing hangs during close.
  • Error Handling
    • Added a dedicated tunnel send-timeout error for clearer reporting and diagnostics.
  • Testing
    • Added coverage for blocked sends, endpoint recovery, keepalive behavior, and reliable shutdown.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb4e7cd4-2fc2-4fcf-bf6e-81e28cdaf67c

📥 Commits

Reviewing files that changed from the base of the PR and between 32df3f7 and 70e3174.

📒 Files selected for processing (2)
  • gravity/grpc_client.go
  • gravity/tunnel_dataplane_test.go
📝 Walkthrough

Walkthrough

Changes

The client now sends tunnel packets and keepalives through per-stream bounded queues, quarantines endpoints whose sends time out, cancels contexts before shutdown waits, and adds tests for blocked sends, endpoint isolation, reconnection, and handler cancellation.

Tunnel send reliability

Layer / File(s) Summary
Per-stream bounded send worker
gravity/grpc_client.go
Adds configurable send timeouts, per-stream send queues, worker-based gRPC sends, timeout errors, and endpoint quarantine.
Dataplane and keepalive integration
gravity/grpc_client.go
Routes packet and keepalive sends through the bounded path and validates circuit-breaker indexes.
Lifecycle and reconnection validation
gravity/grpc_client.go, gravity/tunnel_dataplane_test.go
Updates shutdown and reconnection-hook handling, with tests for blocked sends, endpoint isolation, and cancellation.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
gravity/grpc_client.go (1)

5885-5891: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-packet copy adds an allocation to the outbound hot path.

The copy is only needed because the send may outlive the caller. WritePacket/sendTunnelPacket are per-TUN-packet, so this is an extra alloc + memcpy for every packet in the steady state, on top of the channel handoff. Consider sourcing the copy from g.bufferPool, or passing ownership explicitly so callers that already own a private buffer skip the copy.

🤖 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 `@gravity/grpc_client.go` around lines 5885 - 5891, Update the per-packet
ownership handling in WritePacket/sendTunnelPacket to avoid allocating and
copying a new Data slice on every outbound packet. Reuse g.bufferPool for the
required copy, or propagate explicit buffer ownership so privately owned caller
buffers bypass it, while preserving immutable ownership for sends that can
outlive the caller.
🤖 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 `@gravity/grpc_client.go`:
- Around line 1453-1454: Update the comment near the send path to describe
serialization through the per-stream sendQueue worker instead of the removed
sendMu mutex, while retaining the note that timed-out sends quarantine and
reconnect only the affected endpoint.
- Around line 5892-5916: Update sendTunnelStreamBounded to detect a cancelled or
exited stream before attempting the unbuffered stream.sendQueue handoff,
returning ErrConnectionClosed immediately instead of waiting for the timeout.
Ensure the check covers the stream context and worker lifecycle so sendOnce
cannot leave later sends blocked, while preserving the existing timeout behavior
for an active worker.

---

Nitpick comments:
In `@gravity/grpc_client.go`:
- Around line 5885-5891: Update the per-packet ownership handling in
WritePacket/sendTunnelPacket to avoid allocating and copying a new Data slice on
every outbound packet. Reuse g.bufferPool for the required copy, or propagate
explicit buffer ownership so privately owned caller buffers bypass it, while
preserving immutable ownership for sends that can outlive the caller.
🪄 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

Run ID: 18bb649b-1a9d-4c00-a257-b255cb08f80f

📥 Commits

Reviewing files that changed from the base of the PR and between b817ad6 and 32df3f7.

📒 Files selected for processing (2)
  • gravity/grpc_client.go
  • gravity/tunnel_dataplane_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: build
  • GitHub Check: Analyze (go)
🔇 Additional comments (8)
gravity/grpc_client.go (7)

66-68: LGTM!

Also applies to: 302-302, 436-448, 582-582


3914-3929: LGTM!


4129-4133: LGTM!


5919-5931: LGTM!


5933-5948: LGTM!


6011-6013: LGTM!


6279-6306: 🎯 Functional Correctness

Tunnel health is tracked separately; this isn’t a circuit-breaker bug. Tunnel sends update StreamInfo and endpoint health on failure, so the g.circuitBreakers bounds check is just defensive and doesn’t leave breaker state stale.

			> Likely an incorrect or invalid review comment.
gravity/tunnel_dataplane_test.go (1)

62-63: LGTM!

Also applies to: 79-99, 238-288, 290-323, 325-343

Comment thread gravity/grpc_client.go Outdated
Comment thread gravity/grpc_client.go
@jhaynie
jhaynie merged commit 7b6bed0 into main Jul 27, 2026
5 checks passed
@jhaynie
jhaynie deleted the fix/gravity-bounded-tunnel-send branch July 27, 2026 19:26
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.

1 participant