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

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	tmsync "github.com/number571/tendermint/internal/libs/sync"
    11  	tmjson "github.com/number571/tendermint/libs/json"
    12  	tmpubsub "github.com/number571/tendermint/libs/pubsub"
    13  	"github.com/number571/tendermint/libs/service"
    14  	rpcclient "github.com/number571/tendermint/rpc/client"
    15  	ctypes "github.com/number571/tendermint/rpc/core/types"
    16  	jsonrpcclient "github.com/number571/tendermint/rpc/jsonrpc/client"
    17  )
    18  
    19  var errNotRunning = errors.New("client is not running. Use .Start() method to start")
    20  
    21  // WSOptions for the WS part of the HTTP client.
    22  type WSOptions struct {
    23  	Path string // path (e.g. "/ws")
    24  
    25  	jsonrpcclient.WSOptions // WSClient options
    26  }
    27  
    28  // DefaultWSOptions returns default WS options.
    29  // See jsonrpcclient.DefaultWSOptions.
    30  func DefaultWSOptions() WSOptions {
    31  	return WSOptions{
    32  		Path:      "/websocket",
    33  		WSOptions: jsonrpcclient.DefaultWSOptions(),
    34  	}
    35  }
    36  
    37  // Validate performs a basic validation of WSOptions.
    38  func (wso WSOptions) Validate() error {
    39  	if len(wso.Path) <= 1 {
    40  		return errors.New("empty Path")
    41  	}
    42  	if wso.Path[0] != '/' {
    43  		return errors.New("leading slash is missing in Path")
    44  	}
    45  
    46  	return nil
    47  }
    48  
    49  // wsEvents is a wrapper around WSClient, which implements EventsClient.
    50  type wsEvents struct {
    51  	service.BaseService
    52  	ws *jsonrpcclient.WSClient
    53  
    54  	mtx           tmsync.RWMutex
    55  	subscriptions map[string]*wsSubscription
    56  }
    57  
    58  type wsSubscription struct {
    59  	res   chan ctypes.ResultEvent
    60  	id    string
    61  	query string
    62  }
    63  
    64  var _ rpcclient.EventsClient = (*wsEvents)(nil)
    65  
    66  func newWsEvents(remote string, wso WSOptions) (*wsEvents, error) {
    67  	// validate options
    68  	if err := wso.Validate(); err != nil {
    69  		return nil, fmt.Errorf("invalid WSOptions: %w", err)
    70  	}
    71  
    72  	// remove the trailing / from the remote else the websocket endpoint
    73  	// won't parse correctly
    74  	if remote[len(remote)-1] == '/' {
    75  		remote = remote[:len(remote)-1]
    76  	}
    77  
    78  	w := &wsEvents{
    79  		subscriptions: make(map[string]*wsSubscription),
    80  	}
    81  	w.BaseService = *service.NewBaseService(nil, "wsEvents", w)
    82  
    83  	var err error
    84  	w.ws, err = jsonrpcclient.NewWSWithOptions(remote, wso.Path, wso.WSOptions)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("can't create WS client: %w", err)
    87  	}
    88  	w.ws.OnReconnect(func() {
    89  		// resubscribe immediately
    90  		w.redoSubscriptionsAfter(0 * time.Second)
    91  	})
    92  	w.ws.SetLogger(w.Logger)
    93  
    94  	return w, nil
    95  }
    96  
    97  // OnStart implements service.Service by starting WSClient and event loop.
    98  func (w *wsEvents) OnStart() error {
    99  	if err := w.ws.Start(); err != nil {
   100  		return err
   101  	}
   102  
   103  	go w.eventListener()
   104  
   105  	return nil
   106  }
   107  
   108  // OnStop implements service.Service by stopping WSClient.
   109  func (w *wsEvents) OnStop() {
   110  	if err := w.ws.Stop(); err != nil {
   111  		w.Logger.Error("Can't stop ws client", "err", err)
   112  	}
   113  }
   114  
   115  // Subscribe implements EventsClient by using WSClient to subscribe given
   116  // subscriber to query. By default, it returns a channel with cap=1. Error is
   117  // returned if it fails to subscribe.
   118  //
   119  // When reading from the channel, keep in mind there's a single events loop, so
   120  // if you don't read events for this subscription fast enough, other
   121  // subscriptions will slow down in effect.
   122  //
   123  // The channel is never closed to prevent clients from seeing an erroneous
   124  // event.
   125  //
   126  // It returns an error if wsEvents is not running.
   127  func (w *wsEvents) Subscribe(ctx context.Context, subscriber, query string,
   128  	outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) {
   129  
   130  	if !w.IsRunning() {
   131  		return nil, errNotRunning
   132  	}
   133  
   134  	if err := w.ws.Subscribe(ctx, query); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	outCap := 1
   139  	if len(outCapacity) > 0 {
   140  		outCap = outCapacity[0]
   141  	}
   142  
   143  	outc := make(chan ctypes.ResultEvent, outCap)
   144  	w.mtx.Lock()
   145  	defer w.mtx.Unlock()
   146  	// subscriber param is ignored because Tendermint will override it with
   147  	// remote IP anyway.
   148  	w.subscriptions[query] = &wsSubscription{res: outc, query: query}
   149  
   150  	return outc, nil
   151  }
   152  
   153  // Unsubscribe implements EventsClient by using WSClient to unsubscribe given
   154  // subscriber from query.
   155  //
   156  // It returns an error if wsEvents is not running.
   157  func (w *wsEvents) Unsubscribe(ctx context.Context, subscriber, query string) error {
   158  	if !w.IsRunning() {
   159  		return errNotRunning
   160  	}
   161  
   162  	if err := w.ws.Unsubscribe(ctx, query); err != nil {
   163  		return err
   164  	}
   165  
   166  	w.mtx.Lock()
   167  	info, ok := w.subscriptions[query]
   168  	if ok {
   169  		if info.id != "" {
   170  			delete(w.subscriptions, info.id)
   171  		}
   172  		delete(w.subscriptions, info.query)
   173  	}
   174  	w.mtx.Unlock()
   175  
   176  	return nil
   177  }
   178  
   179  // UnsubscribeAll implements EventsClient by using WSClient to unsubscribe
   180  // given subscriber from all the queries.
   181  //
   182  // It returns an error if wsEvents is not running.
   183  func (w *wsEvents) UnsubscribeAll(ctx context.Context, subscriber string) error {
   184  	if !w.IsRunning() {
   185  		return errNotRunning
   186  	}
   187  
   188  	if err := w.ws.UnsubscribeAll(ctx); err != nil {
   189  		return err
   190  	}
   191  
   192  	w.mtx.Lock()
   193  	w.subscriptions = make(map[string]*wsSubscription)
   194  	w.mtx.Unlock()
   195  
   196  	return nil
   197  }
   198  
   199  // After being reconnected, it is necessary to redo subscription to server
   200  // otherwise no data will be automatically received.
   201  func (w *wsEvents) redoSubscriptionsAfter(d time.Duration) {
   202  	time.Sleep(d)
   203  
   204  	ctx := context.Background()
   205  
   206  	w.mtx.Lock()
   207  	defer w.mtx.Unlock()
   208  
   209  	for q, info := range w.subscriptions {
   210  		if q != "" && q == info.id {
   211  			continue
   212  		}
   213  		err := w.ws.Subscribe(ctx, q)
   214  		if err != nil {
   215  			w.Logger.Error("failed to resubscribe", "query", q, "err", err)
   216  			delete(w.subscriptions, q)
   217  		}
   218  	}
   219  }
   220  
   221  func isErrAlreadySubscribed(err error) bool {
   222  	return strings.Contains(err.Error(), tmpubsub.ErrAlreadySubscribed.Error())
   223  }
   224  
   225  func (w *wsEvents) eventListener() {
   226  	for {
   227  		select {
   228  		case resp, ok := <-w.ws.ResponsesCh:
   229  			if !ok {
   230  				return
   231  			}
   232  
   233  			if resp.Error != nil {
   234  				w.Logger.Error("WS error", "err", resp.Error.Error())
   235  				// Error can be ErrAlreadySubscribed or max client (subscriptions per
   236  				// client) reached or Tendermint exited.
   237  				// We can ignore ErrAlreadySubscribed, but need to retry in other
   238  				// cases.
   239  				if !isErrAlreadySubscribed(resp.Error) {
   240  					// Resubscribe after 1 second to give Tendermint time to restart (if
   241  					// crashed).
   242  					w.redoSubscriptionsAfter(1 * time.Second)
   243  				}
   244  				continue
   245  			}
   246  
   247  			result := new(ctypes.ResultEvent)
   248  			err := tmjson.Unmarshal(resp.Result, result)
   249  			if err != nil {
   250  				w.Logger.Error("failed to unmarshal response", "err", err)
   251  				continue
   252  			}
   253  
   254  			w.mtx.RLock()
   255  			out, ok := w.subscriptions[result.Query]
   256  			if ok {
   257  				if _, idOk := w.subscriptions[result.SubscriptionID]; !idOk {
   258  					out.id = result.SubscriptionID
   259  					w.subscriptions[result.SubscriptionID] = out
   260  				}
   261  			}
   262  
   263  			w.mtx.RUnlock()
   264  			if ok {
   265  				select {
   266  				case out.res <- *result:
   267  				case <-w.Quit():
   268  					return
   269  				}
   270  			}
   271  		case <-w.Quit():
   272  			return
   273  		}
   274  	}
   275  }