Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"time"

"github.com/canopy-network/canopy/fsm"

Expand All @@ -26,6 +27,13 @@ func NewClient(rpcURL, adminRPCUrl string) *Client {
return &Client{rpcURL: rpcURL, adminRpcUrl: adminRPCUrl, client: http.Client{}}
}

// NewClientWithTimeout() creates a Client that aborts requests exceeding the given timeout.
// Required for callers that hold critical locks (e.g. the root-chain manager holds the
// controller mutex), where a hung request would wedge the node.
func NewClientWithTimeout(rpcURL, adminRPCUrl string, timeout time.Duration) *Client {
return &Client{rpcURL: rpcURL, adminRpcUrl: adminRPCUrl, client: http.Client{Timeout: timeout}}
}

func (c *Client) Version() (version *string, err lib.ErrorI) {
version = new(string)
err = c.get(VersionRouteName, "", version)
Expand Down
27 changes: 26 additions & 1 deletion cmd/rpc/sock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rpc

import (
"fmt"
"net"
"net/http"
"net/url"
"slices"
Expand Down Expand Up @@ -34,6 +35,14 @@ const (
// maxReconnectRetries is effectively "retry indefinitely" — the root-chain subscription is
// essential, so we keep attempting to reconnect (at the capped interval) rather than giving up.
maxReconnectRetries = uint64(1) << 62
// rootChainRequestTimeout bounds on-demand root-chain HTTP calls; these often run while
// holding the controller mutex, so an unbounded hang would wedge the node until restart
rootChainRequestTimeout = 10 * time.Second
// rcSubscriptionReadTimeout is the max quiet period on the subscription websocket before it
// is considered dead and redialed (the publisher pings every 50s by default)
rcSubscriptionReadTimeout = 3 * defaultRCSubscriberPingPeriod
// rcSubscriptionPongWriteTimeout bounds pong control-frame writes on the subscription websocket
rcSubscriptionPongWriteTimeout = 10 * time.Second
)

// lotteryCache holds a single cached lottery result keyed by (height, id).
Expand Down Expand Up @@ -429,7 +438,7 @@ func (r *RCManager) NewSubscription(rc lib.RootChain) {
chainId: rc.ChainId,
Info: new(lib.RootChainInfo),
manager: r,
Client: NewClient(rc.Url, rc.Url),
Client: NewClientWithTimeout(rc.Url, rc.Url, rootChainRequestTimeout),
log: r.log,
}
// start to connect with backoff
Expand Down Expand Up @@ -498,13 +507,29 @@ func (r *RCSubscription) dialWithBackoff(chainId uint64, config lib.RootChain) {

// Listen() begins listening on the websockets client
func (r *RCSubscription) Listen() {
// arm a read deadline so a half-open connection errors out and triggers a redial
_ = r.conn.SetReadDeadline(time.Now().Add(rcSubscriptionReadTimeout))
r.conn.SetPingHandler(func(appData string) error {
// a ping proves liveness, refresh the deadline
_ = r.conn.SetReadDeadline(time.Now().Add(rcSubscriptionReadTimeout))
// reply with a pong (mirrors the default gorilla ping handler)
err := r.conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(rcSubscriptionPongWriteTimeout))
if err == websocket.ErrCloseSent {
return nil
} else if e, ok := err.(net.Error); ok && e.Timeout() {
return nil
}
return err
})
for {
// get the message from the buffer
_, bz, err := r.conn.ReadMessage()
if err != nil {
r.Stop(err)
return
}
// a message proves liveness, refresh the deadline
_ = r.conn.SetReadDeadline(time.Now().Add(rcSubscriptionReadTimeout))
// read the message into a rootChainInfo struct
newInfo := new(lib.RootChainInfo)
// unmarshal proto bytes into the message
Expand Down
Loading