238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
package raft
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
)
|
|
|
|
// Initializes the default transport to support standard HTTP and TCP.
|
|
func init() {
|
|
t := NewTransportMux()
|
|
t.Handle("http", &HTTPTransport{})
|
|
DefaultTransport = t
|
|
}
|
|
|
|
// Transport represents a handler for connecting the log to another node.
|
|
// It uses URLs to direct requests over different protocols.
|
|
type Transport interface {
|
|
Join(u *url.URL, nodeURL *url.URL) (uint64, *Config, error)
|
|
Leave(u *url.URL, id uint64) error
|
|
Heartbeat(u *url.URL, term, commitIndex, leaderID uint64) (lastIndex, currentTerm uint64, err error)
|
|
ReadFrom(u *url.URL, id, term, index uint64) (io.ReadCloser, error)
|
|
RequestVote(u *url.URL, term, candidateID, lastLogIndex, lastLogTerm uint64) (uint64, error)
|
|
}
|
|
|
|
// DefaultTransport provides support for HTTP and TCP protocols.
|
|
var DefaultTransport Transport
|
|
|
|
// Transport is a transport multiplexer. It takes incoming requests and delegates
|
|
// them to the matching transport implementation based on their URL scheme.
|
|
type TransportMux struct {
|
|
m map[string]Transport
|
|
}
|
|
|
|
// NewTransportMux returns a new instance of TransportMux.
|
|
func NewTransportMux() *TransportMux {
|
|
return &TransportMux{m: make(map[string]Transport)}
|
|
}
|
|
|
|
// Handle registers a transport for a given scheme.
|
|
func (mux *TransportMux) Handle(scheme string, t Transport) {
|
|
mux.m[scheme] = t
|
|
}
|
|
|
|
// Join requests membership into a node's cluster.
|
|
func (mux *TransportMux) Join(u *url.URL, nodeURL *url.URL) (uint64, *Config, error) {
|
|
if t, ok := mux.m[u.Scheme]; ok {
|
|
return t.Join(u, nodeURL)
|
|
}
|
|
return 0, nil, fmt.Errorf("transport scheme not supported: %s", u.Scheme)
|
|
}
|
|
|
|
// Leave removes a node from a cluster's membership.
|
|
func (mux *TransportMux) Leave(u *url.URL, id uint64) error {
|
|
if t, ok := mux.m[u.Scheme]; ok {
|
|
return t.Leave(u, id)
|
|
}
|
|
return fmt.Errorf("transport scheme not supported: %s", u.Scheme)
|
|
}
|
|
|
|
// Heartbeat checks the status of a follower.
|
|
func (mux *TransportMux) Heartbeat(u *url.URL, term, commitIndex, leaderID uint64) (uint64, uint64, error) {
|
|
if t, ok := mux.m[u.Scheme]; ok {
|
|
return t.Heartbeat(u, term, commitIndex, leaderID)
|
|
}
|
|
return 0, 0, fmt.Errorf("transport scheme not supported: %s", u.Scheme)
|
|
}
|
|
|
|
// ReadFrom streams the log from a leader.
|
|
func (mux *TransportMux) ReadFrom(u *url.URL, id, term, index uint64) (io.ReadCloser, error) {
|
|
if t, ok := mux.m[u.Scheme]; ok {
|
|
return t.ReadFrom(u, id, term, index)
|
|
}
|
|
return nil, fmt.Errorf("transport scheme not supported: %s", u.Scheme)
|
|
}
|
|
|
|
// RequestVote requests a vote for a candidate in a given term.
|
|
func (mux *TransportMux) RequestVote(u *url.URL, term, candidateID, lastLogIndex, lastLogTerm uint64) (uint64, error) {
|
|
if t, ok := mux.m[u.Scheme]; ok {
|
|
return t.RequestVote(u, term, candidateID, lastLogIndex, lastLogTerm)
|
|
}
|
|
return 0, fmt.Errorf("transport scheme not supported: %s", u.Scheme)
|
|
}
|
|
|
|
// HTTPTransport represents a transport for sending RPCs over the HTTP protocol.
|
|
type HTTPTransport struct{}
|
|
|
|
// Join requests membership into a node's cluster.
|
|
func (t *HTTPTransport) Join(uri *url.URL, nodeURL *url.URL) (uint64, *Config, error) {
|
|
// Construct URL.
|
|
u := *uri
|
|
u.Path = path.Join(u.Path, "raft/join")
|
|
u.RawQuery = (&url.Values{"url": {nodeURL.String()}}).Encode()
|
|
|
|
// Send HTTP request.
|
|
resp, err := http.Get(u.String())
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Parse returned id.
|
|
idString := resp.Header.Get("X-Raft-ID")
|
|
id, err := strconv.ParseUint(idString, 10, 64)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("invalid id: %q", idString)
|
|
}
|
|
|
|
// Unmarshal config.
|
|
var config *Config
|
|
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
|
return 0, nil, fmt.Errorf("config unmarshal: %s", err)
|
|
}
|
|
|
|
// Parse returned error.
|
|
if s := resp.Header.Get("X-Raft-Error"); s != "" {
|
|
return 0, nil, errors.New(s)
|
|
}
|
|
|
|
return id, config, nil
|
|
}
|
|
|
|
// Leave removes a node from a cluster's membership.
|
|
func (t *HTTPTransport) Leave(uri *url.URL, id uint64) error {
|
|
return nil // TODO(benbjohnson)
|
|
}
|
|
|
|
// Heartbeat checks the status of a follower.
|
|
func (t *HTTPTransport) Heartbeat(uri *url.URL, term, commitIndex, leaderID uint64) (uint64, uint64, error) {
|
|
// Construct URL.
|
|
u := *uri
|
|
u.Path = path.Join(u.Path, "raft/heartbeat")
|
|
|
|
// Set URL parameters.
|
|
v := &url.Values{}
|
|
v.Set("term", strconv.FormatUint(term, 10))
|
|
v.Set("commitIndex", strconv.FormatUint(commitIndex, 10))
|
|
v.Set("leaderID", strconv.FormatUint(leaderID, 10))
|
|
u.RawQuery = v.Encode()
|
|
|
|
// Send HTTP request.
|
|
resp, err := http.Get(u.String())
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
// Parse returned index.
|
|
newIndexString := resp.Header.Get("X-Raft-Index")
|
|
newIndex, err := strconv.ParseUint(newIndexString, 10, 64)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("invalid index: %q", newIndexString)
|
|
}
|
|
|
|
// Parse returned term.
|
|
newTermString := resp.Header.Get("X-Raft-Term")
|
|
newTerm, err := strconv.ParseUint(newTermString, 10, 64)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("invalid term: %q", newTermString)
|
|
}
|
|
|
|
// Parse returned error.
|
|
if s := resp.Header.Get("X-Raft-Error"); s != "" {
|
|
return newIndex, newTerm, errors.New(s)
|
|
}
|
|
|
|
return newIndex, newTerm, nil
|
|
}
|
|
|
|
// ReadFrom streams the log from a leader.
|
|
func (t *HTTPTransport) ReadFrom(uri *url.URL, id, term, index uint64) (io.ReadCloser, error) {
|
|
// Construct URL.
|
|
u := *uri
|
|
u.Path = path.Join(u.Path, "raft/stream")
|
|
|
|
// Set URL parameters.
|
|
v := &url.Values{}
|
|
v.Set("id", strconv.FormatUint(id, 10))
|
|
v.Set("term", strconv.FormatUint(term, 10))
|
|
v.Set("index", strconv.FormatUint(index, 10))
|
|
u.RawQuery = v.Encode()
|
|
|
|
// Send HTTP request.
|
|
resp, err := http.Get(u.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse returned error.
|
|
if s := resp.Header.Get("X-Raft-Error"); s != "" {
|
|
_ = resp.Body.Close()
|
|
return nil, errors.New(s)
|
|
}
|
|
|
|
return resp.Body, nil
|
|
}
|
|
|
|
// RequestVote requests a vote for a candidate in a given term.
|
|
func (t *HTTPTransport) RequestVote(uri *url.URL, term, candidateID, lastLogIndex, lastLogTerm uint64) (uint64, error) {
|
|
// Construct URL.
|
|
u := *uri
|
|
u.Path = path.Join(u.Path, "raft/vote")
|
|
|
|
// Set URL parameters.
|
|
v := &url.Values{}
|
|
v.Set("term", strconv.FormatUint(term, 10))
|
|
v.Set("candidateID", strconv.FormatUint(candidateID, 10))
|
|
v.Set("lastLogIndex", strconv.FormatUint(lastLogIndex, 10))
|
|
v.Set("lastLogTerm", strconv.FormatUint(lastLogTerm, 10))
|
|
u.RawQuery = v.Encode()
|
|
|
|
// Send HTTP request.
|
|
resp, err := http.Get(u.String())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
// Parse returned term.
|
|
newTermString := resp.Header.Get("X-Raft-Term")
|
|
newTerm, err := strconv.ParseUint(newTermString, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid term: %q", newTermString)
|
|
}
|
|
|
|
// Parse returned error.
|
|
if s := resp.Header.Get("X-Raft-Error"); s != "" {
|
|
return newTerm, errors.New(s)
|
|
}
|
|
|
|
return newTerm, nil
|
|
}
|