526 lines
12 KiB
Go
526 lines
12 KiB
Go
package meta
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/hashicorp/raft"
|
|
"github.com/influxdata/influxdb/services/meta/internal"
|
|
"github.com/influxdata/influxdb/uuid"
|
|
)
|
|
|
|
// handler represents an HTTP handler for the meta service.
|
|
type handler struct {
|
|
config *Config
|
|
|
|
logger *log.Logger
|
|
loggingEnabled bool // Log every HTTP access.
|
|
pprofEnabled bool
|
|
store interface {
|
|
afterIndex(index uint64) <-chan struct{}
|
|
index() uint64
|
|
leader() string
|
|
leaderHTTP() string
|
|
snapshot() (*Data, error)
|
|
apply(b []byte) error
|
|
join(n *NodeInfo) (*NodeInfo, error)
|
|
otherMetaServersHTTP() []string
|
|
peers() []string
|
|
}
|
|
s *Service
|
|
|
|
mu sync.RWMutex
|
|
closing chan struct{}
|
|
leases *Leases
|
|
}
|
|
|
|
// newHandler returns a new instance of handler with routes.
|
|
func newHandler(c *Config, s *Service) *handler {
|
|
h := &handler{
|
|
s: s,
|
|
config: c,
|
|
logger: log.New(os.Stderr, "[meta-http] ", log.LstdFlags),
|
|
loggingEnabled: c.ClusterTracing,
|
|
closing: make(chan struct{}),
|
|
leases: NewLeases(time.Duration(c.LeaseDuration)),
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
// SetRoutes sets the provided routes on the handler.
|
|
func (h *handler) WrapHandler(name string, hf http.HandlerFunc) http.Handler {
|
|
var handler http.Handler
|
|
handler = http.HandlerFunc(hf)
|
|
handler = gzipFilter(handler)
|
|
handler = versionHeader(handler, h)
|
|
handler = requestID(handler)
|
|
if h.loggingEnabled {
|
|
handler = logging(handler, name, h.logger)
|
|
}
|
|
handler = recovery(handler, name, h.logger) // make sure recovery is always last
|
|
|
|
return handler
|
|
}
|
|
|
|
// ServeHTTP responds to HTTP request to the handler.
|
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case "GET":
|
|
switch r.URL.Path {
|
|
case "/ping":
|
|
h.WrapHandler("ping", h.servePing).ServeHTTP(w, r)
|
|
case "/lease":
|
|
h.WrapHandler("lease", h.serveLease).ServeHTTP(w, r)
|
|
case "/peers":
|
|
h.WrapHandler("peers", h.servePeers).ServeHTTP(w, r)
|
|
default:
|
|
h.WrapHandler("snapshot", h.serveSnapshot).ServeHTTP(w, r)
|
|
}
|
|
case "POST":
|
|
h.WrapHandler("execute", h.serveExec).ServeHTTP(w, r)
|
|
default:
|
|
http.Error(w, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func (h *handler) Close() error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
select {
|
|
case <-h.closing:
|
|
// do nothing here
|
|
default:
|
|
close(h.closing)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *handler) isClosed() bool {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
select {
|
|
case <-h.closing:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// serveExec executes the requested command.
|
|
func (h *handler) serveExec(w http.ResponseWriter, r *http.Request) {
|
|
if h.isClosed() {
|
|
h.httpError(fmt.Errorf("server closed"), w, http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
// Read the command from the request body.
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/join" {
|
|
n := &NodeInfo{}
|
|
if err := json.Unmarshal(body, n); err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
node, err := h.store.join(n)
|
|
if err == raft.ErrNotLeader {
|
|
l := h.store.leaderHTTP()
|
|
if l == "" {
|
|
// No cluster leader. Client will have to try again later.
|
|
h.httpError(errors.New("no leader"), w, http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
scheme := "http://"
|
|
if h.config.HTTPSEnabled {
|
|
scheme = "https://"
|
|
}
|
|
|
|
l = scheme + l + "/join"
|
|
http.Redirect(w, r, l, http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return the node with newly assigned ID as json
|
|
w.Header().Add("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(node); err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Make sure it's a valid command.
|
|
if err := validateCommand(body); err != nil {
|
|
h.httpError(err, w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Apply the command to the store.
|
|
var resp *internal.Response
|
|
if err := h.store.apply(body); err != nil {
|
|
// If we aren't the leader, redirect client to the leader.
|
|
if err == raft.ErrNotLeader {
|
|
l := h.store.leaderHTTP()
|
|
if l == "" {
|
|
// No cluster leader. Client will have to try again later.
|
|
h.httpError(errors.New("no leader"), w, http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
scheme := "http://"
|
|
if h.config.HTTPSEnabled {
|
|
scheme = "https://"
|
|
}
|
|
|
|
l = scheme + l + "/execute"
|
|
http.Redirect(w, r, l, http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
// Error wasn't a leadership error so pass it back to client.
|
|
resp = &internal.Response{
|
|
OK: proto.Bool(false),
|
|
Error: proto.String(err.Error()),
|
|
}
|
|
} else {
|
|
// Apply was successful. Return the new store index to the client.
|
|
resp = &internal.Response{
|
|
OK: proto.Bool(false),
|
|
Index: proto.Uint64(h.store.index()),
|
|
}
|
|
}
|
|
|
|
// Marshal the response.
|
|
b, err := proto.Marshal(resp)
|
|
if err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Send response to client.
|
|
w.Header().Add("Content-Type", "application/octet-stream")
|
|
w.Write(b)
|
|
}
|
|
|
|
func validateCommand(b []byte) error {
|
|
// Ensure command can be deserialized before applying.
|
|
if err := proto.Unmarshal(b, &internal.Command{}); err != nil {
|
|
return fmt.Errorf("unable to unmarshal command: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// serveSnapshot is a long polling http connection to server cache updates
|
|
func (h *handler) serveSnapshot(w http.ResponseWriter, r *http.Request) {
|
|
if h.isClosed() {
|
|
h.httpError(fmt.Errorf("server closed"), w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// get the current index that client has
|
|
index, err := strconv.ParseUint(r.URL.Query().Get("index"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "error parsing index", http.StatusBadRequest)
|
|
}
|
|
|
|
select {
|
|
case <-h.store.afterIndex(index):
|
|
// Send updated snapshot to client.
|
|
ss, err := h.store.snapshot()
|
|
if err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
b, err := ss.MarshalBinary()
|
|
if err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Add("Content-Type", "application/octet-stream")
|
|
w.Write(b)
|
|
return
|
|
case <-w.(http.CloseNotifier).CloseNotify():
|
|
// Client closed the connection so we're done.
|
|
return
|
|
case <-h.closing:
|
|
h.httpError(fmt.Errorf("server closed"), w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// servePing will return if the server is up, or if specified will check the status
|
|
// of the other metaservers as well
|
|
func (h *handler) servePing(w http.ResponseWriter, r *http.Request) {
|
|
// if they're not asking to check all servers, just return who we think
|
|
// the leader is
|
|
if r.URL.Query().Get("all") == "" {
|
|
w.Write([]byte(h.store.leader()))
|
|
return
|
|
}
|
|
|
|
leader := h.store.leader()
|
|
healthy := true
|
|
for _, n := range h.store.otherMetaServersHTTP() {
|
|
scheme := "http://"
|
|
if h.config.HTTPSEnabled {
|
|
scheme = "https://"
|
|
}
|
|
url := scheme + n + "/ping"
|
|
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
healthy = false
|
|
break
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
healthy = false
|
|
break
|
|
}
|
|
|
|
if leader != string(b) {
|
|
healthy = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if healthy {
|
|
w.Write([]byte(h.store.leader()))
|
|
return
|
|
}
|
|
|
|
h.httpError(fmt.Errorf("one or more metaservers not up"), w, http.StatusInternalServerError)
|
|
}
|
|
|
|
func (h *handler) servePeers(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Content-Type", "application/json")
|
|
enc := json.NewEncoder(w)
|
|
if err := enc.Encode(h.store.peers()); err != nil {
|
|
h.httpError(err, w, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// serveLease
|
|
func (h *handler) serveLease(w http.ResponseWriter, r *http.Request) {
|
|
var name, nodeIDStr string
|
|
q := r.URL.Query()
|
|
|
|
// Get the requested lease name.
|
|
name = q.Get("name")
|
|
if name == "" {
|
|
http.Error(w, "lease name required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get the ID of the requesting node.
|
|
nodeIDStr = q.Get("nodeid")
|
|
if nodeIDStr == "" {
|
|
http.Error(w, "node ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Redirect to leader if necessary.
|
|
leader := h.store.leaderHTTP()
|
|
if leader != h.s.httpAddr {
|
|
if leader == "" {
|
|
// No cluster leader. Client will have to try again later.
|
|
h.httpError(errors.New("no leader"), w, http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
scheme := "http://"
|
|
if h.config.HTTPSEnabled {
|
|
scheme = "https://"
|
|
}
|
|
|
|
leader = scheme + leader + "/lease?" + q.Encode()
|
|
http.Redirect(w, r, leader, http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
// Convert node ID to an int.
|
|
nodeID, err := strconv.ParseUint(nodeIDStr, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid node ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Try to acquire the requested lease.
|
|
// Always returns a lease. err determins if we own it.
|
|
l, err := h.leases.Acquire(name, nodeID)
|
|
// Marshal the lease to JSON.
|
|
b, e := json.Marshal(l)
|
|
if e != nil {
|
|
h.httpError(e, w, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Write HTTP status.
|
|
if err != nil {
|
|
// Another node owns the lease.
|
|
w.WriteHeader(http.StatusConflict)
|
|
} else {
|
|
// Lease successfully acquired.
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
// Write the lease data.
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(b)
|
|
return
|
|
}
|
|
|
|
type gzipResponseWriter struct {
|
|
io.Writer
|
|
http.ResponseWriter
|
|
}
|
|
|
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|
return w.Writer.Write(b)
|
|
}
|
|
|
|
func (w gzipResponseWriter) Flush() {
|
|
w.Writer.(*gzip.Writer).Flush()
|
|
}
|
|
|
|
func (w gzipResponseWriter) CloseNotify() <-chan bool {
|
|
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
|
}
|
|
|
|
// determines if the client can accept compressed responses, and encodes accordingly
|
|
func gzipFilter(inner http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
inner.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
gz := gzip.NewWriter(w)
|
|
defer gz.Close()
|
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
|
inner.ServeHTTP(gzw, r)
|
|
})
|
|
}
|
|
|
|
// versionHeader takes a HTTP handler and returns a HTTP handler
|
|
// and adds the X-INFLUXBD-VERSION header to outgoing responses.
|
|
func versionHeader(inner http.Handler, h *handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("X-InfluxDB-Version", h.s.Version)
|
|
inner.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func requestID(inner http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
uid := uuid.TimeUUID()
|
|
r.Header.Set("Request-Id", uid.String())
|
|
w.Header().Set("Request-Id", r.Header.Get("Request-Id"))
|
|
|
|
inner.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func logging(inner http.Handler, name string, weblog *log.Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
l := &responseLogger{w: w}
|
|
inner.ServeHTTP(l, r)
|
|
logLine := buildLogLine(l, r, start)
|
|
weblog.Println(logLine)
|
|
})
|
|
}
|
|
|
|
func recovery(inner http.Handler, name string, weblog *log.Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
l := &responseLogger{w: w}
|
|
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
b := make([]byte, 1024)
|
|
runtime.Stack(b, false)
|
|
logLine := buildLogLine(l, r, start)
|
|
logLine = fmt.Sprintf("%s [panic:%s]\n%s", logLine, err, string(b))
|
|
weblog.Println(logLine)
|
|
}
|
|
}()
|
|
|
|
inner.ServeHTTP(l, r)
|
|
})
|
|
}
|
|
|
|
func (h *handler) httpError(err error, w http.ResponseWriter, status int) {
|
|
if h.loggingEnabled {
|
|
h.logger.Println(err)
|
|
}
|
|
http.Error(w, "", status)
|
|
}
|
|
|
|
type Lease struct {
|
|
Name string `json:"name"`
|
|
Expiration time.Time `json:"expiration"`
|
|
Owner uint64 `json:"owner"`
|
|
}
|
|
|
|
type Leases struct {
|
|
mu sync.Mutex
|
|
m map[string]*Lease
|
|
d time.Duration
|
|
}
|
|
|
|
func NewLeases(d time.Duration) *Leases {
|
|
return &Leases{
|
|
m: make(map[string]*Lease),
|
|
d: d,
|
|
}
|
|
}
|
|
|
|
func (leases *Leases) Acquire(name string, nodeID uint64) (*Lease, error) {
|
|
leases.mu.Lock()
|
|
defer leases.mu.Unlock()
|
|
|
|
l, ok := leases.m[name]
|
|
if ok {
|
|
if time.Now().After(l.Expiration) || l.Owner == nodeID {
|
|
l.Expiration = time.Now().Add(leases.d)
|
|
l.Owner = nodeID
|
|
return l, nil
|
|
}
|
|
return l, errors.New("another node has the lease")
|
|
}
|
|
|
|
l = &Lease{
|
|
Name: name,
|
|
Expiration: time.Now().Add(leases.d),
|
|
Owner: nodeID,
|
|
}
|
|
|
|
leases.m[name] = l
|
|
|
|
return l, nil
|
|
}
|