|
|
@ -0,0 +1,318 @@ |
|
|
|
package crawler |
|
|
|
|
|
|
|
import ( |
|
|
|
"fmt" |
|
|
|
rpctypes "github.com/tendermint/tendermint/rpc/core/types" |
|
|
|
rpcclient "github.com/tendermint/tendermint/rpc/core_client" |
|
|
|
types "github.com/tendermint/tendermint/types" |
|
|
|
"sync" |
|
|
|
"time" |
|
|
|
) |
|
|
|
|
|
|
|
const ( |
|
|
|
CheckQueueBufferSize = 100 |
|
|
|
NodeQueueBufferSize = 100 |
|
|
|
) |
|
|
|
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
|
|
// crawler.Node
|
|
|
|
|
|
|
|
// A node is a peer on the network.
|
|
|
|
type Node struct { |
|
|
|
Host string |
|
|
|
P2PPort uint16 |
|
|
|
RPCPort uint16 |
|
|
|
|
|
|
|
failed int |
|
|
|
connected bool |
|
|
|
|
|
|
|
client *NodeClient |
|
|
|
|
|
|
|
LastSeen time.Time |
|
|
|
GenesisHash []byte |
|
|
|
BlockHeight uint |
|
|
|
NetInfo *rpctypes.ResponseNetInfo |
|
|
|
|
|
|
|
Validator bool |
|
|
|
|
|
|
|
// other peers we heard about this peer from
|
|
|
|
heardFrom map[string]struct{} |
|
|
|
} |
|
|
|
|
|
|
|
func (n *Node) Address() string { |
|
|
|
return fmt.Sprintf("%s:%d", n.Host, n.RPCPort) |
|
|
|
} |
|
|
|
|
|
|
|
// Set the basic status and network info for a node from RPC responses
|
|
|
|
func (n *Node) SetInfo(status *rpctypes.ResponseStatus, netinfo *rpctypes.ResponseNetInfo) { |
|
|
|
n.LastSeen = time.Now() |
|
|
|
n.GenesisHash = status.GenesisHash |
|
|
|
n.BlockHeight = status.LatestBlockHeight |
|
|
|
n.NetInfo = netinfo |
|
|
|
// n.Validator
|
|
|
|
} |
|
|
|
|
|
|
|
// A node client is used to talk to a node over rpc and websockets
|
|
|
|
type NodeClient struct { |
|
|
|
rpc rpcclient.Client |
|
|
|
ws *rpcclient.WSClient |
|
|
|
} |
|
|
|
|
|
|
|
// Create a new client for the node at the given addr
|
|
|
|
func NewNodeClient(addr string) *NodeClient { |
|
|
|
return &NodeClient{ |
|
|
|
rpc: rpcclient.NewClient(addr, "JSONRPC"), |
|
|
|
ws: rpcclient.NewWSClient(addr), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// A simple wrapper for mediating access to the maps
|
|
|
|
type nodeInfo struct { |
|
|
|
host string // the new nodes address
|
|
|
|
port uint16 |
|
|
|
from string // the peer that told us about this node
|
|
|
|
removed bool // whether to remove from nodePool
|
|
|
|
} |
|
|
|
|
|
|
|
func (ni nodeInfo) unpack() (string, uint16, string, bool) { |
|
|
|
return ni.host, ni.port, ni.from, ni.removed |
|
|
|
} |
|
|
|
|
|
|
|
// crawler.Node
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
|
|
// crawler.Crawler
|
|
|
|
|
|
|
|
// A crawler has a local node, a set of potential nodes in the nodePool
|
|
|
|
// and connected nodes. Maps are only accessed by one go-routine, mediated through channels
|
|
|
|
type Crawler struct { |
|
|
|
self *Node |
|
|
|
client *NodeClient |
|
|
|
|
|
|
|
checkQueue chan nodeInfo |
|
|
|
nodePool map[string]*Node |
|
|
|
nodes map[string]*Node |
|
|
|
|
|
|
|
nodeQueue chan *Node |
|
|
|
quit chan struct{} |
|
|
|
|
|
|
|
// waits for checkQueue to empty
|
|
|
|
// so we can re-poll all nodes
|
|
|
|
wg sync.WaitGroup |
|
|
|
} |
|
|
|
|
|
|
|
// Create a new Crawler using the local RPC server at addr
|
|
|
|
func NewCrawler(host string, port uint16) *Crawler { |
|
|
|
return &Crawler{ |
|
|
|
self: &Node{Host: host, RPCPort: port}, |
|
|
|
client: NewNodeClient(fmt.Sprintf("%s:%d", host, port)), |
|
|
|
checkQueue: make(chan nodeInfo, CheckQueueBufferSize), |
|
|
|
nodePool: make(map[string]*Node), |
|
|
|
nodes: make(map[string]*Node), |
|
|
|
nodeQueue: make(chan *Node, NodeQueueBufferSize), |
|
|
|
quit: make(chan struct{}), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) checkNode(ni nodeInfo) { |
|
|
|
c.wg.Add(1) |
|
|
|
c.checkQueue <- ni |
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) Start() error { |
|
|
|
// make sure we can connect
|
|
|
|
// to our local node first
|
|
|
|
status, err := c.client.rpc.Status() |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
|
|
|
|
// connect to weboscket and subscribe to local events
|
|
|
|
if err = c.client.ws.Dial(); err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
if err = c.client.ws.Subscribe(types.EventStringNewBlock()); err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
go c.readLocalEvents() |
|
|
|
|
|
|
|
// add ourselves to the nodes list
|
|
|
|
c.nodes[c.self.Address()] = c.self |
|
|
|
|
|
|
|
// get peers from local node
|
|
|
|
netinfo, err := c.client.rpc.NetInfo() |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
|
|
|
|
// set the info for ourselves
|
|
|
|
c.self.SetInfo(status, netinfo) |
|
|
|
|
|
|
|
// fire each peer on the checkQueue
|
|
|
|
for _, p := range netinfo.Peers { |
|
|
|
c.checkNode(nodeInfo{ |
|
|
|
host: p.Host, |
|
|
|
port: p.RPCPort, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// nodes we hear about get put on the
|
|
|
|
// checkQueue and are handled in the checkLoop
|
|
|
|
// if its a node we're not already connected to,
|
|
|
|
// it gets put it on the nodeQueue and
|
|
|
|
// we attempt to connect in the connectLoop
|
|
|
|
go c.checkLoop() |
|
|
|
go c.connectLoop() |
|
|
|
|
|
|
|
// finally, a routine with a ticker to poll nodes for peers
|
|
|
|
go c.pollNodesRoutine() |
|
|
|
|
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) Stop() { |
|
|
|
close(c.quit) |
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) readLocalEvents() { |
|
|
|
// read on our ws for NewBlocks
|
|
|
|
} |
|
|
|
|
|
|
|
// check nodes against the nodePool map one at a time
|
|
|
|
// acts as a mutex on nodePool
|
|
|
|
func (c *Crawler) checkLoop() { |
|
|
|
c.wg.Add(1) |
|
|
|
for { |
|
|
|
// every time the loop restarts
|
|
|
|
// it means we processed from the checkQueue
|
|
|
|
// (except the first time, hence the extra wg.Add)
|
|
|
|
c.wg.Done() |
|
|
|
select { |
|
|
|
case ni := <-c.checkQueue: |
|
|
|
host, port, from, removed := ni.unpack() |
|
|
|
addr := fmt.Sprintf("%s:%d", host, port) |
|
|
|
// check if the node should be removed
|
|
|
|
// (eg. if its been connected to or abandoned)
|
|
|
|
if removed { |
|
|
|
n, _ := c.nodePool[addr] |
|
|
|
if n.connected { |
|
|
|
c.nodes[addr] = n |
|
|
|
} |
|
|
|
delete(c.nodePool, addr) |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
// TODO: if address is badly formed
|
|
|
|
// we should punish ni.from
|
|
|
|
_ = from |
|
|
|
|
|
|
|
n, ok := c.nodePool[addr] |
|
|
|
// create the node if unknown
|
|
|
|
if !ok { |
|
|
|
n = &Node{Host: host, RPCPort: port} |
|
|
|
c.nodes[addr] = n |
|
|
|
} else if n.connected { |
|
|
|
// should be removed soon
|
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
// queue it for connecting to
|
|
|
|
c.nodeQueue <- n |
|
|
|
|
|
|
|
case <-c.quit: |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// read off the nodeQueue and attempt to connect to nodes
|
|
|
|
func (c *Crawler) connectLoop() { |
|
|
|
for { |
|
|
|
select { |
|
|
|
case node := <-c.nodeQueue: |
|
|
|
go c.connectToNode(node) |
|
|
|
case <-c.quit: |
|
|
|
// close all connections
|
|
|
|
for addr, node := range c.nodes { |
|
|
|
_, _ = addr, node |
|
|
|
// TODO: close conn
|
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) connectToNode(node *Node) { |
|
|
|
|
|
|
|
addr := node.Address() |
|
|
|
node.client = NewNodeClient(addr) |
|
|
|
|
|
|
|
if err := node.client.ws.Dial(); err != nil { |
|
|
|
// set failed, return
|
|
|
|
} |
|
|
|
|
|
|
|
// remove from nodePool, add to nodes
|
|
|
|
c.checkNode(nodeInfo{ |
|
|
|
host: node.Host, |
|
|
|
port: node.RPCPort, |
|
|
|
removed: true, |
|
|
|
}) |
|
|
|
|
|
|
|
c.pollNode(node) |
|
|
|
|
|
|
|
// TODO: read loop
|
|
|
|
} |
|
|
|
|
|
|
|
func (c *Crawler) pollNode(node *Node) error { |
|
|
|
status, err := node.client.rpc.Status() |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
|
|
|
|
// get peers
|
|
|
|
netinfo, err := node.client.rpc.NetInfo() |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
|
|
|
|
node.SetInfo(status, netinfo) |
|
|
|
|
|
|
|
// fire each peer on the checkQueue
|
|
|
|
for _, p := range netinfo.Peers { |
|
|
|
c.checkNode(nodeInfo{ |
|
|
|
host: p.Host, |
|
|
|
port: p.RPCPort, |
|
|
|
from: node.Address(), |
|
|
|
}) |
|
|
|
} |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
// wait for the checkQueue to empty and poll all the nodes
|
|
|
|
func (c *Crawler) pollNodesRoutine() { |
|
|
|
for { |
|
|
|
c.wg.Wait() |
|
|
|
|
|
|
|
ticker := time.Tick(time.Second) |
|
|
|
// wait a few seconds to make sure we really have nothing
|
|
|
|
time.Sleep(time.Second * 5) |
|
|
|
ch := make(chan struct{}) |
|
|
|
go func() { |
|
|
|
c.wg.Wait() |
|
|
|
ch <- struct{}{} |
|
|
|
}() |
|
|
|
|
|
|
|
//
|
|
|
|
select { |
|
|
|
case <-ticker: |
|
|
|
// the checkQueue has filled up again, move on
|
|
|
|
continue |
|
|
|
case <-ch: |
|
|
|
// the checkQueue is legit empty,
|
|
|
|
// TODO: poll the nodes!
|
|
|
|
|
|
|
|
case <-c.quit: |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |