github.com/franono/tendermint@v0.32.2-0.20200527150959-749313264ce9/rpc/jsonrpc/client/ws_client.go (about)

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