commit
3822773001
@ -0,0 +1,210 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rqlite/rqlite/store"
|
||||
)
|
||||
|
||||
// Node represents a single node in the cluster and can include
|
||||
// information about the node's reachability and leadership status.
|
||||
// If there was an error communicating with the node, the Error
|
||||
// field will be populated.
|
||||
type Node struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
APIAddr string `json:"api_addr,omitempty"`
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Voter bool `json:"voter"`
|
||||
Reachable bool `json:"reachable"`
|
||||
Leader bool `json:"leader"`
|
||||
Time float64 `json:"time,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewNodeFromServer creates a Node from a Server.
|
||||
func NewNodeFromServer(s *store.Server) *Node {
|
||||
return &Node{
|
||||
ID: s.ID,
|
||||
Addr: s.Addr,
|
||||
Voter: s.Suffrage == "Voter",
|
||||
}
|
||||
}
|
||||
|
||||
// Test tests the node's reachability and leadership status. If an error
|
||||
// occurs, the Error field will be populated.
|
||||
func (n *Node) Test(ga GetAddresser, leaderAddr string, timeout time.Duration) {
|
||||
start := time.Now()
|
||||
apiAddr, err := ga.GetNodeAPIAddr(n.Addr, timeout)
|
||||
if err != nil {
|
||||
n.Error = err.Error()
|
||||
n.Reachable = false
|
||||
return
|
||||
}
|
||||
n.Time = time.Since(start).Seconds()
|
||||
n.APIAddr = apiAddr
|
||||
n.Reachable = true
|
||||
n.Leader = n.Addr == leaderAddr
|
||||
}
|
||||
|
||||
type Nodes []*Node
|
||||
|
||||
func (n Nodes) Len() int { return len(n) }
|
||||
func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n Nodes) Less(i, j int) bool { return n[i].ID < n[j].ID }
|
||||
|
||||
// NewNodesFromServers creates a slice of Nodes from a slice of Servers.
|
||||
func NewNodesFromServers(servers []*store.Server) Nodes {
|
||||
nodes := make([]*Node, len(servers))
|
||||
for i, s := range servers {
|
||||
nodes[i] = NewNodeFromServer(s)
|
||||
}
|
||||
sort.Sort(Nodes(nodes))
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Voters returns a slice of Nodes that are voters.
|
||||
func (n Nodes) Voters() Nodes {
|
||||
v := make(Nodes, 0)
|
||||
for _, node := range n {
|
||||
if node.Voter {
|
||||
v = append(v, node)
|
||||
}
|
||||
}
|
||||
sort.Sort(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// HasAddr returns whether any node in the Nodes slice has the given Raft address.
|
||||
func (n Nodes) HasAddr(addr string) bool {
|
||||
for _, node := range n {
|
||||
if node.Addr == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetNode returns the Node with the given ID, or nil if no such node exists.
|
||||
func (n Nodes) GetNode(id string) *Node {
|
||||
for _, node := range n {
|
||||
if node.ID == id {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test tests the reachability and leadership status of all nodes. It does this
|
||||
// in parallel, and blocks until all nodes have been tested.
|
||||
func (n Nodes) Test(ga GetAddresser, leaderAddr string, timeout time.Duration) {
|
||||
var wg sync.WaitGroup
|
||||
for _, nn := range n {
|
||||
wg.Add(1)
|
||||
go func(nnn *Node) {
|
||||
defer wg.Done()
|
||||
nnn.Test(ga, leaderAddr, timeout)
|
||||
}(nn)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// NodesRespEncoder encodes Nodes into JSON with an option for legacy format.
|
||||
type NodesRespEncoder struct {
|
||||
writer io.Writer
|
||||
legacy bool
|
||||
prefix string
|
||||
indent string
|
||||
}
|
||||
|
||||
// NewNodesRespEncoder creates a new NodesRespEncoder instance with the specified
|
||||
// io.Writer and legacy flag.
|
||||
func NewNodesRespEncoder(w io.Writer, legacy bool) *NodesRespEncoder {
|
||||
return &NodesRespEncoder{
|
||||
writer: w,
|
||||
legacy: legacy,
|
||||
}
|
||||
}
|
||||
|
||||
// SetIndent sets the indentation format for the JSON output.
|
||||
func (e *NodesRespEncoder) SetIndent(prefix, indent string) {
|
||||
e.prefix = prefix
|
||||
e.indent = indent
|
||||
}
|
||||
|
||||
// Encode takes a slice of Nodes and encodes it into JSON,
|
||||
// writing the output to the Encoder's writer.
|
||||
func (e *NodesRespEncoder) Encode(nodes Nodes) error {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if e.legacy {
|
||||
data, err = e.encodeLegacy(nodes)
|
||||
} else {
|
||||
data, err = e.encode(nodes)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.indent != "" {
|
||||
var buf bytes.Buffer
|
||||
err = json.Indent(&buf, data, e.prefix, e.indent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = buf.Bytes()
|
||||
}
|
||||
|
||||
_, err = e.writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// encode encodes the nodes in the standard format.
|
||||
func (e *NodesRespEncoder) encode(nodes Nodes) ([]byte, error) {
|
||||
nodeOutput := &struct {
|
||||
Nodes Nodes `json:"nodes"`
|
||||
}{
|
||||
Nodes: nodes,
|
||||
}
|
||||
return json.Marshal(nodeOutput)
|
||||
}
|
||||
|
||||
// encodeLegacy encodes the nodes in the legacy format.
|
||||
func (e *NodesRespEncoder) encodeLegacy(nodes Nodes) ([]byte, error) {
|
||||
legacyOutput := make(map[string]*Node)
|
||||
for _, node := range nodes {
|
||||
legacyOutput[node.ID] = node
|
||||
}
|
||||
return json.Marshal(legacyOutput)
|
||||
}
|
||||
|
||||
// NodesRespDecoder decodes JSON data into a slice of Nodes.
|
||||
type NodesRespDecoder struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// NewNodesRespDecoder creates a new Decoder instance with the specified io.Reader.
|
||||
func NewNodesRespDecoder(r io.Reader) *NodesRespDecoder {
|
||||
return &NodesRespDecoder{reader: r}
|
||||
}
|
||||
|
||||
// Decode reads JSON from its reader and decodes it into the provided Nodes slice.
|
||||
func (d *NodesRespDecoder) Decode(nodes *Nodes) error {
|
||||
// Temporary structure to facilitate decoding.
|
||||
var data struct {
|
||||
Nodes Nodes `json:"nodes"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(d.reader).Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*nodes = data.Nodes
|
||||
return nil
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rqlite/rqlite/store"
|
||||
)
|
||||
|
||||
func Test_NewNodeFromServer(t *testing.T) {
|
||||
server := &store.Server{ID: "1", Addr: "192.168.1.1", Suffrage: "Voter"}
|
||||
node := NewNodeFromServer(server)
|
||||
|
||||
if node.ID != server.ID || node.Addr != server.Addr || !node.Voter {
|
||||
t.Fatalf("NewNodeFromServer did not correctly initialize Node from Server")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewNodesFromServers(t *testing.T) {
|
||||
servers := []*store.Server{
|
||||
{ID: "1", Addr: "192.168.1.1", Suffrage: "Voter"},
|
||||
{ID: "2", Addr: "192.168.1.2", Suffrage: "Nonvoter"},
|
||||
}
|
||||
nodes := NewNodesFromServers(servers)
|
||||
|
||||
if len(nodes) != len(servers) {
|
||||
t.Fatalf("NewNodesFromServers did not create the correct number of nodes")
|
||||
}
|
||||
for i, node := range nodes {
|
||||
if node.ID != servers[i].ID || node.Addr != servers[i].Addr {
|
||||
t.Fatalf("NewNodesFromServers did not correctly initialize Node %d from Server", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodesVoters(t *testing.T) {
|
||||
nodes := Nodes{
|
||||
{ID: "1", Voter: true},
|
||||
{ID: "2", Voter: false},
|
||||
}
|
||||
voters := nodes.Voters()
|
||||
|
||||
if len(voters) != 1 || !voters[0].Voter {
|
||||
t.Fatalf("Voters method did not correctly filter voter nodes")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodeTestLeader(t *testing.T) {
|
||||
node := &Node{ID: "1", Addr: "leader-raft-addr", APIAddr: "leader-api-addr"}
|
||||
mockGA := newMockGetAddresser("leader-api-addr", nil)
|
||||
|
||||
node.Test(mockGA, "leader-raft-addr", 10*time.Second)
|
||||
if !node.Reachable || !node.Leader {
|
||||
t.Fatalf("Test method did not correctly update node status %s", asJSON(node))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodeTestNotLeader(t *testing.T) {
|
||||
node := &Node{ID: "1", Addr: "follower-raft-addr", APIAddr: "follower-api-addr"}
|
||||
mockGA := newMockGetAddresser("follower-api-addr", nil)
|
||||
|
||||
node.Test(mockGA, "leader-raft-addr", 10*time.Second)
|
||||
if !node.Reachable || node.Leader {
|
||||
t.Fatalf("Test method did not correctly update node status %s", asJSON(node))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodeTestDouble(t *testing.T) {
|
||||
node1 := &Node{ID: "1", Addr: "leader-raft-addr", APIAddr: "leader-api-addr"}
|
||||
node2 := &Node{ID: "2", Addr: "follower-raft-addr", APIAddr: "follower-api-addr"}
|
||||
mockGA := &mockGetAddresser{}
|
||||
mockGA.getAddrFn = func(addr string, timeout time.Duration) (string, error) {
|
||||
if addr == "leader-raft-addr" {
|
||||
return "leader-api-addr", nil
|
||||
}
|
||||
return "", fmt.Errorf("not reachable")
|
||||
}
|
||||
|
||||
nodes := Nodes{node1, node2}
|
||||
nodes.Test(mockGA, "leader-raft-addr", 10*time.Second)
|
||||
if !node1.Reachable || !node1.Leader || node2.Reachable || node2.Leader || node2.Error != "not reachable" {
|
||||
t.Fatalf("Test method did not correctly update node status %s", asJSON(nodes))
|
||||
}
|
||||
|
||||
if !nodes.HasAddr("leader-raft-addr") {
|
||||
t.Fatalf("HasAddr method did not correctly find node")
|
||||
}
|
||||
if nodes.HasAddr("not-found") {
|
||||
t.Fatalf("HasAddr method incorrectly found node")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodesRespEncodeStandard(t *testing.T) {
|
||||
nodes := mockNodes()
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := NewNodesRespEncoder(buffer, false)
|
||||
|
||||
err := encoder.Encode(nodes)
|
||||
if err != nil {
|
||||
t.Errorf("Encode failed: %v", err)
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(buffer.Bytes(), &m); err != nil {
|
||||
t.Errorf("Encode failed: %v", err)
|
||||
}
|
||||
if len(m) != 1 {
|
||||
t.Errorf("unexpected number of keys")
|
||||
}
|
||||
if _, ok := m["nodes"]; !ok {
|
||||
t.Errorf("nodes key missing")
|
||||
}
|
||||
nodesArray, ok := m["nodes"].([]interface{})
|
||||
if !ok {
|
||||
t.Errorf("nodes key is not an array")
|
||||
}
|
||||
if len(nodesArray) != 1 {
|
||||
t.Errorf("unexpected number of nodes")
|
||||
}
|
||||
node, ok := nodesArray[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Errorf("node is not a map")
|
||||
}
|
||||
checkNode(t, node)
|
||||
}
|
||||
|
||||
func Test_NodeRespEncodeLegacy(t *testing.T) {
|
||||
nodes := mockNodes()
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := NewNodesRespEncoder(buffer, true)
|
||||
|
||||
err := encoder.Encode(nodes)
|
||||
if err != nil {
|
||||
t.Errorf("Encode failed: %v", err)
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(buffer.Bytes(), &m); err != nil {
|
||||
t.Errorf("Encode failed: %v", err)
|
||||
}
|
||||
if len(m) != 1 {
|
||||
t.Errorf("unexpected number of keys")
|
||||
}
|
||||
if _, ok := m["1"]; !ok {
|
||||
t.Errorf("node key missing")
|
||||
}
|
||||
node, ok := m["1"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Errorf("nodes key is not an map")
|
||||
}
|
||||
checkNode(t, node)
|
||||
}
|
||||
|
||||
func Test_NodesRespDecoder_Decode_ValidJSON(t *testing.T) {
|
||||
jsonInput := `{"nodes":[{"id":"1","addr":"192.168.1.1","voter":true},{"id":"2","addr":"192.168.1.2","voter":false}]}`
|
||||
reader := strings.NewReader(jsonInput)
|
||||
decoder := NewNodesRespDecoder(reader)
|
||||
|
||||
var nodes Nodes
|
||||
err := decoder.Decode(&nodes)
|
||||
if err != nil {
|
||||
t.Errorf("Decode failed with valid JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 2 || nodes[0].ID != "1" || nodes[1].ID != "2" {
|
||||
t.Errorf("Decode did not properly decode the JSON into Nodes")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodesRespDecoder_Decode_InvalidJSON(t *testing.T) {
|
||||
invalidJsonInput := `{"nodes": "invalid"}`
|
||||
reader := strings.NewReader(invalidJsonInput)
|
||||
decoder := NewNodesRespDecoder(reader)
|
||||
|
||||
var nodes Nodes
|
||||
err := decoder.Decode(&nodes)
|
||||
if err == nil {
|
||||
t.Error("Decode should fail with invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodesRespDecoder_Decode_EmptyJSON(t *testing.T) {
|
||||
emptyJsonInput := `{}`
|
||||
reader := strings.NewReader(emptyJsonInput)
|
||||
decoder := NewNodesRespDecoder(reader)
|
||||
|
||||
var nodes Nodes
|
||||
err := decoder.Decode(&nodes)
|
||||
if err != nil {
|
||||
t.Errorf("Decode failed with empty JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) != 0 {
|
||||
t.Errorf("Decode should result in an empty Nodes slice for empty JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// mockGetAddresser is a mock implementation of the GetAddresser interface.
|
||||
type mockGetAddresser struct {
|
||||
apiAddr string
|
||||
err error
|
||||
getAddrFn func(addr string, timeout time.Duration) (string, error)
|
||||
}
|
||||
|
||||
// newMockGetAddresser creates a new instance of mockGetAddresser.
|
||||
// You can customize the return values for GetNodeAPIAddr by setting apiAddr and err.
|
||||
func newMockGetAddresser(apiAddr string, err error) *mockGetAddresser {
|
||||
return &mockGetAddresser{apiAddr: apiAddr, err: err}
|
||||
}
|
||||
|
||||
// GetNodeAPIAddr is the mock implementation of the GetNodeAPIAddr method.
|
||||
func (m *mockGetAddresser) GetNodeAPIAddr(addr string, timeout time.Duration) (string, error) {
|
||||
if m.getAddrFn != nil {
|
||||
return m.getAddrFn(addr, timeout)
|
||||
}
|
||||
return m.apiAddr, m.err
|
||||
}
|
||||
|
||||
func mockNodes() Nodes {
|
||||
return Nodes{
|
||||
&Node{ID: "1", APIAddr: "http://localhost:4001", Addr: "localhost:4002", Reachable: true, Leader: true},
|
||||
}
|
||||
}
|
||||
|
||||
func checkNode(t *testing.T, node map[string]interface{}) {
|
||||
t.Helper()
|
||||
if _, ok := node["id"]; !ok {
|
||||
t.Errorf("node is missing id")
|
||||
}
|
||||
if node["id"] != "1" {
|
||||
t.Errorf("unexpected node id")
|
||||
}
|
||||
if _, ok := node["api_addr"]; !ok {
|
||||
t.Errorf("node is missing api_addr")
|
||||
}
|
||||
if node["api_addr"] != "http://localhost:4001" {
|
||||
t.Errorf("unexpected node api_addr")
|
||||
}
|
||||
if _, ok := node["addr"]; !ok {
|
||||
t.Errorf("node is missing addr")
|
||||
}
|
||||
if node["addr"] != "localhost:4002" {
|
||||
t.Errorf("unexpected node addr")
|
||||
}
|
||||
if _, ok := node["reachable"]; !ok {
|
||||
t.Errorf("node is missing reachable")
|
||||
}
|
||||
if node["reachable"] != true {
|
||||
t.Errorf("unexpected node reachable")
|
||||
}
|
||||
if _, ok := node["leader"]; !ok {
|
||||
t.Errorf("node is missing leader")
|
||||
}
|
||||
if node["leader"] != true {
|
||||
t.Errorf("unexpected node leader")
|
||||
}
|
||||
}
|
||||
|
||||
func asJSON(v interface{}) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to JSON marshal value: %s", err.Error()))
|
||||
}
|
||||
return string(b)
|
||||
}
|
Loading…
Reference in New Issue