func (p *latencyTester) Close() error {
	logging.Debug("latencytester: kill [tomb]")
	p.tmb.Kill(nil)
	logging.Debug("latencytester: wait [tomb]")
	return p.tmb.Wait()
}
Close stops the tester and waits until all goroutines have finished.
package api
import (
	"context"
	"fmt"
	"math"
	"strings"
	"time"
	"github.com/denkhaus/bitshares/types"
	"github.com/denkhaus/logging"
	sort "github.com/emirpasic/gods/utils"
	deadlock "github.com/sasha-s/go-deadlock"
	"gopkg.in/tomb.v2"
)
var (
	LoopSeconds = 60
	knownEndpoints = []string{
		"wss://eu.openledger.info/ws",
		"wss://bitshares.openledger.info/ws",
		"wss://dex.rnglab.org",
		"wss://api.bitshares.bhuz.info/ws",
		"wss://bitshares.crypto.fans/ws",
		"wss://node.market.rudex.org",
		"wss://api.bts.blckchnd.com",
		"wss://eu.nodes.bitshares.ws",
		"wss://btsws.roelandp.nl/ws",
		"wss://btsfullnode.bangzi.info/ws",
		"wss://api-ru.bts.blckchnd.com",
		"wss://kc-us-dex.xeldal.com/ws",
		"wss://api.btsxchng.com",
		"wss://api.bts.network",
		"wss://dexnode.net/ws",
		"wss://us.nodes.bitshares.ws",
		"wss://api.bts.mobi/ws",
		"wss://blockzms.xyz/ws",
		"wss://bts-api.lafona.net/ws",
		"wss://api.bts.ai/",
		"wss://la.dexnode.net/ws",
		"wss://openledger.hk/ws",
		"wss://sg.nodes.bitshares.ws",
		"wss://bts.open.icowallet.net/ws",
		"wss://ws.gdex.io",
		"wss://bitshares-api.wancloud.io/ws",
		"wss://ws.hellobts.com/",
		"wss://bitshares.dacplay.org/ws",
		"wss://crazybit.online",
		"wss://kimziv.com/ws",
		"wss://wss.ioex.top",
		"wss://node.btscharts.com/ws",
		"wss://bts-seoul.clockwork.gr/",
		"wss://bitshares.cyberit.io/",
		"wss://api.btsgo.net/ws",
		"wss://ws.winex.pro",
		"wss://bts.to0l.cn:4443/ws",
		"wss://bitshares.bts123.cc:15138/",
		"wss://bit.btsabc.org/ws",
		"wss://ws.gdex.top",
	}
)
type LatencyTester interface {
	Start()
	Close() error
	String() string
	AddEndpoint(ep string)
	OnTopNodeChanged(fn func(string) error)
	TopNodeEndpoint() string
	TopNodeClient() WebsocketClient
	Done() <-chan struct{}
}
type NodeStats struct {
	cli        WebsocketClient
	latency    time.Duration
	requiredDB []string
	attempts   int64
	errors     int64
	endpoint   string
}
func (p *NodeStats) onError(err error) {
	p.errors++
}
func (p *NodeStats) Latency() time.Duration {
	if p.attempts > 0 {
		return time.Duration(int64(p.latency) / p.attempts)
	}
	return 0
}
func (p *NodeStats) Score() int64 {
	lat := int64(p.Latency())
	if lat == 0 {
		return math.MaxInt64
	}
	if p.errors == 0 {
		return lat
	}
	return lat + p.errors*50000000
}
func (p *NodeStats) String() string {
	return fmt.Sprintf("ep: %s | attempts: %d | errors: %d | latency: %s | score: %d",
		p.endpoint, p.attempts, p.errors, p.Latency(), p.Score())
}
func NewNodeStats(wsRPCEndpoint string) *NodeStats {
	stats := &NodeStats{
		endpoint: wsRPCEndpoint,
		cli:      NewWebsocketClient(wsRPCEndpoint),
		requiredDB: []string{
			"database",
			"history",
			"network_broadcast",
		},
	}
	stats.cli.OnError(stats.onError)
	return stats
}
func (p *NodeStats) Equals(n *NodeStats) bool {
	return p.endpoint == n.endpoint
}
func (p *NodeStats) checkNode() {
	_, err := p.cli.CallAPI(1, "login", "", "")
	if err != nil {
		p.errors++
	}
	for _, name := range p.requiredDB {
		_, err := p.cli.CallAPI(1, name, types.EmptyParams)
		if err != nil {
			p.errors++
		}
	}
}
func (p *NodeStats) check() {
	p.attempts++
	if err := p.cli.Connect(); err != nil {
		p.errors++
	}
	defer p.cli.Close()
	tm := time.Now()
	p.checkNode()
	p.latency += time.Since(tm)
}
type latencyTester struct {
	mu               deadlock.RWMutex
	tmb              *tomb.Tomb
	toApply          []string
	fallbackURL      string
	onTopNodeChanged func(string) error
	stats            []interface{}
	pass             int
}
func NewLatencyTester(fallbackURL string) (LatencyTester, error) {
	return NewLatencyTesterWithContext(context.Background(), fallbackURL)
}
func NewLatencyTesterWithContext(ctx context.Context, fallbackURL string) (LatencyTester, error) {
	tmb, _ := tomb.WithContext(ctx)
	lat := latencyTester{
		fallbackURL: fallbackURL,
		stats:       make([]interface{}, 0, len(knownEndpoints)),
		tmb:         tmb,
	}
	lat.createStats(knownEndpoints)
	return &lat, nil
}
func (p *latencyTester) String() string {
	builder := strings.Builder{}
	p.mu.Lock()
	defer p.mu.Unlock()
	for _, s := range p.stats {
		stat := s.(*NodeStats)
		builder.WriteString(stat.String())
		builder.WriteString("\n")
	}
	return builder.String()
}
func (p *latencyTester) OnTopNodeChanged(fn func(string) error) {
	p.onTopNodeChanged = fn
}
func (p *latencyTester) AddEndpoint(ep string) {
	p.toApply = append(p.toApply, ep)
}
func (p *latencyTester) sortResults(notify bool) error {
	p.mu.Lock()
	oldTop := p.stats[0].(*NodeStats)
	sort.Sort(p.stats, func(a, b interface{}) int {
		sa := a.(*NodeStats).Score()
		sb := b.(*NodeStats).Score()
		if sa > sb {
			return 1
		}
		if sa < sb {
			return -1
		}
		return 0
	})
	newTop := p.stats[0].(*NodeStats)
	p.mu.Unlock()
	if notify && !oldTop.Equals(newTop) {
		if p.onTopNodeChanged != nil {
			return p.onTopNodeChanged(newTop.endpoint)
		}
	}
	return nil
}
func (p *latencyTester) createStats(eps []string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	for _, ep := range eps {
		found := false
		for _, s := range p.stats {
			stat := s.(*NodeStats)
			if stat.endpoint == ep {
				found = true
			}
		}
		if !found {
			p.stats = append(
				p.stats,
				NewNodeStats(ep),
			)
		}
	}
}
func (p *latencyTester) TopNodeEndpoint() string {
	if p.pass > 0 {
		p.mu.RLock()
		defer p.mu.RUnlock()
		st := p.stats[0].(*NodeStats)
		return st.endpoint
	}
	return p.fallbackURL
}
func (p *latencyTester) TopNodeClient() WebsocketClient {
	return NewWebsocketClient(
		p.TopNodeEndpoint(),
	)
}
func (p *latencyTester) Done() <-chan struct{} {
	return p.tmb.Dead()
}
func (p *latencyTester) runPass() error {
	slp := time.Duration(LoopSeconds/len(p.stats)) * time.Second
	for i := 0; i < len(p.stats); i++ {
		select {
		case <-p.tmb.Dying():
			return tomb.ErrDying
		default:
			time.Sleep(slp)
			p.mu.RLock()
			st := p.stats[i].(*NodeStats)
			st.check()
			p.mu.RUnlock()
		}
	}
	return nil
}
func (p *latencyTester) Start() {
	logging.Debug("latencytester: start")
	p.tmb.Go(func() error {
		for {
			select {
			case <-p.tmb.Dying():
				p.sortResults(false)
				return tomb.ErrDying
			default:
				p.createStats(p.toApply)
				if err := p.runPass(); err != nil {
					p.sortResults(false)
					return err
				}
				p.sortResults(true)
				p.pass++
			}
		}
	})
}