github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/dcrdata/dcrdata.go (about)

     1  // Copyright (c) 2020-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 dcrdata
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"sync"
    11  
    12  	"github.com/decred/dcrd/chaincfg/v3"
    13  	exptypes "github.com/decred/dcrdata/v6/explorer/types"
    14  	pstypes "github.com/decred/dcrdata/v6/pubsub/types"
    15  	backend "github.com/decred/politeia/politeiad/backendv2"
    16  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    17  	"github.com/decred/politeia/politeiad/plugins/dcrdata"
    18  	"github.com/decred/politeia/politeiawww/wsdcrdata"
    19  	"github.com/decred/politeia/util"
    20  )
    21  
    22  const (
    23  	// Dcrdata http routes
    24  	routeBestBlock    = "/api/block/best"
    25  	routeBlockDetails = "/api/block/{height}"
    26  	routeTicketPool   = "/api/stake/pool/b/{hash}/full"
    27  	routeTxsTrimmed   = "/api/txs/trimmed"
    28  
    29  	// Request headers
    30  	headerContentType = "Content-Type"
    31  
    32  	// Header values
    33  	contentTypeJSON = "application/json; charset=utf-8"
    34  )
    35  
    36  var (
    37  	_ plugins.PluginClient = (*dcrdataPlugin)(nil)
    38  )
    39  
    40  // dcrdataPlugin is the tstore backend implementation of the dcrdata plugin.
    41  // The dcrdata plugin provides and API for interacting with the dcrdata http
    42  // and websocket APIs.
    43  //
    44  // dcrdataPlugin satisfies the plugins PluginClient interface.
    45  type dcrdataPlugin struct {
    46  	sync.Mutex
    47  	activeNetParams *chaincfg.Params
    48  	client          *http.Client
    49  	ws              *wsdcrdata.Client
    50  
    51  	// Plugin settings
    52  	hostHTTP string // dcrdata HTTP host
    53  	hostWS   string // dcrdata websocket host
    54  
    55  	// bestBlock is the cached best block height. This field is kept up
    56  	// to date by the websocket connection. If the websocket connection
    57  	// drops, the best block is marked as stale and is not marked as
    58  	// current again until the connection has been re-established and
    59  	// a new best block message is received.
    60  	bestBlock      uint32
    61  	bestBlockStale bool
    62  }
    63  
    64  // bestBlockGet returns the cached best block.
    65  func (p *dcrdataPlugin) bestBlockGet() uint32 {
    66  	p.Lock()
    67  	defer p.Unlock()
    68  
    69  	return p.bestBlock
    70  }
    71  
    72  // bestBlockSet sets the cached best block to a new value.
    73  func (p *dcrdataPlugin) bestBlockSet(bb uint32) {
    74  	p.Lock()
    75  	defer p.Unlock()
    76  
    77  	p.bestBlock = bb
    78  	p.bestBlockStale = false
    79  }
    80  
    81  // bestBlockSetStale marks the cached best block as stale.
    82  func (p *dcrdataPlugin) bestBlockSetStale() {
    83  	p.Lock()
    84  	defer p.Unlock()
    85  
    86  	p.bestBlockStale = true
    87  }
    88  
    89  // bestBlockIsStale returns whether the cached best block has been marked as
    90  // being stale.
    91  func (p *dcrdataPlugin) bestBlockIsStale() bool {
    92  	p.Lock()
    93  	defer p.Unlock()
    94  
    95  	return p.bestBlockStale
    96  }
    97  
    98  func (p *dcrdataPlugin) websocketMonitor() {
    99  	defer func() {
   100  		log.Infof("Dcrdata websocket closed")
   101  	}()
   102  
   103  	// Setup messages channel
   104  	receiver := p.ws.Receive()
   105  
   106  	for {
   107  		// Monitor for a new message
   108  		msg, ok := <-receiver
   109  		if !ok {
   110  			// Check if the websocket was shut down intentionally or was
   111  			// dropped unexpectedly.
   112  			if p.ws.Status() == wsdcrdata.StatusShutdown {
   113  				return
   114  			}
   115  			log.Infof("Dcrdata websocket connection unexpectedly dropped")
   116  			goto reconnect
   117  		}
   118  
   119  		// Handle new message
   120  		switch m := msg.Message.(type) {
   121  		case *exptypes.WebsocketBlock:
   122  			log.Debugf("WebsocketBlock: %v", m.Block.Height)
   123  
   124  			// Update cached best block
   125  			p.bestBlockSet(uint32(m.Block.Height))
   126  
   127  		case *pstypes.HangUp:
   128  			log.Infof("Dcrdata websocket has hung up. Will reconnect.")
   129  			goto reconnect
   130  
   131  		case int:
   132  			// Ping messages are of type int
   133  
   134  		default:
   135  			log.Errorf("ws message of type %v unhandled: %v",
   136  				msg.EventId, m)
   137  		}
   138  
   139  		// Check for next message
   140  		continue
   141  
   142  	reconnect:
   143  		// Mark cached best block as stale
   144  		p.bestBlockSetStale()
   145  
   146  		// Reconnect
   147  		p.ws.Reconnect()
   148  
   149  		// Setup a new messages channel using the new connection.
   150  		receiver = p.ws.Receive()
   151  
   152  		log.Infof("Dcrdata websocket successfully reconnected")
   153  	}
   154  }
   155  
   156  func (p *dcrdataPlugin) websocketSetup() {
   157  	// Setup websocket subscriptions
   158  	var done bool
   159  	for !done {
   160  		// Best block
   161  		err := p.ws.NewBlockSubscribe()
   162  		if err != nil && err != wsdcrdata.ErrDuplicateSub {
   163  			log.Errorf("dcrdataPlugin: NewBlockSubscribe: %v", err)
   164  			goto reconnect
   165  		}
   166  
   167  		// All subscriptions setup
   168  		done = true
   169  		continue
   170  
   171  	reconnect:
   172  		p.ws.Reconnect()
   173  	}
   174  
   175  	// Monitor websocket connection
   176  	go p.websocketMonitor()
   177  }
   178  
   179  // Setup performs any plugin setup that is required.
   180  //
   181  // This function satisfies the plugins PluginClient interface.
   182  func (p *dcrdataPlugin) Setup() error {
   183  	log.Tracef("dcrdata Setup")
   184  
   185  	// Setup dcrdata websocket subscriptions and monitoring. This is
   186  	// done in a go routine so setup will continue in the event that
   187  	// a dcrdata websocket connection was not able to be made during
   188  	// client initialization and reconnection attempts are required.
   189  	go p.websocketSetup()
   190  
   191  	return nil
   192  }
   193  
   194  // Cmd executes a plugin command.
   195  //
   196  // This function satisfies the plugins PluginClient interface.
   197  func (p *dcrdataPlugin) Cmd(token []byte, cmd, payload string) (string, error) {
   198  	log.Tracef("dcrdata Cmd: %x %v %v", token, cmd, payload)
   199  
   200  	switch cmd {
   201  	case dcrdata.CmdBestBlock:
   202  		return p.cmdBestBlock(payload)
   203  	case dcrdata.CmdBlockDetails:
   204  		return p.cmdBlockDetails(payload)
   205  	case dcrdata.CmdTicketPool:
   206  		return p.cmdTicketPool(payload)
   207  	case dcrdata.CmdTxsTrimmed:
   208  		return p.cmdTxsTrimmed(payload)
   209  	}
   210  
   211  	return "", backend.ErrPluginCmdInvalid
   212  }
   213  
   214  // Hook executes a plugin hook.
   215  //
   216  // This function satisfies the plugins PluginClient interface.
   217  func (p *dcrdataPlugin) Hook(h plugins.HookT, payload string) error {
   218  	log.Tracef("dcrdata Hook: %v", plugins.Hooks[h])
   219  
   220  	return nil
   221  }
   222  
   223  // Fsck performs a plugin file system check. The plugin is provided with the
   224  // tokens for all records in the backend.
   225  //
   226  // This function satisfies the plugins PluginClient interface.
   227  func (p *dcrdataPlugin) Fsck(tokens [][]byte) error {
   228  	log.Tracef("dcrdata Fsck")
   229  
   230  	return nil
   231  }
   232  
   233  // Settings returns the plugin's settings.
   234  //
   235  // This function satisfies the plugins PluginClient interface.
   236  func (p *dcrdataPlugin) Settings() []backend.PluginSetting {
   237  	log.Tracef("dcrdata Settings")
   238  
   239  	return nil
   240  }
   241  
   242  func New(settings []backend.PluginSetting, activeNetParams *chaincfg.Params) (*dcrdataPlugin, error) {
   243  	// Plugin setting
   244  	var (
   245  		hostHTTP string
   246  		hostWS   string
   247  	)
   248  
   249  	// Set plugin settings to defaults. These will be overwritten if
   250  	// the setting was specified by the user.
   251  	switch activeNetParams.Name {
   252  	case chaincfg.MainNetParams().Name:
   253  		hostHTTP = dcrdata.SettingHostHTTPMainNet
   254  		hostWS = dcrdata.SettingHostWSMainNet
   255  	case chaincfg.TestNet3Params().Name:
   256  		hostHTTP = dcrdata.SettingHostHTTPTestNet
   257  		hostWS = dcrdata.SettingHostWSTestNet
   258  	default:
   259  		return nil, fmt.Errorf("unknown active net: %v", activeNetParams.Name)
   260  	}
   261  
   262  	// Override defaults with any passed in settings
   263  	for _, v := range settings {
   264  		switch v.Key {
   265  		case dcrdata.SettingKeyHostHTTP:
   266  			hostHTTP = v.Value
   267  			log.Infof("Plugin setting updated: dcrdata %v %v",
   268  				dcrdata.SettingKeyHostHTTP, hostHTTP)
   269  
   270  		case dcrdata.SettingKeyHostWS:
   271  			hostWS = v.Value
   272  			log.Infof("Plugin setting updated: dcrdata %v %v",
   273  				dcrdata.SettingKeyHostWS, hostWS)
   274  
   275  		default:
   276  			return nil, fmt.Errorf("invalid plugin setting '%v'", v.Key)
   277  		}
   278  	}
   279  
   280  	// Setup http client
   281  	log.Infof("Dcrdata HTTP host: %v", hostHTTP)
   282  	client, err := util.NewHTTPClient(false, "")
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	// Setup websocket client
   288  	ws, err := wsdcrdata.New(hostWS)
   289  	if err != nil {
   290  		// Continue even if a websocket connection was not able to be
   291  		// made. Reconnection attempts will be made in the plugin setup.
   292  		log.Errorf("wsdcrdata New: %v", err)
   293  	}
   294  
   295  	return &dcrdataPlugin{
   296  		activeNetParams: activeNetParams,
   297  		client:          client,
   298  		ws:              ws,
   299  		hostHTTP:        hostHTTP,
   300  		hostWS:          hostWS,
   301  	}, nil
   302  }