github.com/decred/politeia@v1.4.0/politeiawww/wsdcrdata/wsdcrdata.go (about)

     1  // Copyright (c) 2019-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package wsdcrdata provides a client for managing dcrdata websocket
     6  // subscriptions.
     7  package wsdcrdata
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/decred/dcrdata/v6/pubsub/psclient"
    17  	"github.com/decred/dcrdata/v6/semver"
    18  )
    19  
    20  type StatusT int
    21  
    22  const (
    23  	// Websocket statuses
    24  	StatusInvalid      StatusT = 0 // Invalid status
    25  	StatusOpen         StatusT = 1 // Websocket is open
    26  	StatusReconnecting StatusT = 2 // Websocket is attempting to reconnect
    27  	StatusShutdown     StatusT = 3 // Websocket client has been shutdown
    28  
    29  	// eventAddress is used to subscribe to events for a specific dcr
    30  	// address. The dcr address must be appended onto the eventAddress
    31  	// string.
    32  	eventAddress = "address:"
    33  
    34  	// eventNewBlock is used to subscribe to new block events.
    35  	eventNewBlock = "newblock"
    36  )
    37  
    38  var (
    39  	// ErrDuplicateSub is emitted when attempting to subscribe to an
    40  	// event that has already been subscribed to.
    41  	ErrDuplicateSub = errors.New("duplicate subscription")
    42  
    43  	// ErrSubNotFound is emitted when attempting to unsubscribe to an
    44  	// event that has not yet been subscribed to.
    45  	ErrSubNotFound = errors.New("subscription not found")
    46  
    47  	// ErrReconnecting is emitted when attempting to use the Client
    48  	// while it is in the process of reconnecting to dcrdata. All
    49  	// subscribe/unsubscribe actions that are attempted while the
    50  	// client is reconnecting are recorded and completed once the new
    51  	// connection has been made.
    52  	ErrReconnecting = errors.New("reconnecting to dcrdata")
    53  
    54  	// ErrShutdown is emitted when attempting to use the Client after
    55  	// it has already been shut down.
    56  	ErrShutdown = errors.New("client is shutdown")
    57  )
    58  
    59  const (
    60  	// Pending event actions
    61  	actionSubscribe   = "subscribe"
    62  	actionUnsubscribe = "unsubscribe"
    63  )
    64  
    65  // pendingEvent represents an event action (subscribe/unsubscribe) that is
    66  // attempted to be made while the Client is in a StateReconnecting state. The
    67  // pending event actions are replayed in the order in which they were received
    68  // once a new dcrdata connection has been established.
    69  type pendingEvent struct {
    70  	event  string // Websocket event
    71  	action string // Subscribe/unsubscribe
    72  }
    73  
    74  // Client is a dcrdata websocket client for managing dcrdata websocket
    75  // subscriptions.
    76  type Client struct {
    77  	sync.Mutex
    78  	url           string
    79  	status        StatusT             // Websocket status
    80  	client        *psclient.Client    // dcrdata websocket client
    81  	subscriptions map[string]struct{} // Active subscriptions
    82  
    83  	// pending contains events that were attempted to be subscribed to
    84  	// or unsubscribed from while the client was in a StateReconnecting
    85  	// state. Once a new connection has been established the pending
    86  	// events are replayed in the order in which they were received.
    87  	pending []pendingEvent
    88  }
    89  
    90  // statusSet sets the client status.
    91  func (c *Client) statusSet(s StatusT) {
    92  	c.Lock()
    93  	defer c.Unlock()
    94  
    95  	c.status = s
    96  }
    97  
    98  // clientSet sets the websocket client. The lock is held for this so that the
    99  // golang race detector doesn't complain when the a new client is created and
   100  // set on reconnection attempts.
   101  func (c *Client) clientSet(psc *psclient.Client) {
   102  	c.Lock()
   103  	defer c.Unlock()
   104  
   105  	c.client = psc
   106  }
   107  
   108  // subAdd adds an event subscription to the subscriptions map.
   109  func (c *Client) subAdd(event string) {
   110  	c.Lock()
   111  	defer c.Unlock()
   112  
   113  	c.subscriptions[event] = struct{}{}
   114  }
   115  
   116  // subDel removes an event subscription from the subscriptions map.
   117  func (c *Client) subDel(event string) {
   118  	c.Lock()
   119  	defer c.Unlock()
   120  
   121  	delete(c.subscriptions, event)
   122  }
   123  
   124  // subsGet returns a copy of the full subscriptions list.
   125  func (c *Client) subsGet() map[string]struct{} {
   126  	c.Lock()
   127  	defer c.Unlock()
   128  
   129  	s := make(map[string]struct{}, len(c.subscriptions))
   130  	for k := range c.subscriptions {
   131  		s[k] = struct{}{}
   132  	}
   133  
   134  	return s
   135  }
   136  
   137  // subsDel removes all of the subscriptions from the subscriptions map.
   138  func (c *Client) subsDel() {
   139  	c.Lock()
   140  	defer c.Unlock()
   141  
   142  	c.subscriptions = make(map[string]struct{})
   143  }
   144  
   145  // isSubscribed returns whether the client is subscribed to the provided event.
   146  func (c *Client) isSubscribed(event string) bool {
   147  	c.Lock()
   148  	defer c.Unlock()
   149  
   150  	_, ok := c.subscriptions[event]
   151  	return ok
   152  }
   153  
   154  // pendingAdd adds a pending event to the list of pending events.
   155  func (c *Client) pendingAdd(pe pendingEvent) {
   156  	c.Lock()
   157  	defer c.Unlock()
   158  
   159  	c.pending = append(c.pending, pe)
   160  }
   161  
   162  // pendingDel deletes the full list of pending events.
   163  func (c *Client) pendingDel() {
   164  	c.Lock()
   165  	defer c.Unlock()
   166  
   167  	c.pending = make([]pendingEvent, 0)
   168  }
   169  
   170  // pendingGet returns a copy of the pending events list.
   171  func (c *Client) pendingGet() []pendingEvent {
   172  	c.Lock()
   173  	defer c.Unlock()
   174  
   175  	p := make([]pendingEvent, 0, len(c.pending))
   176  	p = append(p, c.pending...)
   177  
   178  	return p
   179  }
   180  
   181  // subscribe subscribes the dcrdata client to an event.
   182  func (c *Client) subscribe(event string) error {
   183  	// Check connection status
   184  	switch c.Status() {
   185  	case StatusShutdown:
   186  		return ErrShutdown
   187  	case StatusReconnecting:
   188  		// Add to list of pending events
   189  		c.pendingAdd(pendingEvent{
   190  			event:  event,
   191  			action: actionSubscribe,
   192  		})
   193  		log.Debugf("Pending event added: subscribe %v", event)
   194  		return ErrReconnecting
   195  	}
   196  
   197  	// Ensure subscription doesn't already exist
   198  	if c.isSubscribed(event) {
   199  		return ErrDuplicateSub
   200  	}
   201  
   202  	// Subscribe
   203  	_, err := c.client.Subscribe(event)
   204  	if err != nil {
   205  		return fmt.Errorf("wcDcrdata failed to subscribe to %v: %v",
   206  			event, err)
   207  	}
   208  
   209  	log.Debugf("Subscribed to %v", event)
   210  
   211  	// Update subscriptions list
   212  	c.subAdd(event)
   213  
   214  	return nil
   215  }
   216  
   217  // unsubscribe ubsubscribes the dcrdata client from an event.
   218  func (c *Client) unsubscribe(event string) error {
   219  	// Check connection status
   220  	switch c.Status() {
   221  	case StatusShutdown:
   222  		return ErrShutdown
   223  	case StatusReconnecting:
   224  		// Add to list of pending events
   225  		c.pendingAdd(pendingEvent{
   226  			event:  event,
   227  			action: actionUnsubscribe,
   228  		})
   229  		log.Debugf("Pending event added: unsubscribe %v", event)
   230  		return ErrReconnecting
   231  	}
   232  
   233  	// Ensure subscription exists
   234  	if !c.isSubscribed(event) {
   235  		return ErrSubNotFound
   236  	}
   237  
   238  	// Unsubscribe
   239  	_, err := c.client.Unsubscribe(event)
   240  	if err != nil {
   241  		return fmt.Errorf("Client failed to unsubscribe from %v: %v",
   242  			event, err)
   243  	}
   244  
   245  	log.Debugf("Unsubscribed from %v", event)
   246  
   247  	// Update subscriptions list
   248  	c.subDel(event)
   249  
   250  	return nil
   251  }
   252  
   253  // Status returns the websocket status.
   254  func (c *Client) Status() StatusT {
   255  	log.Tracef("Status")
   256  
   257  	c.Lock()
   258  	defer c.Unlock()
   259  
   260  	return c.status
   261  }
   262  
   263  // AddressSubscribe subscribes to events for the provided address.
   264  func (c *Client) AddressSubscribe(address string) error {
   265  	log.Tracef("AddressSubscribe: %v", address)
   266  
   267  	return c.subscribe(eventAddress + address)
   268  }
   269  
   270  // AddressUnsubscribe unsubscribes from events for the provided address.
   271  func (c *Client) AddressUnsubscribe(address string) error {
   272  	log.Tracef("AddressUnsubscribe: %v", address)
   273  
   274  	return c.unsubscribe(eventAddress + address)
   275  }
   276  
   277  // NewBlockSubscribe subscibes to the new block event.
   278  func (c *Client) NewBlockSubscribe() error {
   279  	log.Tracef("NewBlockSubscribe")
   280  
   281  	return c.subscribe(eventNewBlock)
   282  }
   283  
   284  // NewBlockUnsubscribe unsubscibes from the new block event.
   285  func (c *Client) NewBlockUnsubscribe() error {
   286  	log.Tracef("NewBlockUnsubscribe")
   287  
   288  	return c.unsubscribe(eventNewBlock)
   289  }
   290  
   291  // Receive returns a new channel that receives websocket messages from the
   292  // dcrdata server.
   293  func (c *Client) Receive() <-chan *psclient.ClientMessage {
   294  	log.Tracef("Receive")
   295  
   296  	// Hold the lock to prevent the go race detector from complaining
   297  	// when the client is switched out on reconnection attempts.
   298  	c.Lock()
   299  	defer c.Unlock()
   300  
   301  	return c.client.Receive()
   302  }
   303  
   304  // Reconnect creates a new websocket client and subscribes to the same
   305  // subscriptions as the previous client. If a connection cannot be established,
   306  // this function will continue to episodically attempt to reconnect until
   307  // either a connection is made or the application is shut down. If any new
   308  // subscribe/unsubscribe events are registered during this reconnection
   309  // process, they are added to a pending events list and are replayed in the
   310  // order in which they are received once a new connection has been established.
   311  func (c *Client) Reconnect() {
   312  	log.Tracef("Reconnect")
   313  
   314  	// Update client status
   315  	c.statusSet(StatusReconnecting)
   316  
   317  	// prevSubs is used to track the subscriptions that existed prior
   318  	// to being disconnected so that we can resubscribe to them after
   319  	// establishing a new connection.
   320  	prevSubs := c.subsGet()
   321  
   322  	// Clear out disconnected client subscriptions
   323  	c.subsDel()
   324  
   325  	// timeToWait specifies the time to wait in between reconnection
   326  	// attempts. This limit is increased if reconnection attempts fail.
   327  	timeToWait := 1 * time.Minute
   328  
   329  	// Keep attempting to reconnect until a new connection has been
   330  	// made and all previous subscriptions have been resubscribed to.
   331  	var done bool
   332  	for !done {
   333  		log.Infof("Attempting to reconnect dcrdata websocket")
   334  
   335  		// Reconnect to dcrdata
   336  		client, err := psclientNew(c.url)
   337  		if err != nil {
   338  			log.Errorf("New client failed: %v", err)
   339  			goto wait
   340  		}
   341  		c.clientSet(client)
   342  
   343  		// Connection open again. Update status.
   344  		c.statusSet(StatusOpen)
   345  
   346  		// Resubscribe to previous event subscriptions
   347  		for event := range prevSubs {
   348  			// Ensure not already subscribed
   349  			if c.isSubscribed(event) {
   350  				continue
   351  			}
   352  
   353  			// Subscribe
   354  			_, err := c.client.Subscribe(event)
   355  			if err != nil {
   356  				log.Errorf("Failed to subscribe to %v: %v", event, err)
   357  				goto wait
   358  			}
   359  
   360  			// Update subscriptions list
   361  			c.subAdd(event)
   362  			log.Debugf("Subscribed to %v", event)
   363  		}
   364  
   365  		// Replay any pending event actions that were registered while
   366  		// the client was attempting to reconnect.
   367  		for _, v := range c.pendingGet() {
   368  			switch v.action {
   369  			case actionSubscribe:
   370  				// Ensure not already subscribed
   371  				if c.isSubscribed(v.event) {
   372  					continue
   373  				}
   374  
   375  				// Subscribe
   376  				_, err := c.client.Subscribe(v.event)
   377  				if err != nil {
   378  					log.Errorf("Failed to subscribe to %v: %v", v.event, err)
   379  					goto wait
   380  				}
   381  
   382  				// Update subscriptions list
   383  				c.subAdd(v.event)
   384  				log.Debugf("Subscribed to %v", v.event)
   385  
   386  			case actionUnsubscribe:
   387  				// Ensure not already unsubscribed
   388  				if !c.isSubscribed(v.event) {
   389  					continue
   390  				}
   391  
   392  				// Unsubscribe
   393  				_, err := c.client.Unsubscribe(v.event)
   394  				if err != nil {
   395  					log.Errorf("Failed to unsubscribe to %v: %v", v.event, err)
   396  					goto wait
   397  				}
   398  
   399  				// Update subscriptions list
   400  				c.subDel(v.event)
   401  				log.Debugf("Unsubscribed from %v", v.event)
   402  
   403  			default:
   404  				log.Errorf("unknown pending event action: %v", v.action)
   405  			}
   406  		}
   407  
   408  		// Clear out pending events list. These have all been replayed.
   409  		c.pendingDel()
   410  
   411  		// We're done!
   412  		done = true
   413  		continue
   414  
   415  	wait:
   416  		// Websocket connection is either still closed or closed again
   417  		// before we were able to re-subscribe to all events. Update
   418  		// websocket status and retry again after wait time has elapsed.
   419  		c.statusSet(StatusReconnecting)
   420  
   421  		log.Infof("Dcrdata websocket reconnect waiting %v", timeToWait)
   422  		time.Sleep(timeToWait)
   423  
   424  		// Increase the wait time until it reaches 15m and then try to
   425  		// reconnect every 15m.
   426  		limit := 15 * time.Minute
   427  		timeToWait *= 2
   428  		if timeToWait > limit {
   429  			timeToWait = limit
   430  		}
   431  	}
   432  }
   433  
   434  // Close closes the dcrdata websocket client.
   435  func (c *Client) Close() error {
   436  	log.Tracef("Close")
   437  
   438  	// Update websocket status
   439  	c.statusSet(StatusShutdown)
   440  
   441  	// Clear out subscriptions list
   442  	c.subsDel()
   443  
   444  	// Close connection
   445  	return c.client.Close()
   446  }
   447  
   448  func psclientNew(url string) (*psclient.Client, error) {
   449  	opts := psclient.Opts{
   450  		ReadTimeout:  psclient.DefaultReadTimeout,
   451  		WriteTimeout: 3 * time.Second,
   452  	}
   453  	c, err := psclient.New(url, context.Background(), &opts)
   454  	if err != nil {
   455  		return nil, fmt.Errorf("failed to connect to %v: %v", url, err)
   456  	}
   457  
   458  	log.Infof("Dcrdata websocket host: %v", url)
   459  
   460  	// Check client and server compatibility
   461  	v, err := c.ServerVersion()
   462  	if err != nil {
   463  		return nil, fmt.Errorf("server version failed: %v", err)
   464  	}
   465  	serverSemVer := semver.NewSemver(v.Major, v.Minor, v.Patch)
   466  	clientSemVer := psclient.Version()
   467  	if !semver.Compatible(clientSemVer, serverSemVer) {
   468  		return nil, fmt.Errorf("version mismatch; client %v, server %v",
   469  			serverSemVer, clientSemVer)
   470  	}
   471  
   472  	log.Infof("Dcrdata pubsub server version: %v, client version %v",
   473  		serverSemVer, clientSemVer)
   474  
   475  	return c, nil
   476  }
   477  
   478  // New returns a new Client.
   479  func New(dcrdataURL string) (*Client, error) {
   480  	log.Tracef("New: %v", dcrdataURL)
   481  
   482  	// Setup dcrdata connection. If there is an error when connecting
   483  	// to dcrdata, return both the error and the Client so that the
   484  	// caller can decide if reconnection attempts should be made.
   485  	var status StatusT
   486  	c, err := psclientNew(dcrdataURL)
   487  	if err == nil {
   488  		// Connection is good
   489  		status = StatusOpen
   490  	} else {
   491  		// Unable to make a connection
   492  		c = &psclient.Client{}
   493  		status = StatusShutdown
   494  	}
   495  
   496  	return &Client{
   497  		url:           dcrdataURL,
   498  		status:        status,
   499  		client:        c,
   500  		subscriptions: make(map[string]struct{}),
   501  		pending:       make([]pendingEvent, 0),
   502  	}, err
   503  }