235 lines
4.9 KiB
Go
235 lines
4.9 KiB
Go
package elasticsearch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// A Node is a structure which represents a single ElasticSearch host.
|
|
type Node struct {
|
|
sync.RWMutex
|
|
endpoint string
|
|
health Health
|
|
client *http.Client // default http client
|
|
pingClient *http.Client // used for Ping() only
|
|
}
|
|
|
|
// NewNode constructs a Node handle. The endpoint should be of the form
|
|
// "scheme://host:port", eg. "http://es001:9200".
|
|
//
|
|
// The ping interval is dictated at a higher level (the Cluster), but individual
|
|
// ping timeouts are stored with the Nodes themselves, in a custom HTTP client,
|
|
// with a timeout as part of the Transport dialer. This custom pingClient is
|
|
// used exclusively for Ping() calls.
|
|
//
|
|
// Regular queries are made with the default client http.Client, which has
|
|
// no explicit timeout set in the Transport dialer.
|
|
func NewNode(endpoint string, pingTimeout time.Duration) *Node {
|
|
return &Node{
|
|
endpoint: endpoint,
|
|
health: Yellow,
|
|
client: &http.Client{
|
|
Transport: &http.Transport{
|
|
MaxIdleConnsPerHost: 250,
|
|
},
|
|
},
|
|
pingClient: &http.Client{
|
|
Transport: &http.Transport{
|
|
Dial: timeoutDialer(pingTimeout),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Ping attempts to HTTP GET a specific endpoint, parse some kind of
|
|
// status indicator, and returns true if everything was successful.
|
|
func (n *Node) Ping() bool {
|
|
u, err := url.Parse(n.endpoint)
|
|
if err != nil {
|
|
log.Printf("ElasticSearch: ping: resolve: %s", err)
|
|
return false
|
|
}
|
|
u.Path = "/_cluster/nodes/_local" // some arbitrary, reasonable endpoint
|
|
|
|
resp, err := n.pingClient.Get(u.String())
|
|
if err != nil {
|
|
log.Printf("ElasticSearch: ping %s: GET: %s", u.Host, err)
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var status struct {
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
if err = json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
|
log.Printf("ElasticSearch: ping %s: %s", u.Host, err)
|
|
return false
|
|
}
|
|
|
|
if !status.OK {
|
|
log.Printf("ElasticSearch: ping %s: ok=false", u.Host)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// PingAndSet performs a Ping, and updates the Node's health accordingly.
|
|
func (n *Node) pingAndSet() {
|
|
success := n.Ping()
|
|
func() {
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
if success {
|
|
n.health = n.health.Improve()
|
|
} else {
|
|
n.health = n.health.Degrade()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// GetHealth returns the health of the node, for use in the Cluster's GetBest.
|
|
func (n *Node) GetHealth() Health {
|
|
n.RLock()
|
|
defer n.RUnlock()
|
|
return n.health
|
|
}
|
|
|
|
// Executes the Fireable f against the node and decodes the server's reply into
|
|
// response.
|
|
func (n *Node) Execute(f Fireable, response interface{}) error {
|
|
uri, err := url.Parse(n.endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
request, err := f.Request(uri)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := n.client.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
return json.NewDecoder(r.Body).Decode(response)
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
|
|
type Nodes []*Node
|
|
|
|
// PingAll triggers simultaneous PingAndSets across all Nodes,
|
|
// and blocks until they've all completed.
|
|
func (n Nodes) pingAll() {
|
|
c := make(chan bool, len(n))
|
|
for _, node := range n {
|
|
go func(tgt *Node) { tgt.pingAndSet(); c <- true }(node)
|
|
}
|
|
for i := 0; i < cap(c); i++ {
|
|
<-c
|
|
}
|
|
}
|
|
|
|
// GetBest returns the "best" Node, as decided by each Node's health.
|
|
// It's possible that no Node will be healthy enough to be returned.
|
|
// In that case, GetBest returns an error, and processing cannot continue.
|
|
func (n Nodes) getBest() (*Node, error) {
|
|
green, yellow := []*Node{}, []*Node{}
|
|
for _, node := range n {
|
|
switch node.GetHealth() {
|
|
case Green:
|
|
green = append(green, node)
|
|
case Yellow:
|
|
yellow = append(yellow, node)
|
|
}
|
|
}
|
|
|
|
if len(green) > 0 {
|
|
return green[rand.Intn(len(green))], nil
|
|
}
|
|
|
|
if len(yellow) > 0 {
|
|
return yellow[rand.Intn(len(yellow))], nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("no healthy nodes available")
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
|
|
// Health is some encoding of the perceived state of a Node.
|
|
// A Cluster should favor sending queries against healthier nodes.
|
|
type Health int
|
|
|
|
const (
|
|
Green Health = iota // resemblance to cluster health codes is coincidental
|
|
Yellow
|
|
Red
|
|
)
|
|
|
|
func (h Health) String() string {
|
|
switch h {
|
|
case Green:
|
|
return "Green"
|
|
case Yellow:
|
|
return "Yellow"
|
|
case Red:
|
|
return "Red"
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
func (h Health) Improve() Health {
|
|
switch h {
|
|
case Red:
|
|
return Yellow
|
|
default:
|
|
return Green
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
func (h Health) Degrade() Health {
|
|
switch h {
|
|
case Green:
|
|
return Yellow
|
|
default:
|
|
return Red
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
|
|
// timeoutDialer returns a function that can be put into an HTTP Client's
|
|
// Transport, which will cause all requests made on that client to abort
|
|
// if they're not handled within the passed duration.
|
|
func timeoutDialer(d time.Duration) func(net, addr string) (net.Conn, error) {
|
|
return func(netw, addr string) (net.Conn, error) {
|
|
c, err := net.Dial(netw, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.SetDeadline(time.Now().Add(d))
|
|
return c, nil
|
|
}
|
|
}
|