github.com/number571/tendermint@v0.34.11-gost/rpc/jsonrpc/client/ws_client.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	mrand "math/rand"
     8  	"net"
     9  	"net/http"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/gorilla/websocket"
    14  	metrics "github.com/rcrowley/go-metrics"
    15  
    16  	tmsync "github.com/number571/tendermint/internal/libs/sync"
    17  	"github.com/number571/tendermint/libs/service"
    18  	types "github.com/number571/tendermint/rpc/jsonrpc/types"
    19  )
    20  
    21  // WSOptions for WSClient.
    22  type WSOptions struct {
    23  	MaxReconnectAttempts uint          // maximum attempts to reconnect
    24  	ReadWait             time.Duration // deadline for any read op
    25  	WriteWait            time.Duration // deadline for any write op
    26  	PingPeriod           time.Duration // frequency with which pings are sent
    27  }
    28  
    29  // DefaultWSOptions returns default WS options.
    30  func DefaultWSOptions() WSOptions {
    31  	return WSOptions{
    32  		MaxReconnectAttempts: 10, // first: 2 sec, last: 17 min.
    33  		WriteWait:            10 * time.Second,
    34  		ReadWait:             0,
    35  		PingPeriod:           0,
    36  	}
    37  }
    38  
    39  // WSClient is a JSON-RPC client, which uses WebSocket for communication with
    40  // the remote server.
    41  //
    42  // WSClient is safe for concurrent use by multiple goroutines.
    43  type WSClient struct { // nolint: maligned
    44  	conn *websocket.Conn
    45  
    46  	Address  string // IP:PORT or /path/to/socket
    47  	Endpoint string // /websocket/url/endpoint
    48  	Dialer   func(string, string) (net.Conn, error)
    49  
    50  	// Single user facing channel to read RPCResponses from, closed only when the
    51  	// client is being stopped.
    52  	ResponsesCh chan types.RPCResponse
    53  
    54  	// Callback, which will be called each time after successful reconnect.
    55  	onReconnect func()
    56  
    57  	// internal channels
    58  	send            chan types.RPCRequest // user requests
    59  	backlog         chan types.RPCRequest // stores a single user request received during a conn failure
    60  	reconnectAfter  chan error            // reconnect requests
    61  	readRoutineQuit chan struct{}         // a way for readRoutine to close writeRoutine
    62  
    63  	// Maximum reconnect attempts (0 or greater; default: 25).
    64  	maxReconnectAttempts uint
    65  
    66  	// Support both ws and wss protocols
    67  	protocol string
    68  
    69  	wg sync.WaitGroup
    70  
    71  	mtx            tmsync.RWMutex
    72  	sentLastPingAt time.Time
    73  	reconnecting   bool
    74  	nextReqID      int
    75  	// sentIDs        map[types.JSONRPCIntID]bool // IDs of the requests currently in flight
    76  
    77  	// Time allowed to write a message to the server. 0 means block until operation succeeds.
    78  	writeWait time.Duration
    79  
    80  	// Time allowed to read the next message from the server. 0 means block until operation succeeds.
    81  	readWait time.Duration
    82  
    83  	// Send pings to server with this period. Must be less than readWait. If 0, no pings will be sent.
    84  	pingPeriod time.Duration
    85  
    86  	service.BaseService
    87  
    88  	// Time between sending a ping and receiving a pong. See
    89  	// https://godoc.org/github.com/rcrowley/go-metrics#Timer.
    90  	PingPongLatencyTimer metrics.Timer
    91  }
    92  
    93  // NewWS returns a new client. The endpoint argument must begin with a `/`. An
    94  // error is returned on invalid remote.
    95  // It uses DefaultWSOptions.
    96  func NewWS(remoteAddr, endpoint string) (*WSClient, error) {
    97  	return NewWSWithOptions(remoteAddr, endpoint, DefaultWSOptions())
    98  }
    99  
   100  // NewWSWithOptions allows you to provide custom WSOptions.
   101  func NewWSWithOptions(remoteAddr, endpoint string, opts WSOptions) (*WSClient, error) {
   102  	parsedURL, err := newParsedURL(remoteAddr)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	// default to ws protocol, unless wss is explicitly specified
   107  	if parsedURL.Scheme != protoWSS {
   108  		parsedURL.Scheme = protoWS
   109  	}
   110  
   111  	dialFn, err := makeHTTPDialer(remoteAddr)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	c := &WSClient{
   117  		Address:              parsedURL.GetTrimmedHostWithPath(),
   118  		Dialer:               dialFn,
   119  		Endpoint:             endpoint,
   120  		PingPongLatencyTimer: metrics.NewTimer(),
   121  
   122  		maxReconnectAttempts: opts.MaxReconnectAttempts,
   123  		readWait:             opts.ReadWait,
   124  		writeWait:            opts.WriteWait,
   125  		pingPeriod:           opts.PingPeriod,
   126  		protocol:             parsedURL.Scheme,
   127  
   128  		// sentIDs: make(map[types.JSONRPCIntID]bool),
   129  	}
   130  	c.BaseService = *service.NewBaseService(nil, "WSClient", c)
   131  	return c, nil
   132  }
   133  
   134  // OnReconnect sets the callback, which will be called every time after
   135  // successful reconnect.
   136  // Could only be set before Start.
   137  func (c *WSClient) OnReconnect(cb func()) {
   138  	c.onReconnect = cb
   139  }
   140  
   141  // String returns WS client full address.
   142  func (c *WSClient) String() string {
   143  	return fmt.Sprintf("WSClient{%s (%s)}", c.Address, c.Endpoint)
   144  }
   145  
   146  // OnStart implements service.Service by dialing a server and creating read and
   147  // write routines.
   148  func (c *WSClient) OnStart() error {
   149  	err := c.dial()
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	c.ResponsesCh = make(chan types.RPCResponse)
   155  
   156  	c.send = make(chan types.RPCRequest)
   157  	// 1 additional error may come from the read/write
   158  	// goroutine depending on which failed first.
   159  	c.reconnectAfter = make(chan error, 1)
   160  	// capacity for 1 request. a user won't be able to send more because the send
   161  	// channel is unbuffered.
   162  	c.backlog = make(chan types.RPCRequest, 1)
   163  
   164  	c.startReadWriteRoutines()
   165  	go c.reconnectRoutine()
   166  
   167  	return nil
   168  }
   169  
   170  // Stop overrides service.Service#Stop. There is no other way to wait until Quit
   171  // channel is closed.
   172  func (c *WSClient) Stop() error {
   173  	if err := c.BaseService.Stop(); err != nil {
   174  		return err
   175  	}
   176  	// only close user-facing channels when we can't write to them
   177  	c.wg.Wait()
   178  	close(c.ResponsesCh)
   179  
   180  	return nil
   181  }
   182  
   183  // IsReconnecting returns true if the client is reconnecting right now.
   184  func (c *WSClient) IsReconnecting() bool {
   185  	c.mtx.RLock()
   186  	defer c.mtx.RUnlock()
   187  	return c.reconnecting
   188  }
   189  
   190  // IsActive returns true if the client is running and not reconnecting.
   191  func (c *WSClient) IsActive() bool {
   192  	return c.IsRunning() && !c.IsReconnecting()
   193  }
   194  
   195  // Send the given RPC request to the server. Results will be available on
   196  // ResponsesCh, errors, if any, on ErrorsCh. Will block until send succeeds or
   197  // ctx.Done is closed.
   198  func (c *WSClient) Send(ctx context.Context, request types.RPCRequest) error {
   199  	select {
   200  	case c.send <- request:
   201  		c.Logger.Info("sent a request", "req", request)
   202  		// c.mtx.Lock()
   203  		// c.sentIDs[request.ID.(types.JSONRPCIntID)] = true
   204  		// c.mtx.Unlock()
   205  		return nil
   206  	case <-ctx.Done():
   207  		return ctx.Err()
   208  	}
   209  }
   210  
   211  // Call enqueues a call request onto the Send queue. Requests are JSON encoded.
   212  func (c *WSClient) Call(ctx context.Context, method string, params map[string]interface{}) error {
   213  	request, err := types.MapToRequest(c.nextRequestID(), method, params)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	return c.Send(ctx, request)
   218  }
   219  
   220  // CallWithArrayParams enqueues a call request onto the Send queue. Params are
   221  // in a form of array (e.g. []interface{}{"abcd"}). Requests are JSON encoded.
   222  func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, params []interface{}) error {
   223  	request, err := types.ArrayToRequest(c.nextRequestID(), method, params)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	return c.Send(ctx, request)
   228  }
   229  
   230  // Private methods
   231  
   232  func (c *WSClient) nextRequestID() types.JSONRPCIntID {
   233  	c.mtx.Lock()
   234  	id := c.nextReqID
   235  	c.nextReqID++
   236  	c.mtx.Unlock()
   237  	return types.JSONRPCIntID(id)
   238  }
   239  
   240  func (c *WSClient) dial() error {
   241  	dialer := &websocket.Dialer{
   242  		NetDial: c.Dialer,
   243  		Proxy:   http.ProxyFromEnvironment,
   244  	}
   245  	rHeader := http.Header{}
   246  	conn, _, err := dialer.Dial(c.protocol+"://"+c.Address+c.Endpoint, rHeader) // nolint:bodyclose
   247  	if err != nil {
   248  		return err
   249  	}
   250  	c.conn = conn
   251  	return nil
   252  }
   253  
   254  // reconnect tries to redial up to maxReconnectAttempts with exponential
   255  // backoff.
   256  func (c *WSClient) reconnect() error {
   257  	attempt := uint(0)
   258  
   259  	c.mtx.Lock()
   260  	c.reconnecting = true
   261  	c.mtx.Unlock()
   262  	defer func() {
   263  		c.mtx.Lock()
   264  		c.reconnecting = false
   265  		c.mtx.Unlock()
   266  	}()
   267  
   268  	for {
   269  		// nolint:gosec // G404: Use of weak random number generator
   270  		jitter := time.Duration(mrand.Float64() * float64(time.Second)) // 1s == (1e9 ns)
   271  		backoffDuration := jitter + ((1 << attempt) * time.Second)
   272  
   273  		c.Logger.Info("reconnecting", "attempt", attempt+1, "backoff_duration", backoffDuration)
   274  		time.Sleep(backoffDuration)
   275  
   276  		err := c.dial()
   277  		if err != nil {
   278  			c.Logger.Error("failed to redial", "err", err)
   279  		} else {
   280  			c.Logger.Info("reconnected")
   281  			if c.onReconnect != nil {
   282  				go c.onReconnect()
   283  			}
   284  			return nil
   285  		}
   286  
   287  		attempt++
   288  
   289  		if attempt > c.maxReconnectAttempts {
   290  			return fmt.Errorf("reached maximum reconnect attempts: %w", err)
   291  		}
   292  	}
   293  }
   294  
   295  func (c *WSClient) startReadWriteRoutines() {
   296  	c.wg.Add(2)
   297  	c.readRoutineQuit = make(chan struct{})
   298  	go c.readRoutine()
   299  	go c.writeRoutine()
   300  }
   301  
   302  func (c *WSClient) processBacklog() error {
   303  	select {
   304  	case request := <-c.backlog:
   305  		if c.writeWait > 0 {
   306  			if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil {
   307  				c.Logger.Error("failed to set write deadline", "err", err)
   308  			}
   309  		}
   310  		if err := c.conn.WriteJSON(request); err != nil {
   311  			c.Logger.Error("failed to resend request", "err", err)
   312  			c.reconnectAfter <- err
   313  			// requeue request
   314  			c.backlog <- request
   315  			return err
   316  		}
   317  		c.Logger.Info("resend a request", "req", request)
   318  	default:
   319  	}
   320  	return nil
   321  }
   322  
   323  func (c *WSClient) reconnectRoutine() {
   324  	for {
   325  		select {
   326  		case originalError := <-c.reconnectAfter:
   327  			// wait until writeRoutine and readRoutine finish
   328  			c.wg.Wait()
   329  			if err := c.reconnect(); err != nil {
   330  				c.Logger.Error("failed to reconnect", "err", err, "original_err", originalError)
   331  				if err = c.Stop(); err != nil {
   332  					c.Logger.Error("failed to stop conn", "error", err)
   333  				}
   334  
   335  				return
   336  			}
   337  			// drain reconnectAfter
   338  		LOOP:
   339  			for {
   340  				select {
   341  				case <-c.reconnectAfter:
   342  				default:
   343  					break LOOP
   344  				}
   345  			}
   346  			err := c.processBacklog()
   347  			if err == nil {
   348  				c.startReadWriteRoutines()
   349  			}
   350  
   351  		case <-c.Quit():
   352  			return
   353  		}
   354  	}
   355  }
   356  
   357  // The client ensures that there is at most one writer to a connection by
   358  // executing all writes from this goroutine.
   359  func (c *WSClient) writeRoutine() {
   360  	var ticker *time.Ticker
   361  	if c.pingPeriod > 0 {
   362  		// ticker with a predefined period
   363  		ticker = time.NewTicker(c.pingPeriod)
   364  	} else {
   365  		// ticker that never fires
   366  		ticker = &time.Ticker{C: make(<-chan time.Time)}
   367  	}
   368  
   369  	defer func() {
   370  		ticker.Stop()
   371  		c.conn.Close()
   372  		// err != nil {
   373  		// ignore error; it will trigger in tests
   374  		// likely because it's closing an already closed connection
   375  		// }
   376  		c.wg.Done()
   377  	}()
   378  
   379  	for {
   380  		select {
   381  		case request := <-c.send:
   382  			if c.writeWait > 0 {
   383  				if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil {
   384  					c.Logger.Error("failed to set write deadline", "err", err)
   385  				}
   386  			}
   387  			if err := c.conn.WriteJSON(request); err != nil {
   388  				c.Logger.Error("failed to send request", "err", err)
   389  				c.reconnectAfter <- err
   390  				// add request to the backlog, so we don't lose it
   391  				c.backlog <- request
   392  				return
   393  			}
   394  		case <-ticker.C:
   395  			if c.writeWait > 0 {
   396  				if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil {
   397  					c.Logger.Error("failed to set write deadline", "err", err)
   398  				}
   399  			}
   400  			if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
   401  				c.Logger.Error("failed to write ping", "err", err)
   402  				c.reconnectAfter <- err
   403  				return
   404  			}
   405  			c.mtx.Lock()
   406  			c.sentLastPingAt = time.Now()
   407  			c.mtx.Unlock()
   408  			c.Logger.Debug("sent ping")
   409  		case <-c.readRoutineQuit:
   410  			return
   411  		case <-c.Quit():
   412  			if err := c.conn.WriteMessage(
   413  				websocket.CloseMessage,
   414  				websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
   415  			); err != nil {
   416  				c.Logger.Error("failed to write message", "err", err)
   417  			}
   418  			return
   419  		}
   420  	}
   421  }
   422  
   423  // The client ensures that there is at most one reader to a connection by
   424  // executing all reads from this goroutine.
   425  func (c *WSClient) readRoutine() {
   426  	defer func() {
   427  		c.conn.Close()
   428  		// err != nil {
   429  		// ignore error; it will trigger in tests
   430  		// likely because it's closing an already closed connection
   431  		// }
   432  		c.wg.Done()
   433  	}()
   434  
   435  	c.conn.SetPongHandler(func(string) error {
   436  		// gather latency stats
   437  		c.mtx.RLock()
   438  		t := c.sentLastPingAt
   439  		c.mtx.RUnlock()
   440  		c.PingPongLatencyTimer.UpdateSince(t)
   441  
   442  		c.Logger.Debug("got pong")
   443  		return nil
   444  	})
   445  
   446  	for {
   447  		// reset deadline for every message type (control or data)
   448  		if c.readWait > 0 {
   449  			if err := c.conn.SetReadDeadline(time.Now().Add(c.readWait)); err != nil {
   450  				c.Logger.Error("failed to set read deadline", "err", err)
   451  			}
   452  		}
   453  		_, data, err := c.conn.ReadMessage()
   454  		if err != nil {
   455  			if !websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
   456  				return
   457  			}
   458  
   459  			c.Logger.Error("failed to read response", "err", err)
   460  			close(c.readRoutineQuit)
   461  			c.reconnectAfter <- err
   462  			return
   463  		}
   464  
   465  		var response types.RPCResponse
   466  		err = json.Unmarshal(data, &response)
   467  		if err != nil {
   468  			c.Logger.Error("failed to parse response", "err", err, "data", string(data))
   469  			continue
   470  		}
   471  
   472  		if err = validateResponseID(response.ID); err != nil {
   473  			c.Logger.Error("error in response ID", "id", response.ID, "err", err)
   474  			continue
   475  		}
   476  
   477  		// TODO: events resulting from /subscribe do not work with ->
   478  		// because they are implemented as responses with the subscribe request's
   479  		// ID. According to the spec, they should be notifications (requests
   480  		// without IDs).
   481  		// https://github.com/number571/tendermint/issues/2949
   482  		// c.mtx.Lock()
   483  		// if _, ok := c.sentIDs[response.ID.(types.JSONRPCIntID)]; !ok {
   484  		// 	c.Logger.Error("unsolicited response ID", "id", response.ID, "expected", c.sentIDs)
   485  		// 	c.mtx.Unlock()
   486  		// 	continue
   487  		// }
   488  		// delete(c.sentIDs, response.ID.(types.JSONRPCIntID))
   489  		// c.mtx.Unlock()
   490  		// Combine a non-blocking read on BaseService.Quit with a non-blocking write on ResponsesCh to avoid blocking
   491  		// c.wg.Wait() in c.Stop(). Note we rely on Quit being closed so that it sends unlimited Quit signals to stop
   492  		// both readRoutine and writeRoutine
   493  
   494  		c.Logger.Info("got response", "id", response.ID, "result", response.Result)
   495  
   496  		select {
   497  		case <-c.Quit():
   498  		case c.ResponsesCh <- response:
   499  		}
   500  	}
   501  }
   502  
   503  // Predefined methods
   504  
   505  // Subscribe to a query. Note the server must have a "subscribe" route
   506  // defined.
   507  func (c *WSClient) Subscribe(ctx context.Context, query string) error {
   508  	params := map[string]interface{}{"query": query}
   509  	return c.Call(ctx, "subscribe", params)
   510  }
   511  
   512  // Unsubscribe from a query. Note the server must have a "unsubscribe" route
   513  // defined.
   514  func (c *WSClient) Unsubscribe(ctx context.Context, query string) error {
   515  	params := map[string]interface{}{"query": query}
   516  	return c.Call(ctx, "unsubscribe", params)
   517  }
   518  
   519  // UnsubscribeAll from all. Note the server must have a "unsubscribe_all" route
   520  // defined.
   521  func (c *WSClient) UnsubscribeAll(ctx context.Context) error {
   522  	params := map[string]interface{}{}
   523  	return c.Call(ctx, "unsubscribe_all", params)
   524  }