Skip to content

fix(rpc): stop draining the ping timer channel on Reset - #3035

Merged
gzliudan merged 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:fix-websocket-ping-drain
Nov 12, 2026
Merged

gzliudan merged 1 commit into
XinFinOrg:dev-upgradefrom
gzliudan:fix-websocket-ping-drain

Conversation

@gzliudan

@gzliudan gzliudan commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

Proposed changes

pingLoop drains pingTimer.C when Stop reports false, then re-arms the timer. That was the documented idiom before Go 1.23, when timer channels were buffered: a value the select had not taken yet was still in the channel, so the drain returned and the Reset that followed re-armed the timer. Go 1.23 made the channel unbuffered, and the same receive now waits for a send that never arrives once the value has been taken, leaving pingLoop wedged with every later pingReset queued behind it.

Reset stops the timer and discards a pending value by itself, so the drain is dead weight with a live failure mode. The automatic re-arm after an idle ping is the only thing keeping it out of reach today. This mirrors #3034, which removes the same drain in common/countdown.

Summary

 pingLoop
   select
     case <-wc.closed()
       return
     case <-wc.pingReset
-      if !pingTimer.Stop() {
-          <-pingTimer.C            # blocks once the select took the value
-      }
       pingTimer.Reset(wsPingInterval)
     case <-pingTimer.C
       send ping frame
       pingTimer.Reset(wsPingInterval)   # automatic re-arm
     case <-wc.pongReceived
       clear read deadline

Evidence

  • Before: with the automatic re-arm removed on this branch's base (4ac2cc42cd) plus the const-to-variable test seam, the new test:

    websocket_pingloop_test.go:98: no ping after reset: the reset path is blocked
    FAIL  github.com/XinFinOrg/XDPoSChain/rpc  2.025s
    

    After: the same mutation on this branch:

    ok  github.com/XinFinOrg/XDPoSChain/rpc  0.128s
    

    The re-arm is what keeps the drain out of reach, so removing it is the smallest edit that reaches the branch. A pingReset arriving after the timer fired is the production shape: every successful writeJSON sends one.

  • New test matrices: -count=100 4.14s, -race -count=30 2.29s, -race -cpu=1 -count=20 1.86s, all green.

  • Full suite on f016616fd3: make all, make generate (No stale files detected), make tidy (No untidy module files detected), make lint (0 issues), go vet ./..., make test — all packages ok.

Merge Danger

Door: two-way

One drain removed and one constant turned into a variable with the same value; reverting restores both.

Blast Radius: rpc

rpc/websocket.go is the WebSocket transport for JSON-RPC, and every connection's pingLoop goes through this branch. The behaviour is unchanged on Go 1.23+: Reset already stopped the timer and discarded a pending value, so the drain only ever ran on a value that the next Reset overwrote anyway. The worst historical failure mode was a wedged pingLoop on a single connection.

Types of changes

What types of changes does your code introduce to XDC network?
Put an ✅ in the boxes that apply

  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Changes that don't change source code or tests
  • docs: Documentation only changes
  • feat: A new feature
  • fix: A bug fix
  • perf: A code change that improves performance
  • refactor: A code change that neither fixes a bug nor adds a feature
  • revert: Revert something
  • style: Changes that do not affect the meaning of the code
  • test: Adding missing tests or correcting existing tests

Impacted Components

Which parts of the codebase does this PR touch?
Put an ✅ in the boxes that apply

  • Consensus
  • Account
  • Network (rpc WebSocket transport)
  • Geth
  • Smart Contract
  • External components
  • Not sure (Please specify below)

Checklist

Put an ✅ in the boxes once you have confirmed below actions (or provide reasons on not doing so) that

  • This PR has sufficient test coverage (unit/integration test) OR I have provided reason in the PR description for not having test coverage
  • Tested on a private network from the genesis block and monitored the chain operating correctly for multiple epochs.
  • Provide an end-to-end test plan in the PR description on how to manually test it on the devnet/testnet.
  • Tested the backwards compatibility.
  • Tested with XDC nodes running this version co-exist with those running the previous version.
  • Relevant documentation has been updated as part of this PR
  • N/A

The unchecked items are node-level: on Go 1.23+ the change removes a drain that never ran on a live timer and turns a constant into a variable with the same value, so there is no devnet behaviour to observe, no cross-version difference to check, and no documentation to update.

@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 30 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between ab211d3 and 1a5d17e.

Files selected for processing: 2

  • rpc/websocket.go
  • rpc/websocket_pingloop_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@gzliudan
gzliudan force-pushed the fix-websocket-ping-drain branch from 3c7e396 to 2b9d9f0 Compare November 9, 2026 08:35
The pingLoop reset path drained pingTimer.C whenever Stop reported false,
which was the documented way to re-arm a timer before Go 1.23: the channel
was buffered, so a value the select had not taken yet was still there to
take. Go 1.23 made the channel unbuffered, and the same receive now waits
for a send that never comes once the value has been taken, wedging the
pingLoop goroutine with every later pingReset queued behind it.

Reset stops the timer and discards a pending value on its own, so the drain
is dead weight with a live failure mode: the automatic re-arm after an idle
ping is the only thing that keeps it out of reach today, and skipping that
re-arm turns it into a permanent block.

websocket_pingloop_test.go covers the path: a pingReset arriving after the
timer fired has to return and leave the timer armed for the next ping. With
the re-arm removed, that test fails inside its two second bound on the old
code and passes on this one. wsPingInterval becomes a variable so the test
can drive the timer at test speed.
@gzliudan
gzliudan force-pushed the fix-websocket-ping-drain branch from 2b9d9f0 to 1a5d17e Compare November 9, 2026 10:36

@benjamin202410 benjamin202410 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.

LGTM

@gzliudan
gzliudan merged commit 35a5a97 into XinFinOrg:dev-upgrade Nov 12, 2026
42 of 43 checks passed
@gzliudan
gzliudan deleted the fix-websocket-ping-drain branch November 12, 2026 09:35
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.

4 participants