mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
commit
2b291aabea
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
src/
|
||||
pkg/
|
||||
/etcd
|
||||
release_version.go
|
||||
/server/release_version.go
|
||||
/machine*
|
||||
|
2
build
2
build
@ -21,5 +21,5 @@ for i in third_party/*; do
|
||||
cp -R "$i" src/
|
||||
done
|
||||
|
||||
./scripts/release-version > release_version.go
|
||||
./scripts/release-version > server/release_version.go
|
||||
go build "${ETCD_PACKAGE}"
|
||||
|
@ -20,5 +20,5 @@ foreach($i in (ls third_party/*)){
|
||||
cp -Recurse -force "$i" src/
|
||||
}
|
||||
|
||||
./scripts/release-version.ps1 | Out-File -Encoding UTF8 release_version.go
|
||||
./scripts/release-version.ps1 | Out-File -Encoding UTF8 server/release_version.go
|
||||
go build -v "${ETCD_PACKAGE}"
|
||||
|
307
command.go
307
command.go
@ -1,307 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
const commandPrefix = "etcd:"
|
||||
|
||||
func commandName(name string) string {
|
||||
return commandPrefix + name
|
||||
}
|
||||
|
||||
// A command represents an action to be taken on the replicated state machine.
|
||||
type Command interface {
|
||||
CommandName() string
|
||||
Apply(server *raft.Server) (interface{}, error)
|
||||
}
|
||||
|
||||
// Create command
|
||||
type CreateCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
IncrementalSuffix bool `json:"incrementalSuffix"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
// The name of the create command in the log
|
||||
func (c *CreateCommand) CommandName() string {
|
||||
return commandName("create")
|
||||
}
|
||||
|
||||
// Create node
|
||||
func (c *CreateCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
e, err := s.Create(c.Key, c.Value, c.IncrementalSuffix, c.Force, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Update command
|
||||
type UpdateCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
}
|
||||
|
||||
// The name of the update command in the log
|
||||
func (c *UpdateCommand) CommandName() string {
|
||||
return commandName("update")
|
||||
}
|
||||
|
||||
// Update node
|
||||
func (c *UpdateCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
e, err := s.Update(c.Key, c.Value, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// TestAndSet command
|
||||
type TestAndSetCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
PrevValue string `json: prevValue`
|
||||
PrevIndex uint64 `json: prevValue`
|
||||
}
|
||||
|
||||
// The name of the testAndSet command in the log
|
||||
func (c *TestAndSetCommand) CommandName() string {
|
||||
return commandName("testAndSet")
|
||||
}
|
||||
|
||||
// Set the key-value pair if the current value of the key equals to the given prevValue
|
||||
func (c *TestAndSetCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
e, err := s.TestAndSet(c.Key, c.PrevValue, c.PrevIndex,
|
||||
c.Value, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Get command
|
||||
type GetCommand struct {
|
||||
Key string `json:"key"`
|
||||
Recursive bool `json:"recursive"`
|
||||
Sorted bool `json:"sorted"`
|
||||
}
|
||||
|
||||
// The name of the get command in the log
|
||||
func (c *GetCommand) CommandName() string {
|
||||
return commandName("get")
|
||||
}
|
||||
|
||||
// Get the value of key
|
||||
func (c *GetCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
e, err := s.Get(c.Key, c.Recursive, c.Sorted, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Delete command
|
||||
type DeleteCommand struct {
|
||||
Key string `json:"key"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// The name of the delete command in the log
|
||||
func (c *DeleteCommand) CommandName() string {
|
||||
return commandName("delete")
|
||||
}
|
||||
|
||||
// Delete the key
|
||||
func (c *DeleteCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
e, err := s.Delete(c.Key, c.Recursive, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Watch command
|
||||
type WatchCommand struct {
|
||||
Key string `json:"key"`
|
||||
SinceIndex uint64 `json:"sinceIndex"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// The name of the watch command in the log
|
||||
func (c *WatchCommand) CommandName() string {
|
||||
return commandName("watch")
|
||||
}
|
||||
|
||||
func (c *WatchCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
|
||||
eventChan, err := s.Watch(c.Key, c.Recursive, c.SinceIndex, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := <-eventChan
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// JoinCommand
|
||||
type JoinCommand struct {
|
||||
RaftVersion string `json:"raftVersion"`
|
||||
Name string `json:"name"`
|
||||
RaftURL string `json:"raftURL"`
|
||||
EtcdURL string `json:"etcdURL"`
|
||||
}
|
||||
|
||||
func newJoinCommand(version, name, raftUrl, etcdUrl string) *JoinCommand {
|
||||
return &JoinCommand{
|
||||
RaftVersion: version,
|
||||
Name: name,
|
||||
RaftURL: raftUrl,
|
||||
EtcdURL: etcdUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// The name of the join command in the log
|
||||
func (c *JoinCommand) CommandName() string {
|
||||
return commandName("join")
|
||||
}
|
||||
|
||||
// Join a server to the cluster
|
||||
func (c *JoinCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
r, _ := server.Context().(*raftServer)
|
||||
|
||||
// check if the join command is from a previous machine, who lost all its previous log.
|
||||
e, _ := s.Get(path.Join("/_etcd/machines", c.Name), false, false, server.CommitIndex(), server.Term())
|
||||
|
||||
b := make([]byte, 8)
|
||||
binary.PutUvarint(b, server.CommitIndex())
|
||||
|
||||
if e != nil {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// check machine number in the cluster
|
||||
num := machineNum()
|
||||
if num == maxClusterSize {
|
||||
debug("Reject join request from ", c.Name)
|
||||
return []byte{0}, etcdErr.NewError(etcdErr.EcodeNoMoreMachine, "", server.CommitIndex(), server.Term())
|
||||
}
|
||||
|
||||
addNameToURL(c.Name, c.RaftVersion, c.RaftURL, c.EtcdURL)
|
||||
|
||||
// add peer in raft
|
||||
err := server.AddPeer(c.Name, "")
|
||||
|
||||
// add machine in etcd storage
|
||||
key := path.Join("_etcd/machines", c.Name)
|
||||
value := fmt.Sprintf("raft=%s&etcd=%s&raftVersion=%s", c.RaftURL, c.EtcdURL, c.RaftVersion)
|
||||
s.Create(key, value, false, false, store.Permanent, server.CommitIndex(), server.Term())
|
||||
|
||||
// add peer stats
|
||||
if c.Name != r.Name() {
|
||||
r.followersStats.Followers[c.Name] = &raftFollowerStats{}
|
||||
r.followersStats.Followers[c.Name].Latency.Minimum = 1 << 63
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (c *JoinCommand) NodeName() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
// RemoveCommand
|
||||
type RemoveCommand struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// The name of the remove command in the log
|
||||
func (c *RemoveCommand) CommandName() string {
|
||||
return commandName("remove")
|
||||
}
|
||||
|
||||
// Remove a server from the cluster
|
||||
func (c *RemoveCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*store.Store)
|
||||
r, _ := server.Context().(*raftServer)
|
||||
|
||||
// remove machine in etcd storage
|
||||
key := path.Join("_etcd/machines", c.Name)
|
||||
|
||||
_, err := s.Delete(key, false, server.CommitIndex(), server.Term())
|
||||
// delete from stats
|
||||
delete(r.followersStats.Followers, c.Name)
|
||||
|
||||
if err != nil {
|
||||
return []byte{0}, err
|
||||
}
|
||||
|
||||
// remove peer in raft
|
||||
err = server.RemovePeer(c.Name)
|
||||
|
||||
if err != nil {
|
||||
return []byte{0}, err
|
||||
}
|
||||
|
||||
if c.Name == server.Name() {
|
||||
// the removed node is this node
|
||||
|
||||
// if the node is not replaying the previous logs
|
||||
// and the node has sent out a join request in this
|
||||
// start. It is sure that this node received a new remove
|
||||
// command and need to be removed
|
||||
if server.CommitIndex() > r.joinIndex && r.joinIndex != 0 {
|
||||
debugf("server [%s] is removed", server.Name())
|
||||
os.Exit(0)
|
||||
} else {
|
||||
// else ignore remove
|
||||
debugf("ignore previous remove command.")
|
||||
}
|
||||
}
|
||||
|
||||
b := make([]byte, 8)
|
||||
binary.PutUvarint(b, server.CommitIndex())
|
||||
|
||||
return b, err
|
||||
}
|
19
config.go
19
config.go
@ -8,6 +8,9 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/server"
|
||||
)
|
||||
|
||||
//--------------------------------------
|
||||
@ -30,7 +33,7 @@ func getInfo(path string) *Info {
|
||||
os.Remove(confPath)
|
||||
os.RemoveAll(snapshotPath)
|
||||
} else if info := readInfo(infoPath); info != nil {
|
||||
infof("Found node configuration in '%s'. Ignoring flags", infoPath)
|
||||
log.Infof("Found node configuration in '%s'. Ignoring flags", infoPath)
|
||||
return info
|
||||
}
|
||||
|
||||
@ -41,10 +44,10 @@ func getInfo(path string) *Info {
|
||||
content, _ := json.MarshalIndent(info, "", " ")
|
||||
content = []byte(string(content) + "\n")
|
||||
if err := ioutil.WriteFile(infoPath, content, 0644); err != nil {
|
||||
fatalf("Unable to write info to file: %v", err)
|
||||
log.Fatalf("Unable to write info to file: %v", err)
|
||||
}
|
||||
|
||||
infof("Wrote node configuration to '%s'", infoPath)
|
||||
log.Infof("Wrote node configuration to '%s'", infoPath)
|
||||
|
||||
return info
|
||||
}
|
||||
@ -57,7 +60,7 @@ func readInfo(path string) *Info {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@ -65,19 +68,19 @@ func readInfo(path string) *Info {
|
||||
|
||||
content, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fatalf("Unable to read info: %v", err)
|
||||
log.Fatalf("Unable to read info: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(content, &info); err != nil {
|
||||
fatalf("Unable to parse info: %v", err)
|
||||
log.Fatalf("Unable to parse info: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func tlsConfigFromInfo(info TLSInfo) (t TLSConfig, ok bool) {
|
||||
func tlsConfigFromInfo(info server.TLSInfo) (t server.TLSConfig, ok bool) {
|
||||
var keyFile, certFile, CAFile string
|
||||
var tlsCert tls.Certificate
|
||||
var err error
|
||||
@ -101,7 +104,7 @@ func tlsConfigFromInfo(info TLSInfo) (t TLSConfig, ok bool) {
|
||||
|
||||
tlsCert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
t.Scheme = "https"
|
||||
|
96
etcd.go
96
etcd.go
@ -1,15 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
@ -21,7 +19,6 @@ import (
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
veryVerbose bool
|
||||
|
||||
machines string
|
||||
@ -44,12 +41,11 @@ var (
|
||||
|
||||
cpuprofile string
|
||||
|
||||
cors string
|
||||
corsList map[string]bool
|
||||
cors string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&verbose, "v", false, "verbose logging")
|
||||
flag.BoolVar(&log.Verbose, "v", false, "verbose logging")
|
||||
flag.BoolVar(&veryVerbose, "vv", false, "very verbose logging")
|
||||
|
||||
flag.StringVar(&machines, "C", "", "the ip address and port of a existing machines in the cluster, sepearate by comma")
|
||||
@ -87,24 +83,12 @@ func init() {
|
||||
flag.StringVar(&cors, "cors", "", "whitelist origins for cross-origin resource sharing (e.g. '*' or 'http://localhost:8001,etc')")
|
||||
}
|
||||
|
||||
const (
|
||||
ElectionTimeout = 200 * time.Millisecond
|
||||
HeartbeatTimeout = 50 * time.Millisecond
|
||||
RetryInterval = 10
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Typedefs
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type TLSInfo struct {
|
||||
CertFile string `json:"CertFile"`
|
||||
KeyFile string `json:"KeyFile"`
|
||||
CAFile string `json:"CAFile"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@ -115,24 +99,10 @@ type Info struct {
|
||||
RaftListenHost string `json:"raftListenHost"`
|
||||
EtcdListenHost string `json:"etcdListenHost"`
|
||||
|
||||
RaftTLS TLSInfo `json:"raftTLS"`
|
||||
EtcdTLS TLSInfo `json:"etcdTLS"`
|
||||
RaftTLS server.TLSInfo `json:"raftTLS"`
|
||||
EtcdTLS server.TLSInfo `json:"etcdTLS"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
Scheme string
|
||||
Server tls.Config
|
||||
Client tls.Config
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Variables
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
var etcdStore *store.Store
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Functions
|
||||
@ -151,18 +121,16 @@ func main() {
|
||||
}
|
||||
|
||||
if veryVerbose {
|
||||
verbose = true
|
||||
log.Verbose = true
|
||||
raft.SetLogLevel(raft.Debug)
|
||||
}
|
||||
|
||||
parseCorsFlag()
|
||||
|
||||
if machines != "" {
|
||||
cluster = strings.Split(machines, ",")
|
||||
} else if machinesFile != "" {
|
||||
b, err := ioutil.ReadFile(machinesFile)
|
||||
if err != nil {
|
||||
fatalf("Unable to read the given machines file: %s", err)
|
||||
log.Fatalf("Unable to read the given machines file: %s", err)
|
||||
}
|
||||
cluster = strings.Split(string(b), ",")
|
||||
}
|
||||
@ -170,17 +138,17 @@ func main() {
|
||||
// Check TLS arguments
|
||||
raftTLSConfig, ok := tlsConfigFromInfo(argInfo.RaftTLS)
|
||||
if !ok {
|
||||
fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
|
||||
log.Fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
|
||||
}
|
||||
|
||||
etcdTLSConfig, ok := tlsConfigFromInfo(argInfo.EtcdTLS)
|
||||
if !ok {
|
||||
fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
|
||||
log.Fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
|
||||
}
|
||||
|
||||
argInfo.Name = strings.TrimSpace(argInfo.Name)
|
||||
if argInfo.Name == "" {
|
||||
fatal("ERROR: server name required. e.g. '-n=server_name'")
|
||||
log.Fatal("ERROR: server name required. e.g. '-n=server_name'")
|
||||
}
|
||||
|
||||
// Check host name arguments
|
||||
@ -193,39 +161,29 @@ func main() {
|
||||
|
||||
// Read server info from file or grab it from user.
|
||||
if err := os.MkdirAll(dirPath, 0744); err != nil {
|
||||
fatalf("Unable to create path: %s", err)
|
||||
log.Fatalf("Unable to create path: %s", err)
|
||||
}
|
||||
|
||||
info := getInfo(dirPath)
|
||||
|
||||
// Create etcd key-value store
|
||||
etcdStore = store.New()
|
||||
store := store.New()
|
||||
|
||||
// Create etcd and raft server
|
||||
r := newRaftServer(info.Name, info.RaftURL, info.RaftListenHost, &raftTLSConfig, &info.RaftTLS)
|
||||
snapConf = r.newSnapshotConf()
|
||||
// Create a shared node registry.
|
||||
registry := server.NewRegistry(store)
|
||||
|
||||
e = newEtcdServer(info.Name, info.EtcdURL, info.EtcdListenHost, &etcdTLSConfig, &info.EtcdTLS, r)
|
||||
// Create peer server.
|
||||
ps := server.NewPeerServer(info.Name, dirPath, info.RaftURL, info.RaftListenHost, &raftTLSConfig, &info.RaftTLS, registry, store)
|
||||
ps.MaxClusterSize = maxClusterSize
|
||||
ps.RetryTimes = retryTimes
|
||||
|
||||
r.ListenAndServe()
|
||||
e.ListenAndServe()
|
||||
|
||||
}
|
||||
|
||||
// parseCorsFlag gathers up the cors whitelist and puts it into the corsList.
|
||||
func parseCorsFlag() {
|
||||
if cors != "" {
|
||||
corsList = make(map[string]bool)
|
||||
list := strings.Split(cors, ",")
|
||||
for _, v := range list {
|
||||
fmt.Println(v)
|
||||
if v != "*" {
|
||||
_, err := url.Parse(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("bad cors url: %s", err))
|
||||
}
|
||||
}
|
||||
corsList[v] = true
|
||||
}
|
||||
s := server.New(info.Name, info.EtcdURL, info.EtcdListenHost, &etcdTLSConfig, &info.EtcdTLS, ps, registry, store)
|
||||
if err := s.AllowOrigins(cors); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ps.SetServer(s)
|
||||
|
||||
ps.ListenAndServe(snapshot, cluster)
|
||||
s.ListenAndServe()
|
||||
}
|
||||
|
@ -1,250 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Handlers to handle etcd-store related request via etcd url
|
||||
//-------------------------------------------------------------------
|
||||
// Multiplex GET/POST/DELETE request to corresponding handlers
|
||||
func (e *etcdServer) MultiplexerV1(w http.ResponseWriter, req *http.Request) error {
|
||||
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return e.GetHttpHandlerV1(w, req)
|
||||
case "POST":
|
||||
return e.SetHttpHandlerV1(w, req)
|
||||
case "PUT":
|
||||
return e.SetHttpHandlerV1(w, req)
|
||||
case "DELETE":
|
||||
return e.DeleteHttpHandlerV1(w, req)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// State sensitive handlers
|
||||
// Set/Delete will dispatch to leader
|
||||
//--------------------------------------
|
||||
|
||||
// Set Command Handler
|
||||
func (e *etcdServer) SetHttpHandlerV1(w http.ResponseWriter, req *http.Request) error {
|
||||
key := req.URL.Path[len("/v1/keys/"):]
|
||||
|
||||
debugf("[recv] POST %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
|
||||
|
||||
req.ParseForm()
|
||||
|
||||
value := req.Form.Get("value")
|
||||
|
||||
if len(value) == 0 {
|
||||
return etcdErr.NewError(200, "Set", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
strDuration := req.Form.Get("ttl")
|
||||
|
||||
expireTime, err := durationToExpireTime(strDuration)
|
||||
|
||||
if err != nil {
|
||||
return etcdErr.NewError(202, "Set", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
if prevValueArr, ok := req.Form["prevValue"]; ok && len(prevValueArr) > 0 {
|
||||
command := &TestAndSetCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
PrevValue: prevValueArr[0],
|
||||
ExpireTime: expireTime,
|
||||
}
|
||||
|
||||
return dispatchEtcdCommandV1(command, w, req)
|
||||
|
||||
} else {
|
||||
command := &CreateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
Force: true,
|
||||
}
|
||||
|
||||
return dispatchEtcdCommandV1(command, w, req)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Handler
|
||||
func (e *etcdServer) DeleteHttpHandlerV1(w http.ResponseWriter, req *http.Request) error {
|
||||
key := req.URL.Path[len("/v1/keys/"):]
|
||||
|
||||
debugf("[recv] DELETE %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
|
||||
|
||||
command := &DeleteCommand{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
return dispatchEtcdCommandV1(command, w, req)
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// State non-sensitive handlers
|
||||
// will not dispatch to leader
|
||||
// TODO: add sensitive version for these
|
||||
// command?
|
||||
//--------------------------------------
|
||||
|
||||
// Get Handler
|
||||
func (e *etcdServer) GetHttpHandlerV1(w http.ResponseWriter, req *http.Request) error {
|
||||
key := req.URL.Path[len("/v1/keys/"):]
|
||||
|
||||
r := e.raftServer
|
||||
debugf("[recv] GET %s/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
|
||||
|
||||
command := &GetCommand{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if event, err := command.Apply(r.Server); err != nil {
|
||||
return err
|
||||
} else {
|
||||
event, _ := event.(*store.Event)
|
||||
|
||||
response := eventToResponse(event)
|
||||
bytes, _ := json.Marshal(response)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Watch handler
|
||||
func (e *etcdServer) WatchHttpHandlerV1(w http.ResponseWriter, req *http.Request) error {
|
||||
key := req.URL.Path[len("/v1/watch/"):]
|
||||
|
||||
command := &WatchCommand{
|
||||
Key: key,
|
||||
}
|
||||
r := e.raftServer
|
||||
if req.Method == "GET" {
|
||||
debugf("[recv] GET %s/watch/%s [%s]", e.url, key, req.RemoteAddr)
|
||||
command.SinceIndex = 0
|
||||
|
||||
} else if req.Method == "POST" {
|
||||
// watch from a specific index
|
||||
|
||||
debugf("[recv] POST %s/watch/%s [%s]", e.url, key, req.RemoteAddr)
|
||||
content := req.FormValue("index")
|
||||
|
||||
sinceIndex, err := strconv.ParseUint(string(content), 10, 64)
|
||||
if err != nil {
|
||||
return etcdErr.NewError(203, "Watch From Index", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
command.SinceIndex = sinceIndex
|
||||
|
||||
} else {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
if event, err := command.Apply(r.Server); err != nil {
|
||||
return etcdErr.NewError(500, key, store.UndefIndex, store.UndefTerm)
|
||||
} else {
|
||||
event, _ := event.(*store.Event)
|
||||
|
||||
response := eventToResponse(event)
|
||||
bytes, _ := json.Marshal(response)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
w.Write(bytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Dispatch the command to leader
|
||||
func dispatchEtcdCommandV1(c Command, w http.ResponseWriter, req *http.Request) error {
|
||||
return dispatchV1(c, w, req, nameToEtcdURL)
|
||||
}
|
||||
|
||||
func dispatchV1(c Command, w http.ResponseWriter, req *http.Request, toURL func(name string) (string, bool)) error {
|
||||
r := e.raftServer
|
||||
if r.State() == raft.Leader {
|
||||
if event, err := r.Do(c); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if event == nil {
|
||||
return etcdErr.NewError(300, "Empty result from raft", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
event, _ := event.(*store.Event)
|
||||
|
||||
response := eventToResponse(event)
|
||||
bytes, _ := json.Marshal(response)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
leader := r.Leader()
|
||||
// current no leader
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
url, _ := toURL(leader)
|
||||
|
||||
redirect(url, w, req)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func eventToResponse(event *store.Event) interface{} {
|
||||
if !event.Dir {
|
||||
response := &store.Response{
|
||||
Action: event.Action,
|
||||
Key: event.Key,
|
||||
Value: event.Value,
|
||||
PrevValue: event.PrevValue,
|
||||
Index: event.Index,
|
||||
TTL: event.TTL,
|
||||
Expiration: event.Expiration,
|
||||
}
|
||||
|
||||
if response.Action == store.Create || response.Action == store.Update {
|
||||
response.Action = "set"
|
||||
if response.PrevValue == "" {
|
||||
response.NewKey = true
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} else {
|
||||
responses := make([]*store.Response, len(event.KVPairs))
|
||||
|
||||
for i, kv := range event.KVPairs {
|
||||
responses[i] = &store.Response{
|
||||
Action: event.Action,
|
||||
Key: kv.Key,
|
||||
Value: kv.Value,
|
||||
Dir: kv.Dir,
|
||||
Index: event.Index,
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
}
|
364
etcd_handlers.go
364
etcd_handlers.go
@ -1,364 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Handlers to handle etcd-store related request via etcd url
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
func NewEtcdMuxer() *http.ServeMux {
|
||||
// external commands
|
||||
etcdMux := http.NewServeMux()
|
||||
etcdMux.Handle("/"+version+"/keys/", errorHandler(e.Multiplexer))
|
||||
etcdMux.Handle("/"+version+"/leader", errorHandler(e.LeaderHttpHandler))
|
||||
etcdMux.Handle("/"+version+"/machines", errorHandler(e.MachinesHttpHandler))
|
||||
etcdMux.Handle("/"+version+"/stats/", errorHandler(e.StatsHttpHandler))
|
||||
etcdMux.Handle("/version", errorHandler(e.VersionHttpHandler))
|
||||
etcdMux.HandleFunc("/test/", TestHttpHandler)
|
||||
|
||||
// backward support
|
||||
etcdMux.Handle("/v1/keys/", errorHandler(e.MultiplexerV1))
|
||||
etcdMux.Handle("/v1/leader", errorHandler(e.LeaderHttpHandler))
|
||||
etcdMux.Handle("/v1/machines", errorHandler(e.MachinesHttpHandler))
|
||||
etcdMux.Handle("/v1/stats/", errorHandler(e.StatsHttpHandler))
|
||||
|
||||
return etcdMux
|
||||
}
|
||||
|
||||
type errorHandler func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// addCorsHeader parses the request Origin header and loops through the user
|
||||
// provided allowed origins and sets the Access-Control-Allow-Origin header if
|
||||
// there is a match.
|
||||
func addCorsHeader(w http.ResponseWriter, r *http.Request) {
|
||||
val, ok := corsList["*"]
|
||||
if val && ok {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
return
|
||||
}
|
||||
|
||||
requestOrigin := r.Header.Get("Origin")
|
||||
val, ok = corsList[requestOrigin]
|
||||
if val && ok {
|
||||
w.Header().Add("Access-Control-Allow-Origin", requestOrigin)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (fn errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
addCorsHeader(w, r)
|
||||
if e := fn(w, r); e != nil {
|
||||
if etcdErr, ok := e.(*etcdErr.Error); ok {
|
||||
debug("Return error: ", (*etcdErr).Error())
|
||||
etcdErr.Write(w)
|
||||
} else {
|
||||
http.Error(w, e.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplex GET/POST/DELETE request to corresponding handlers
|
||||
func (e *etcdServer) Multiplexer(w http.ResponseWriter, req *http.Request) error {
|
||||
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return e.GetHttpHandler(w, req)
|
||||
case "POST":
|
||||
return e.CreateHttpHandler(w, req)
|
||||
case "PUT":
|
||||
return e.UpdateHttpHandler(w, req)
|
||||
case "DELETE":
|
||||
return e.DeleteHttpHandler(w, req)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// State sensitive handlers
|
||||
// Set/Delete will dispatch to leader
|
||||
//--------------------------------------
|
||||
|
||||
func (e *etcdServer) CreateHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
key := getNodePath(req.URL.Path)
|
||||
|
||||
debugf("recv.post[%v] [%v%v]\n", req.RemoteAddr, req.Host, req.URL)
|
||||
|
||||
value := req.FormValue("value")
|
||||
|
||||
expireTime, err := durationToExpireTime(req.FormValue("ttl"))
|
||||
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Create", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
command := &CreateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
}
|
||||
|
||||
if req.FormValue("incremental") == "true" {
|
||||
command.IncrementalSuffix = true
|
||||
}
|
||||
|
||||
return e.dispatchEtcdCommand(command, w, req)
|
||||
|
||||
}
|
||||
|
||||
func (e *etcdServer) UpdateHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
key := getNodePath(req.URL.Path)
|
||||
|
||||
debugf("recv.put[%v] [%v%v]\n", req.RemoteAddr, req.Host, req.URL)
|
||||
|
||||
req.ParseForm()
|
||||
|
||||
value := req.Form.Get("value")
|
||||
|
||||
expireTime, err := durationToExpireTime(req.Form.Get("ttl"))
|
||||
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
// update should give at least one option
|
||||
if value == "" && expireTime.Sub(store.Permanent) == 0 {
|
||||
return etcdErr.NewError(etcdErr.EcodeValueOrTTLRequired, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
prevValue, valueOk := req.Form["prevValue"]
|
||||
|
||||
prevIndexStr, indexOk := req.Form["prevIndex"]
|
||||
|
||||
if !valueOk && !indexOk { // update without test
|
||||
command := &UpdateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
}
|
||||
|
||||
return e.dispatchEtcdCommand(command, w, req)
|
||||
|
||||
} else { // update with test
|
||||
var prevIndex uint64
|
||||
|
||||
if indexOk {
|
||||
prevIndex, err = strconv.ParseUint(prevIndexStr[0], 10, 64)
|
||||
|
||||
// bad previous index
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
} else {
|
||||
prevIndex = 0
|
||||
}
|
||||
|
||||
command := &TestAndSetCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
PrevValue: prevValue[0],
|
||||
PrevIndex: prevIndex,
|
||||
}
|
||||
|
||||
return e.dispatchEtcdCommand(command, w, req)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete Handler
|
||||
func (e *etcdServer) DeleteHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
key := getNodePath(req.URL.Path)
|
||||
|
||||
debugf("recv.delete[%v] [%v%v]\n", req.RemoteAddr, req.Host, req.URL)
|
||||
|
||||
command := &DeleteCommand{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if req.FormValue("recursive") == "true" {
|
||||
command.Recursive = true
|
||||
}
|
||||
|
||||
return e.dispatchEtcdCommand(command, w, req)
|
||||
}
|
||||
|
||||
// Dispatch the command to leader
|
||||
func (e *etcdServer) dispatchEtcdCommand(c Command, w http.ResponseWriter, req *http.Request) error {
|
||||
return e.raftServer.dispatch(c, w, req, nameToEtcdURL)
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// State non-sensitive handlers
|
||||
// command with consistent option will
|
||||
// still dispatch to the leader
|
||||
//--------------------------------------
|
||||
|
||||
// Handler to return the current leader's raft address
|
||||
func (e *etcdServer) LeaderHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
r := e.raftServer
|
||||
|
||||
leader := r.Leader()
|
||||
|
||||
if leader != "" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
raftURL, _ := nameToRaftURL(leader)
|
||||
w.Write([]byte(raftURL))
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return etcdErr.NewError(etcdErr.EcodeLeaderElect, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler to return all the known machines in the current cluster
|
||||
func (e *etcdServer) MachinesHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
machines := e.raftServer.getMachines(nameToEtcdURL)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(strings.Join(machines, ", ")))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler to return the current version of etcd
|
||||
func (e *etcdServer) VersionHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "etcd %s", releaseVersion)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler to return the basic stats of etcd
|
||||
func (e *etcdServer) StatsHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
option := req.URL.Path[len("/v1/stats/"):]
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
r := e.raftServer
|
||||
|
||||
switch option {
|
||||
case "self":
|
||||
w.Write(r.Stats())
|
||||
case "leader":
|
||||
if r.State() == raft.Leader {
|
||||
w.Write(r.PeerStats())
|
||||
} else {
|
||||
leader := r.Leader()
|
||||
// current no leader
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
hostname, _ := nameToEtcdURL(leader)
|
||||
redirect(hostname, w, req)
|
||||
}
|
||||
case "store":
|
||||
w.Write(etcdStore.JsonStats())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *etcdServer) GetHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
var err error
|
||||
var event interface{}
|
||||
|
||||
r := e.raftServer
|
||||
|
||||
debugf("recv.get[%v] [%v%v]\n", req.RemoteAddr, req.Host, req.URL)
|
||||
|
||||
if req.FormValue("consistent") == "true" && r.State() != raft.Leader {
|
||||
// help client to redirect the request to the current leader
|
||||
leader := r.Leader()
|
||||
hostname, _ := nameToEtcdURL(leader)
|
||||
redirect(hostname, w, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
key := getNodePath(req.URL.Path)
|
||||
|
||||
recursive := req.FormValue("recursive")
|
||||
|
||||
if req.FormValue("wait") == "true" { // watch
|
||||
command := &WatchCommand{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if recursive == "true" {
|
||||
command.Recursive = true
|
||||
}
|
||||
|
||||
indexStr := req.FormValue("wait_index")
|
||||
if indexStr != "" {
|
||||
sinceIndex, err := strconv.ParseUint(indexStr, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Watch From Index", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
command.SinceIndex = sinceIndex
|
||||
}
|
||||
|
||||
event, err = command.Apply(r.Server)
|
||||
|
||||
} else { //get
|
||||
|
||||
command := &GetCommand{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
sorted := req.FormValue("sorted")
|
||||
if sorted == "true" {
|
||||
command.Sorted = true
|
||||
}
|
||||
|
||||
if recursive == "true" {
|
||||
command.Recursive = true
|
||||
}
|
||||
|
||||
event, err = command.Apply(r.Server)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
} else {
|
||||
event, _ := event.(*store.Event)
|
||||
bytes, _ := json.Marshal(event)
|
||||
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(event.Index))
|
||||
w.Header().Add("X-Etcd-Term", fmt.Sprint(event.Term))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestHandler
|
||||
func TestHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
testType := req.URL.Path[len("/test/"):]
|
||||
|
||||
if testType == "speed" {
|
||||
directSet()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("speed test success"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type etcdServer struct {
|
||||
http.Server
|
||||
raftServer *raftServer
|
||||
name string
|
||||
url string
|
||||
tlsConf *TLSConfig
|
||||
tlsInfo *TLSInfo
|
||||
}
|
||||
|
||||
var e *etcdServer
|
||||
|
||||
func newEtcdServer(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, raftServer *raftServer) *etcdServer {
|
||||
e = &etcdServer{
|
||||
Server: http.Server{
|
||||
TLSConfig: &tlsConf.Server,
|
||||
Addr: listenHost,
|
||||
},
|
||||
name: name,
|
||||
url: urlStr,
|
||||
tlsConf: tlsConf,
|
||||
tlsInfo: tlsInfo,
|
||||
raftServer: raftServer,
|
||||
}
|
||||
e.Handler = NewEtcdMuxer()
|
||||
return e
|
||||
}
|
||||
|
||||
// Start to listen and response etcd client command
|
||||
func (e *etcdServer) ListenAndServe() {
|
||||
|
||||
infof("etcd server [name %s, listen on %s, advertised url %s]", e.name, e.Server.Addr, e.url)
|
||||
|
||||
if e.tlsConf.Scheme == "http" {
|
||||
fatal(e.Server.ListenAndServe())
|
||||
} else {
|
||||
fatal(e.Server.ListenAndServeTLS(e.tlsInfo.CertFile, e.tlsInfo.KeyFile))
|
||||
}
|
||||
}
|
26
etcd_test.go
26
etcd_test.go
@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/test"
|
||||
"github.com/coreos/go-etcd/etcd"
|
||||
)
|
||||
@ -39,7 +40,7 @@ func TestSingleNode(t *testing.T) {
|
||||
|
||||
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.TTL < 95 {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatal("Set 1: ", err)
|
||||
}
|
||||
|
||||
t.Fatalf("Set 1 failed with %s %s %v", result.Key, result.Value, result.TTL)
|
||||
@ -51,7 +52,7 @@ func TestSingleNode(t *testing.T) {
|
||||
|
||||
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.PrevValue != "bar" || result.TTL != 100 {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatal("Set 2: ", err)
|
||||
}
|
||||
t.Fatalf("Set 2 failed with %s %s %v", result.Key, result.Value, result.TTL)
|
||||
}
|
||||
@ -105,7 +106,7 @@ func TestInternalVersionFail(t *testing.T) {
|
||||
|
||||
procAttr := new(os.ProcAttr)
|
||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
||||
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/node1", "-vv", "-C=" + fakeURL.Host}
|
||||
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/node1", "-C=" + fakeURL.Host}
|
||||
|
||||
process, err := os.StartProcess("etcd", args, procAttr)
|
||||
if err != nil {
|
||||
@ -248,6 +249,7 @@ func TestMultiNodeKillAllAndRecovery(t *testing.T) {
|
||||
|
||||
clusterSize := 5
|
||||
argGroup, etcds, err := test.CreateCluster(clusterSize, procAttr, false)
|
||||
defer test.DestroyCluster(etcds)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("cannot create cluster")
|
||||
@ -293,15 +295,12 @@ func TestMultiNodeKillAllAndRecovery(t *testing.T) {
|
||||
result, err := c.Set("foo", "bar", 0)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
t.Fatalf("Recovery error: %s", err)
|
||||
}
|
||||
|
||||
if result.Index != 18 {
|
||||
t.Fatalf("recovery failed! [%d/18]", result.Index)
|
||||
}
|
||||
|
||||
// kill all
|
||||
test.DestroyCluster(etcds)
|
||||
}
|
||||
|
||||
// Create a five nodes
|
||||
@ -398,8 +397,8 @@ func TestKillLeader(t *testing.T) {
|
||||
totalTime += take
|
||||
avgTime := totalTime / (time.Duration)(i+1)
|
||||
|
||||
fmt.Println("Leader election time is ", take, "with election timeout", ElectionTimeout)
|
||||
fmt.Println("Leader election time average is", avgTime, "with election timeout", ElectionTimeout)
|
||||
fmt.Println("Leader election time is ", take, "with election timeout", server.ElectionTimeout)
|
||||
fmt.Println("Leader election time average is", avgTime, "with election timeout", server.ElectionTimeout)
|
||||
etcds[num], err = os.StartProcess("etcd", argGroup[num], procAttr)
|
||||
}
|
||||
stop <- true
|
||||
@ -456,7 +455,7 @@ func TestKillRandom(t *testing.T) {
|
||||
etcds[num].Wait()
|
||||
}
|
||||
|
||||
time.Sleep(ElectionTimeout)
|
||||
time.Sleep(server.ElectionTimeout)
|
||||
|
||||
<-leaderChan
|
||||
|
||||
@ -478,6 +477,7 @@ func TestRemoveNode(t *testing.T) {
|
||||
|
||||
clusterSize := 3
|
||||
argGroup, etcds, _ := test.CreateCluster(clusterSize, procAttr, false)
|
||||
defer test.DestroyCluster(etcds)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@ -525,7 +525,7 @@ func TestRemoveNode(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(resp) != 3 {
|
||||
t.Fatal("add machine fails")
|
||||
t.Fatalf("add machine fails #1 (%d != 3)", len(resp))
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,12 +567,10 @@ func TestRemoveNode(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(resp) != 3 {
|
||||
t.Fatal("add machine fails")
|
||||
t.Fatalf("add machine fails #2 (%d != 3)", len(resp))
|
||||
}
|
||||
}
|
||||
}
|
||||
test.DestroyCluster(etcds)
|
||||
|
||||
}
|
||||
|
||||
func templateBenchmarkEtcdDirectCall(b *testing.B, tls bool) {
|
||||
|
44
log/log.go
Normal file
44
log/log.go
Normal file
@ -0,0 +1,44 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
golog "github.com/coreos/go-log/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// The Verbose flag turns on verbose logging.
|
||||
var Verbose bool = false
|
||||
|
||||
var logger *golog.Logger = golog.New("etcd", false,
|
||||
golog.CombinedSink(os.Stdout, "[%s] %s %-9s | %s\n", []string{"prefix", "time", "priority", "message"}))
|
||||
|
||||
func Infof(format string, v ...interface{}) {
|
||||
logger.Infof(format, v...)
|
||||
}
|
||||
|
||||
func Debugf(format string, v ...interface{}) {
|
||||
if Verbose {
|
||||
logger.Debugf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
if Verbose {
|
||||
logger.Debug(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func Warnf(format string, v ...interface{}) {
|
||||
logger.Warningf(format, v...)
|
||||
}
|
||||
|
||||
func Warn(v ...interface{}) {
|
||||
logger.Warning(v...)
|
||||
}
|
||||
|
||||
func Fatalf(format string, v ...interface{}) {
|
||||
logger.Fatalf(format, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
logger.Fatalln(v...)
|
||||
}
|
45
machines.go
45
machines.go
@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
// machineNum returns the number of machines in the cluster
|
||||
func machineNum() int {
|
||||
e, err := etcdStore.Get("/_etcd/machines", false, false, 0, 0)
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(e.KVPairs)
|
||||
}
|
||||
|
||||
// getMachines gets the current machines in the cluster
|
||||
func (r *raftServer) getMachines(toURL func(string) (string, bool)) []string {
|
||||
peers := r.Peers()
|
||||
|
||||
machines := make([]string, len(peers)+1)
|
||||
|
||||
leader, ok := toURL(r.Leader())
|
||||
self, _ := toURL(r.Name())
|
||||
i := 1
|
||||
|
||||
if ok {
|
||||
machines[0] = leader
|
||||
if leader != self {
|
||||
machines[1] = self
|
||||
i = 2
|
||||
}
|
||||
} else {
|
||||
machines[0] = self
|
||||
}
|
||||
|
||||
// Add all peers to the slice
|
||||
for peerName, _ := range peers {
|
||||
if machine, ok := toURL(peerName); ok {
|
||||
// do not add leader twice
|
||||
if machine != leader {
|
||||
machines[i] = machine
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return machines
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
// we map node name to url
|
||||
type nodeInfo struct {
|
||||
raftVersion string
|
||||
raftURL string
|
||||
etcdURL string
|
||||
}
|
||||
|
||||
var namesMap = make(map[string]*nodeInfo)
|
||||
|
||||
// nameToEtcdURL maps node name to its etcd http address
|
||||
func nameToEtcdURL(name string) (string, bool) {
|
||||
|
||||
if info, ok := namesMap[name]; ok {
|
||||
// first try to read from the map
|
||||
return info.etcdURL, true
|
||||
}
|
||||
|
||||
// if fails, try to recover from etcd storage
|
||||
return readURL(name, "etcd")
|
||||
|
||||
}
|
||||
|
||||
// nameToRaftURL maps node name to its raft http address
|
||||
func nameToRaftURL(name string) (string, bool) {
|
||||
if info, ok := namesMap[name]; ok {
|
||||
// first try to read from the map
|
||||
return info.raftURL, true
|
||||
|
||||
}
|
||||
|
||||
// if fails, try to recover from etcd storage
|
||||
return readURL(name, "raft")
|
||||
}
|
||||
|
||||
// addNameToURL add a name that maps to raftURL and etcdURL
|
||||
func addNameToURL(name string, version string, raftURL string, etcdURL string) {
|
||||
namesMap[name] = &nodeInfo{
|
||||
raftVersion: raftVersion,
|
||||
raftURL: raftURL,
|
||||
etcdURL: etcdURL,
|
||||
}
|
||||
}
|
||||
|
||||
func readURL(nodeName string, urlName string) (string, bool) {
|
||||
if nodeName == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// convert nodeName to url from etcd storage
|
||||
key := path.Join("/_etcd/machines", nodeName)
|
||||
|
||||
e, err := etcdStore.Get(key, false, false, 0, 0)
|
||||
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
m, err := url.ParseQuery(e.Value)
|
||||
|
||||
if err != nil {
|
||||
panic("Failed to parse machines entry")
|
||||
}
|
||||
|
||||
url := m[urlName][0]
|
||||
|
||||
return url, true
|
||||
}
|
147
raft_handlers.go
147
raft_handlers.go
@ -1,147 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
//-------------------------------------------------------------
|
||||
// Handlers to handle raft related request via raft server port
|
||||
//-------------------------------------------------------------
|
||||
|
||||
// Get all the current logs
|
||||
func (r *raftServer) GetLogHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
debugf("[recv] GET %s/log", r.url)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(r.LogEntries())
|
||||
}
|
||||
|
||||
// Response to vote request
|
||||
func (r *raftServer) VoteHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
rvreq := &raft.RequestVoteRequest{}
|
||||
err := decodeJsonRequest(req, rvreq)
|
||||
if err == nil {
|
||||
debugf("[recv] POST %s/vote [%s]", r.url, rvreq.CandidateName)
|
||||
if resp := r.RequestVote(rvreq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
warnf("[vote] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to append entries request
|
||||
func (r *raftServer) AppendEntriesHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.AppendEntriesRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
|
||||
if err == nil {
|
||||
debugf("[recv] POST %s/log/append [%d]", r.url, len(aereq.Entries))
|
||||
|
||||
r.serverStats.RecvAppendReq(aereq.LeaderName, int(req.ContentLength))
|
||||
|
||||
if resp := r.AppendEntries(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
if !resp.Success {
|
||||
debugf("[Append Entry] Step back")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
warnf("[Append Entry] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to recover from snapshot request
|
||||
func (r *raftServer) SnapshotHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.SnapshotRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
if err == nil {
|
||||
debugf("[recv] POST %s/snapshot/ ", r.url)
|
||||
if resp := r.RequestSnapshot(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
warnf("[Snapshot] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to recover from snapshot request
|
||||
func (r *raftServer) SnapshotRecoveryHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.SnapshotRecoveryRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
if err == nil {
|
||||
debugf("[recv] POST %s/snapshotRecovery/ ", r.url)
|
||||
if resp := r.SnapshotRecoveryRequest(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
warnf("[Snapshot] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Get the port that listening for etcd connecting of the server
|
||||
func (r *raftServer) EtcdURLHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
debugf("[recv] Get %s/etcdURL/ ", r.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(argInfo.EtcdURL))
|
||||
}
|
||||
|
||||
// Response to the join request
|
||||
func (r *raftServer) JoinHttpHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
|
||||
command := &JoinCommand{}
|
||||
|
||||
if err := decodeJsonRequest(req, command); err == nil {
|
||||
debugf("Receive Join Request from %s", command.Name)
|
||||
return r.dispatchRaftCommand(command, w, req)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Response to remove request
|
||||
func (r *raftServer) RemoveHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "DELETE" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
nodeName := req.URL.Path[len("/remove/"):]
|
||||
command := &RemoveCommand{
|
||||
Name: nodeName,
|
||||
}
|
||||
|
||||
debugf("[recv] Remove Request [%s]", command.Name)
|
||||
|
||||
r.dispatchRaftCommand(command, w, req)
|
||||
}
|
||||
|
||||
// Response to the name request
|
||||
func (r *raftServer) NameHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
debugf("[recv] Get %s/name/ ", r.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.name))
|
||||
}
|
||||
|
||||
// Response to the name request
|
||||
func (r *raftServer) RaftVersionHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
debugf("[recv] Get %s/version/ ", r.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.version))
|
||||
}
|
||||
|
||||
func (r *raftServer) dispatchRaftCommand(c Command, w http.ResponseWriter, req *http.Request) error {
|
||||
return r.dispatch(c, w, req, nameToRaftURL)
|
||||
}
|
329
raft_server.go
329
raft_server.go
@ -1,329 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
type raftServer struct {
|
||||
*raft.Server
|
||||
version string
|
||||
joinIndex uint64
|
||||
name string
|
||||
url string
|
||||
listenHost string
|
||||
tlsConf *TLSConfig
|
||||
tlsInfo *TLSInfo
|
||||
followersStats *raftFollowersStats
|
||||
serverStats *raftServerStats
|
||||
}
|
||||
|
||||
//var r *raftServer
|
||||
|
||||
func newRaftServer(name string, url string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo) *raftServer {
|
||||
|
||||
raftWrapper := &raftServer{
|
||||
version: raftVersion,
|
||||
name: name,
|
||||
url: url,
|
||||
listenHost: listenHost,
|
||||
tlsConf: tlsConf,
|
||||
tlsInfo: tlsInfo,
|
||||
followersStats: &raftFollowersStats{
|
||||
Leader: name,
|
||||
Followers: make(map[string]*raftFollowerStats),
|
||||
},
|
||||
serverStats: &raftServerStats{
|
||||
StartTime: time.Now(),
|
||||
sendRateQueue: &statsQueue{
|
||||
back: -1,
|
||||
},
|
||||
recvRateQueue: &statsQueue{
|
||||
back: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create transporter for raft
|
||||
raftTransporter := newTransporter(tlsConf.Scheme, tlsConf.Client, raftWrapper)
|
||||
|
||||
// Create raft server
|
||||
server, err := raft.NewServer(name, dirPath, raftTransporter, etcdStore, raftWrapper, "")
|
||||
check(err)
|
||||
|
||||
raftWrapper.Server = server
|
||||
|
||||
return raftWrapper
|
||||
}
|
||||
|
||||
// Start the raft server
|
||||
func (r *raftServer) ListenAndServe() {
|
||||
// Setup commands.
|
||||
registerCommands()
|
||||
|
||||
// LoadSnapshot
|
||||
if snapshot {
|
||||
err := r.LoadSnapshot()
|
||||
|
||||
if err == nil {
|
||||
debugf("%s finished load snapshot", r.name)
|
||||
} else {
|
||||
debug(err)
|
||||
}
|
||||
}
|
||||
|
||||
r.SetElectionTimeout(ElectionTimeout)
|
||||
r.SetHeartbeatTimeout(HeartbeatTimeout)
|
||||
|
||||
r.Start()
|
||||
|
||||
if r.IsLogEmpty() {
|
||||
|
||||
// start as a leader in a new cluster
|
||||
if len(cluster) == 0 {
|
||||
r.startAsLeader()
|
||||
|
||||
} else {
|
||||
r.startAsFollower()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// rejoin the previous cluster
|
||||
cluster = r.getMachines(nameToRaftURL)
|
||||
for i := 0; i < len(cluster); i++ {
|
||||
u, err := url.Parse(cluster[i])
|
||||
if err != nil {
|
||||
debug("rejoin cannot parse url: ", err)
|
||||
}
|
||||
cluster[i] = u.Host
|
||||
}
|
||||
ok := r.joinCluster(cluster)
|
||||
if !ok {
|
||||
warn("the entire cluster is down! this machine will restart the cluster.")
|
||||
}
|
||||
|
||||
debugf("%s restart as a follower", r.name)
|
||||
}
|
||||
|
||||
// open the snapshot
|
||||
if snapshot {
|
||||
go r.monitorSnapshot()
|
||||
}
|
||||
|
||||
// start to response to raft requests
|
||||
go r.startTransport(r.tlsConf.Scheme, r.tlsConf.Server)
|
||||
|
||||
}
|
||||
|
||||
func (r *raftServer) startAsLeader() {
|
||||
// leader need to join self as a peer
|
||||
for {
|
||||
_, err := r.Do(newJoinCommand(r.version, r.Name(), r.url, e.url))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
debugf("%s start as a leader", r.name)
|
||||
}
|
||||
|
||||
func (r *raftServer) startAsFollower() {
|
||||
// start as a follower in a existing cluster
|
||||
for i := 0; i < retryTimes; i++ {
|
||||
ok := r.joinCluster(cluster)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
warnf("cannot join to cluster via given machines, retry in %d seconds", RetryInterval)
|
||||
time.Sleep(time.Second * RetryInterval)
|
||||
}
|
||||
|
||||
fatalf("Cannot join the cluster via given machines after %x retries", retryTimes)
|
||||
}
|
||||
|
||||
// Start to listen and response raft command
|
||||
func (r *raftServer) startTransport(scheme string, tlsConf tls.Config) {
|
||||
infof("raft server [name %s, listen on %s, advertised url %s]", r.name, r.listenHost, r.url)
|
||||
|
||||
raftMux := http.NewServeMux()
|
||||
|
||||
server := &http.Server{
|
||||
Handler: raftMux,
|
||||
TLSConfig: &tlsConf,
|
||||
Addr: r.listenHost,
|
||||
}
|
||||
|
||||
// internal commands
|
||||
raftMux.HandleFunc("/name", r.NameHttpHandler)
|
||||
raftMux.HandleFunc("/version", r.RaftVersionHttpHandler)
|
||||
raftMux.Handle("/join", errorHandler(r.JoinHttpHandler))
|
||||
raftMux.HandleFunc("/remove/", r.RemoveHttpHandler)
|
||||
raftMux.HandleFunc("/vote", r.VoteHttpHandler)
|
||||
raftMux.HandleFunc("/log", r.GetLogHttpHandler)
|
||||
raftMux.HandleFunc("/log/append", r.AppendEntriesHttpHandler)
|
||||
raftMux.HandleFunc("/snapshot", r.SnapshotHttpHandler)
|
||||
raftMux.HandleFunc("/snapshotRecovery", r.SnapshotRecoveryHttpHandler)
|
||||
raftMux.HandleFunc("/etcdURL", r.EtcdURLHttpHandler)
|
||||
|
||||
if scheme == "http" {
|
||||
fatal(server.ListenAndServe())
|
||||
} else {
|
||||
fatal(server.ListenAndServeTLS(r.tlsInfo.CertFile, r.tlsInfo.KeyFile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// getVersion fetches the raft version of a peer. This works for now but we
|
||||
// will need to do something more sophisticated later when we allow mixed
|
||||
// version clusters.
|
||||
func getVersion(t *transporter, versionURL url.URL) (string, error) {
|
||||
resp, req, err := t.Get(versionURL.String())
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
t.CancelWhenTimeout(req)
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (r *raftServer) joinCluster(cluster []string) bool {
|
||||
for _, machine := range cluster {
|
||||
|
||||
if len(machine) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err := r.joinByMachine(r.Server, machine, r.tlsConf.Scheme)
|
||||
if err == nil {
|
||||
debugf("%s success join to the cluster via machine %s", r.name, machine)
|
||||
return true
|
||||
|
||||
} else {
|
||||
if _, ok := err.(etcdErr.Error); ok {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
debugf("cannot join to cluster via machine %s %s", machine, err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Send join requests to machine.
|
||||
func (r *raftServer) joinByMachine(s *raft.Server, machine string, scheme string) error {
|
||||
var b bytes.Buffer
|
||||
|
||||
// t must be ok
|
||||
t, _ := r.Transporter().(*transporter)
|
||||
|
||||
// Our version must match the leaders version
|
||||
versionURL := url.URL{Host: machine, Scheme: scheme, Path: "/version"}
|
||||
version, err := getVersion(t, versionURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to join: %v", err)
|
||||
}
|
||||
|
||||
// TODO: versioning of the internal protocol. See:
|
||||
// Documentation/internatl-protocol-versioning.md
|
||||
if version != r.version {
|
||||
return fmt.Errorf("Unable to join: internal version mismatch, entire cluster must be running identical versions of etcd")
|
||||
}
|
||||
|
||||
json.NewEncoder(&b).Encode(newJoinCommand(r.version, r.Name(), r.url, e.url))
|
||||
|
||||
joinURL := url.URL{Host: machine, Scheme: scheme, Path: "/join"}
|
||||
|
||||
debugf("Send Join Request to %s", joinURL.String())
|
||||
|
||||
resp, req, err := t.Post(joinURL.String(), &b)
|
||||
|
||||
for {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to join: %v", err)
|
||||
}
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
|
||||
t.CancelWhenTimeout(req)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
r.joinIndex, _ = binary.Uvarint(b)
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusTemporaryRedirect {
|
||||
|
||||
address := resp.Header.Get("Location")
|
||||
debugf("Send Join Request to %s", address)
|
||||
|
||||
json.NewEncoder(&b).Encode(newJoinCommand(r.version, r.Name(), r.url, e.url))
|
||||
|
||||
resp, req, err = t.Post(address, &b)
|
||||
|
||||
} else if resp.StatusCode == http.StatusBadRequest {
|
||||
debug("Reach max number machines in the cluster")
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err := &etcdErr.Error{}
|
||||
decoder.Decode(err)
|
||||
return *err
|
||||
} else {
|
||||
return fmt.Errorf("Unable to join")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("Unable to join: %v", err)
|
||||
}
|
||||
|
||||
func (r *raftServer) Stats() []byte {
|
||||
r.serverStats.LeaderInfo.Uptime = time.Now().Sub(r.serverStats.LeaderInfo.startTime).String()
|
||||
|
||||
queue := r.serverStats.sendRateQueue
|
||||
|
||||
r.serverStats.SendingPkgRate, r.serverStats.SendingBandwidthRate = queue.Rate()
|
||||
|
||||
queue = r.serverStats.recvRateQueue
|
||||
|
||||
r.serverStats.RecvingPkgRate, r.serverStats.RecvingBandwidthRate = queue.Rate()
|
||||
|
||||
b, _ := json.Marshal(r.serverStats)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (r *raftServer) PeerStats() []byte {
|
||||
if r.State() == raft.Leader {
|
||||
b, _ := json.Marshal(r.followersStats)
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register commands to raft server
|
||||
func registerCommands() {
|
||||
raft.RegisterCommand(&JoinCommand{})
|
||||
raft.RegisterCommand(&RemoveCommand{})
|
||||
raft.RegisterCommand(&GetCommand{})
|
||||
raft.RegisterCommand(&DeleteCommand{})
|
||||
raft.RegisterCommand(&WatchCommand{})
|
||||
raft.RegisterCommand(&TestAndSetCommand{})
|
||||
|
||||
raft.RegisterCommand(&CreateCommand{})
|
||||
raft.RegisterCommand(&UpdateCommand{})
|
||||
}
|
210
raft_stats.go
210
raft_stats.go
@ -1,210 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
const (
|
||||
queueCapacity = 200
|
||||
)
|
||||
|
||||
// packageStats represent the stats we need for a package.
|
||||
// It has sending time and the size of the package.
|
||||
type packageStats struct {
|
||||
sendingTime time.Time
|
||||
size int
|
||||
}
|
||||
|
||||
// NewPackageStats creates a pacakgeStats and return the pointer to it.
|
||||
func NewPackageStats(now time.Time, size int) *packageStats {
|
||||
return &packageStats{
|
||||
sendingTime: now,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Time return the sending time of the package.
|
||||
func (ps *packageStats) Time() time.Time {
|
||||
return ps.sendingTime
|
||||
}
|
||||
|
||||
type raftServerStats struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
|
||||
LeaderInfo struct {
|
||||
Name string `json:"leader"`
|
||||
Uptime string `json:"uptime"`
|
||||
startTime time.Time
|
||||
} `json:"leaderInfo"`
|
||||
|
||||
RecvAppendRequestCnt uint64 `json:"recvAppendRequestCnt,"`
|
||||
RecvingPkgRate float64 `json:"recvPkgRate,omitempty"`
|
||||
RecvingBandwidthRate float64 `json:"recvBandwidthRate,omitempty"`
|
||||
|
||||
SendAppendRequestCnt uint64 `json:"sendAppendRequestCnt"`
|
||||
SendingPkgRate float64 `json:"sendPkgRate,omitempty"`
|
||||
SendingBandwidthRate float64 `json:"sendBandwidthRate,omitempty"`
|
||||
|
||||
sendRateQueue *statsQueue
|
||||
recvRateQueue *statsQueue
|
||||
}
|
||||
|
||||
func (ss *raftServerStats) RecvAppendReq(leaderName string, pkgSize int) {
|
||||
ss.State = raft.Follower
|
||||
if leaderName != ss.LeaderInfo.Name {
|
||||
ss.LeaderInfo.Name = leaderName
|
||||
ss.LeaderInfo.startTime = time.Now()
|
||||
}
|
||||
|
||||
ss.recvRateQueue.Insert(NewPackageStats(time.Now(), pkgSize))
|
||||
ss.RecvAppendRequestCnt++
|
||||
}
|
||||
|
||||
func (ss *raftServerStats) SendAppendReq(pkgSize int) {
|
||||
now := time.Now()
|
||||
|
||||
if ss.State != raft.Leader {
|
||||
ss.State = raft.Leader
|
||||
ss.LeaderInfo.Name = ss.Name
|
||||
ss.LeaderInfo.startTime = now
|
||||
}
|
||||
|
||||
ss.sendRateQueue.Insert(NewPackageStats(now, pkgSize))
|
||||
|
||||
ss.SendAppendRequestCnt++
|
||||
}
|
||||
|
||||
type raftFollowersStats struct {
|
||||
Leader string `json:"leader"`
|
||||
Followers map[string]*raftFollowerStats `json:"followers"`
|
||||
}
|
||||
|
||||
type raftFollowerStats struct {
|
||||
Latency struct {
|
||||
Current float64 `json:"current"`
|
||||
Average float64 `json:"average"`
|
||||
averageSquare float64
|
||||
StandardDeviation float64 `json:"standardDeviation"`
|
||||
Minimum float64 `json:"minimum"`
|
||||
Maximum float64 `json:"maximum"`
|
||||
} `json:"latency"`
|
||||
|
||||
Counts struct {
|
||||
Fail uint64 `json:"fail"`
|
||||
Success uint64 `json:"success"`
|
||||
} `json:"counts"`
|
||||
}
|
||||
|
||||
// Succ function update the raftFollowerStats with a successful send
|
||||
func (ps *raftFollowerStats) Succ(d time.Duration) {
|
||||
total := float64(ps.Counts.Success) * ps.Latency.Average
|
||||
totalSquare := float64(ps.Counts.Success) * ps.Latency.averageSquare
|
||||
|
||||
ps.Counts.Success++
|
||||
|
||||
ps.Latency.Current = float64(d) / (1000000.0)
|
||||
|
||||
if ps.Latency.Current > ps.Latency.Maximum {
|
||||
ps.Latency.Maximum = ps.Latency.Current
|
||||
}
|
||||
|
||||
if ps.Latency.Current < ps.Latency.Minimum {
|
||||
ps.Latency.Minimum = ps.Latency.Current
|
||||
}
|
||||
|
||||
ps.Latency.Average = (total + ps.Latency.Current) / float64(ps.Counts.Success)
|
||||
ps.Latency.averageSquare = (totalSquare + ps.Latency.Current*ps.Latency.Current) / float64(ps.Counts.Success)
|
||||
|
||||
// sdv = sqrt(avg(x^2) - avg(x)^2)
|
||||
ps.Latency.StandardDeviation = math.Sqrt(ps.Latency.averageSquare - ps.Latency.Average*ps.Latency.Average)
|
||||
}
|
||||
|
||||
// Fail function update the raftFollowerStats with a unsuccessful send
|
||||
func (ps *raftFollowerStats) Fail() {
|
||||
ps.Counts.Fail++
|
||||
}
|
||||
|
||||
type statsQueue struct {
|
||||
items [queueCapacity]*packageStats
|
||||
size int
|
||||
front int
|
||||
back int
|
||||
totalPkgSize int
|
||||
rwl sync.RWMutex
|
||||
}
|
||||
|
||||
func (q *statsQueue) Len() int {
|
||||
return q.size
|
||||
}
|
||||
|
||||
func (q *statsQueue) PkgSize() int {
|
||||
return q.totalPkgSize
|
||||
}
|
||||
|
||||
// FrontAndBack gets the front and back elements in the queue
|
||||
// We must grab front and back together with the protection of the lock
|
||||
func (q *statsQueue) frontAndBack() (*packageStats, *packageStats) {
|
||||
q.rwl.RLock()
|
||||
defer q.rwl.RUnlock()
|
||||
if q.size != 0 {
|
||||
return q.items[q.front], q.items[q.back]
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Insert function insert a packageStats into the queue and update the records
|
||||
func (q *statsQueue) Insert(p *packageStats) {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
|
||||
q.back = (q.back + 1) % queueCapacity
|
||||
|
||||
if q.size == queueCapacity { //dequeue
|
||||
q.totalPkgSize -= q.items[q.front].size
|
||||
q.front = (q.back + 1) % queueCapacity
|
||||
} else {
|
||||
q.size++
|
||||
}
|
||||
|
||||
q.items[q.back] = p
|
||||
q.totalPkgSize += q.items[q.back].size
|
||||
|
||||
}
|
||||
|
||||
// Rate function returns the package rate and byte rate
|
||||
func (q *statsQueue) Rate() (float64, float64) {
|
||||
front, back := q.frontAndBack()
|
||||
|
||||
if front == nil || back == nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if time.Now().Sub(back.Time()) > time.Second {
|
||||
q.Clear()
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
sampleDuration := back.Time().Sub(front.Time())
|
||||
|
||||
pr := float64(q.Len()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
br := float64(q.PkgSize()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
return pr, br
|
||||
}
|
||||
|
||||
// Clear function clear up the statsQueue
|
||||
func (q *statsQueue) Clear() {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
q.back = -1
|
||||
q.front = 0
|
||||
q.size = 0
|
||||
q.totalPkgSize = 0
|
||||
}
|
@ -3,6 +3,6 @@
|
||||
VER=$(git describe --tags HEAD)
|
||||
|
||||
cat <<EOF
|
||||
package main
|
||||
package server
|
||||
const releaseVersion = "$VER"
|
||||
EOF
|
||||
|
75
server/join_command.go
Normal file
75
server/join_command.go
Normal file
@ -0,0 +1,75 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&JoinCommand{})
|
||||
}
|
||||
|
||||
// The JoinCommand adds a node to the cluster.
|
||||
type JoinCommand struct {
|
||||
RaftVersion string `json:"raftVersion"`
|
||||
Name string `json:"name"`
|
||||
RaftURL string `json:"raftURL"`
|
||||
EtcdURL string `json:"etcdURL"`
|
||||
}
|
||||
|
||||
func NewJoinCommand(version, name, raftUrl, etcdUrl string) *JoinCommand {
|
||||
return &JoinCommand{
|
||||
RaftVersion: version,
|
||||
Name: name,
|
||||
RaftURL: raftUrl,
|
||||
EtcdURL: etcdUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// The name of the join command in the log
|
||||
func (c *JoinCommand) CommandName() string {
|
||||
return "etcd:join"
|
||||
}
|
||||
|
||||
// Join a server to the cluster
|
||||
func (c *JoinCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
ps, _ := server.Context().(*PeerServer)
|
||||
|
||||
b := make([]byte, 8)
|
||||
binary.PutUvarint(b, server.CommitIndex())
|
||||
|
||||
// Make sure we're not getting a cached value from the registry.
|
||||
ps.registry.Invalidate(c.Name)
|
||||
|
||||
// Check if the join command is from a previous machine, who lost all its previous log.
|
||||
if _, ok := ps.registry.URL(c.Name); ok {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Check machine number in the cluster
|
||||
if ps.registry.Count() == ps.MaxClusterSize {
|
||||
log.Debug("Reject join request from ", c.Name)
|
||||
return []byte{0}, etcdErr.NewError(etcdErr.EcodeNoMoreMachine, "", server.CommitIndex(), server.Term())
|
||||
}
|
||||
|
||||
// Add to shared machine registry.
|
||||
ps.registry.Register(c.Name, c.RaftVersion, c.RaftURL, c.EtcdURL, server.CommitIndex(), server.Term())
|
||||
|
||||
// Add peer in raft
|
||||
err := server.AddPeer(c.Name, "")
|
||||
|
||||
// Add peer stats
|
||||
if c.Name != ps.Name() {
|
||||
ps.followersStats.Followers[c.Name] = &raftFollowerStats{}
|
||||
ps.followersStats.Followers[c.Name].Latency.Minimum = 1 << 63
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (c *JoinCommand) NodeName() string {
|
||||
return c.Name
|
||||
}
|
25
server/package_stats.go
Normal file
25
server/package_stats.go
Normal file
@ -0,0 +1,25 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// packageStats represent the stats we need for a package.
|
||||
// It has sending time and the size of the package.
|
||||
type packageStats struct {
|
||||
sendingTime time.Time
|
||||
size int
|
||||
}
|
||||
|
||||
// NewPackageStats creates a pacakgeStats and return the pointer to it.
|
||||
func NewPackageStats(now time.Time, size int) *packageStats {
|
||||
return &packageStats{
|
||||
sendingTime: now,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Time return the sending time of the package.
|
||||
func (ps *packageStats) Time() time.Time {
|
||||
return ps.sendingTime
|
||||
}
|
543
server/peer_server.go
Normal file
543
server/peer_server.go
Normal file
@ -0,0 +1,543 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
type PeerServer struct {
|
||||
*raft.Server
|
||||
server *Server
|
||||
joinIndex uint64
|
||||
name string
|
||||
url string
|
||||
listenHost string
|
||||
tlsConf *TLSConfig
|
||||
tlsInfo *TLSInfo
|
||||
followersStats *raftFollowersStats
|
||||
serverStats *raftServerStats
|
||||
registry *Registry
|
||||
store *store.Store
|
||||
snapConf *snapshotConf
|
||||
MaxClusterSize int
|
||||
RetryTimes int
|
||||
}
|
||||
|
||||
// TODO: find a good policy to do snapshot
|
||||
type snapshotConf struct {
|
||||
// Etcd will check if snapshot is need every checkingInterval
|
||||
checkingInterval time.Duration
|
||||
|
||||
// The number of writes when the last snapshot happened
|
||||
lastWrites uint64
|
||||
|
||||
// If the incremental number of writes since the last snapshot
|
||||
// exceeds the write Threshold, etcd will do a snapshot
|
||||
writesThr uint64
|
||||
}
|
||||
|
||||
func NewPeerServer(name string, path string, url string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, registry *Registry, store *store.Store) *PeerServer {
|
||||
s := &PeerServer{
|
||||
name: name,
|
||||
url: url,
|
||||
listenHost: listenHost,
|
||||
tlsConf: tlsConf,
|
||||
tlsInfo: tlsInfo,
|
||||
registry: registry,
|
||||
store: store,
|
||||
snapConf: &snapshotConf{time.Second * 3, 0, 20 * 1000},
|
||||
followersStats: &raftFollowersStats{
|
||||
Leader: name,
|
||||
Followers: make(map[string]*raftFollowerStats),
|
||||
},
|
||||
serverStats: &raftServerStats{
|
||||
StartTime: time.Now(),
|
||||
sendRateQueue: &statsQueue{
|
||||
back: -1,
|
||||
},
|
||||
recvRateQueue: &statsQueue{
|
||||
back: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create transporter for raft
|
||||
raftTransporter := newTransporter(tlsConf.Scheme, tlsConf.Client, s)
|
||||
|
||||
// Create raft server
|
||||
server, err := raft.NewServer(name, path, raftTransporter, s.store, s, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s.Server = server
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start the raft server
|
||||
func (s *PeerServer) ListenAndServe(snapshot bool, cluster []string) {
|
||||
// LoadSnapshot
|
||||
if snapshot {
|
||||
err := s.LoadSnapshot()
|
||||
|
||||
if err == nil {
|
||||
log.Debugf("%s finished load snapshot", s.name)
|
||||
} else {
|
||||
log.Debug(err)
|
||||
}
|
||||
}
|
||||
|
||||
s.SetElectionTimeout(ElectionTimeout)
|
||||
s.SetHeartbeatTimeout(HeartbeatTimeout)
|
||||
|
||||
s.Start()
|
||||
|
||||
if s.IsLogEmpty() {
|
||||
// start as a leader in a new cluster
|
||||
if len(cluster) == 0 {
|
||||
s.startAsLeader()
|
||||
} else {
|
||||
s.startAsFollower(cluster)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Rejoin the previous cluster
|
||||
cluster = s.registry.PeerURLs(s.Leader(), s.name)
|
||||
for i := 0; i < len(cluster); i++ {
|
||||
u, err := url.Parse(cluster[i])
|
||||
if err != nil {
|
||||
log.Debug("rejoin cannot parse url: ", err)
|
||||
}
|
||||
cluster[i] = u.Host
|
||||
}
|
||||
ok := s.joinCluster(cluster)
|
||||
if !ok {
|
||||
log.Warn("the entire cluster is down! this machine will restart the cluster.")
|
||||
}
|
||||
|
||||
log.Debugf("%s restart as a follower", s.name)
|
||||
}
|
||||
|
||||
// open the snapshot
|
||||
if snapshot {
|
||||
go s.monitorSnapshot()
|
||||
}
|
||||
|
||||
// start to response to raft requests
|
||||
go s.startTransport(s.tlsConf.Scheme, s.tlsConf.Server)
|
||||
|
||||
}
|
||||
|
||||
// Retrieves the underlying Raft server.
|
||||
func (s *PeerServer) RaftServer() *raft.Server {
|
||||
return s.Server
|
||||
}
|
||||
|
||||
// Associates the client server with the peer server.
|
||||
func (s *PeerServer) SetServer(server *Server) {
|
||||
s.server = server
|
||||
}
|
||||
|
||||
// Get all the current logs
|
||||
func (s *PeerServer) GetLogHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("[recv] GET %s/log", s.url)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(s.LogEntries())
|
||||
}
|
||||
|
||||
// Response to vote request
|
||||
func (s *PeerServer) VoteHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
rvreq := &raft.RequestVoteRequest{}
|
||||
err := decodeJsonRequest(req, rvreq)
|
||||
if err == nil {
|
||||
log.Debugf("[recv] POST %s/vote [%s]", s.url, rvreq.CandidateName)
|
||||
if resp := s.RequestVote(rvreq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Warnf("[vote] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to append entries request
|
||||
func (s *PeerServer) AppendEntriesHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.AppendEntriesRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
|
||||
if err == nil {
|
||||
log.Debugf("[recv] POST %s/log/append [%d]", s.url, len(aereq.Entries))
|
||||
|
||||
s.serverStats.RecvAppendReq(aereq.LeaderName, int(req.ContentLength))
|
||||
|
||||
if resp := s.AppendEntries(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
if !resp.Success {
|
||||
log.Debugf("[Append Entry] Step back")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Warnf("[Append Entry] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to recover from snapshot request
|
||||
func (s *PeerServer) SnapshotHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.SnapshotRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
if err == nil {
|
||||
log.Debugf("[recv] POST %s/snapshot/ ", s.url)
|
||||
if resp := s.RequestSnapshot(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Warnf("[Snapshot] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Response to recover from snapshot request
|
||||
func (s *PeerServer) SnapshotRecoveryHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
aereq := &raft.SnapshotRecoveryRequest{}
|
||||
err := decodeJsonRequest(req, aereq)
|
||||
if err == nil {
|
||||
log.Debugf("[recv] POST %s/snapshotRecovery/ ", s.url)
|
||||
if resp := s.SnapshotRecoveryRequest(aereq); resp != nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Warnf("[Snapshot] ERROR: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Get the port that listening for etcd connecting of the server
|
||||
func (s *PeerServer) EtcdURLHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("[recv] Get %s/etcdURL/ ", s.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(s.server.URL()))
|
||||
}
|
||||
|
||||
// Response to the join request
|
||||
func (s *PeerServer) JoinHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
command := &JoinCommand{}
|
||||
|
||||
// Write CORS header.
|
||||
if s.server.OriginAllowed("*") {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
} else if s.server.OriginAllowed(req.Header.Get("Origin")) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", req.Header.Get("Origin"))
|
||||
}
|
||||
|
||||
err := decodeJsonRequest(req, command)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Receive Join Request from %s", command.Name)
|
||||
err = s.dispatchRaftCommand(command, w, req)
|
||||
|
||||
// Return status.
|
||||
if err != nil {
|
||||
if etcdErr, ok := err.(*etcdErr.Error); ok {
|
||||
log.Debug("Return error: ", (*etcdErr).Error())
|
||||
etcdErr.Write(w)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Response to remove request
|
||||
func (s *PeerServer) RemoveHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "DELETE" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
nodeName := req.URL.Path[len("/remove/"):]
|
||||
command := &RemoveCommand{
|
||||
Name: nodeName,
|
||||
}
|
||||
|
||||
log.Debugf("[recv] Remove Request [%s]", command.Name)
|
||||
|
||||
s.dispatchRaftCommand(command, w, req)
|
||||
}
|
||||
|
||||
// Response to the name request
|
||||
func (s *PeerServer) NameHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("[recv] Get %s/name/ ", s.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(s.name))
|
||||
}
|
||||
|
||||
// Response to the name request
|
||||
func (s *PeerServer) RaftVersionHttpHandler(w http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("[recv] Get %s/version/ ", s.url)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(PeerVersion))
|
||||
}
|
||||
|
||||
func (s *PeerServer) dispatchRaftCommand(c raft.Command, w http.ResponseWriter, req *http.Request) error {
|
||||
return s.dispatch(c, w, req)
|
||||
}
|
||||
|
||||
func (s *PeerServer) startAsLeader() {
|
||||
// leader need to join self as a peer
|
||||
for {
|
||||
_, err := s.Do(NewJoinCommand(PeerVersion, s.Name(), s.url, s.server.URL()))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Debugf("%s start as a leader", s.name)
|
||||
}
|
||||
|
||||
func (s *PeerServer) startAsFollower(cluster []string) {
|
||||
// start as a follower in a existing cluster
|
||||
for i := 0; i < s.RetryTimes; i++ {
|
||||
ok := s.joinCluster(cluster)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
log.Warnf("cannot join to cluster via given machines, retry in %d seconds", RetryInterval)
|
||||
time.Sleep(time.Second * RetryInterval)
|
||||
}
|
||||
|
||||
log.Fatalf("Cannot join the cluster via given machines after %x retries", s.RetryTimes)
|
||||
}
|
||||
|
||||
// Start to listen and response raft command
|
||||
func (s *PeerServer) startTransport(scheme string, tlsConf tls.Config) {
|
||||
log.Infof("raft server [name %s, listen on %s, advertised url %s]", s.name, s.listenHost, s.url)
|
||||
|
||||
raftMux := http.NewServeMux()
|
||||
|
||||
server := &http.Server{
|
||||
Handler: raftMux,
|
||||
TLSConfig: &tlsConf,
|
||||
Addr: s.listenHost,
|
||||
}
|
||||
|
||||
// internal commands
|
||||
raftMux.HandleFunc("/name", s.NameHttpHandler)
|
||||
raftMux.HandleFunc("/version", s.RaftVersionHttpHandler)
|
||||
raftMux.HandleFunc("/join", s.JoinHttpHandler)
|
||||
raftMux.HandleFunc("/remove/", s.RemoveHttpHandler)
|
||||
raftMux.HandleFunc("/vote", s.VoteHttpHandler)
|
||||
raftMux.HandleFunc("/log", s.GetLogHttpHandler)
|
||||
raftMux.HandleFunc("/log/append", s.AppendEntriesHttpHandler)
|
||||
raftMux.HandleFunc("/snapshot", s.SnapshotHttpHandler)
|
||||
raftMux.HandleFunc("/snapshotRecovery", s.SnapshotRecoveryHttpHandler)
|
||||
raftMux.HandleFunc("/etcdURL", s.EtcdURLHttpHandler)
|
||||
|
||||
if scheme == "http" {
|
||||
log.Fatal(server.ListenAndServe())
|
||||
} else {
|
||||
log.Fatal(server.ListenAndServeTLS(s.tlsInfo.CertFile, s.tlsInfo.KeyFile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// getVersion fetches the raft version of a peer. This works for now but we
|
||||
// will need to do something more sophisticated later when we allow mixed
|
||||
// version clusters.
|
||||
func getVersion(t *transporter, versionURL url.URL) (string, error) {
|
||||
resp, req, err := t.Get(versionURL.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
t.CancelWhenTimeout(req)
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (s *PeerServer) joinCluster(cluster []string) bool {
|
||||
for _, machine := range cluster {
|
||||
if len(machine) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err := s.joinByMachine(s.Server, machine, s.tlsConf.Scheme)
|
||||
if err == nil {
|
||||
log.Debugf("%s success join to the cluster via machine %s", s.name, machine)
|
||||
return true
|
||||
|
||||
} else {
|
||||
if _, ok := err.(etcdErr.Error); ok {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("cannot join to cluster via machine %s %s", machine, err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Send join requests to machine.
|
||||
func (s *PeerServer) joinByMachine(server *raft.Server, machine string, scheme string) error {
|
||||
var b bytes.Buffer
|
||||
|
||||
// t must be ok
|
||||
t, _ := server.Transporter().(*transporter)
|
||||
|
||||
// Our version must match the leaders version
|
||||
versionURL := url.URL{Host: machine, Scheme: scheme, Path: "/version"}
|
||||
version, err := getVersion(t, versionURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error during join version check: %v", err)
|
||||
}
|
||||
|
||||
// TODO: versioning of the internal protocol. See:
|
||||
// Documentation/internatl-protocol-versioning.md
|
||||
if version != PeerVersion {
|
||||
return fmt.Errorf("Unable to join: internal version mismatch, entire cluster must be running identical versions of etcd")
|
||||
}
|
||||
|
||||
json.NewEncoder(&b).Encode(NewJoinCommand(PeerVersion, server.Name(), s.url, s.server.URL()))
|
||||
|
||||
joinURL := url.URL{Host: machine, Scheme: scheme, Path: "/join"}
|
||||
|
||||
log.Debugf("Send Join Request to %s", joinURL.String())
|
||||
|
||||
resp, req, err := t.Post(joinURL.String(), &b)
|
||||
|
||||
for {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to join: %v", err)
|
||||
}
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
|
||||
t.CancelWhenTimeout(req)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
s.joinIndex, _ = binary.Uvarint(b)
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusTemporaryRedirect {
|
||||
address := resp.Header.Get("Location")
|
||||
log.Debugf("Send Join Request to %s", address)
|
||||
json.NewEncoder(&b).Encode(NewJoinCommand(PeerVersion, server.Name(), s.url, s.server.URL()))
|
||||
resp, req, err = t.Post(address, &b)
|
||||
|
||||
} else if resp.StatusCode == http.StatusBadRequest {
|
||||
log.Debug("Reach max number machines in the cluster")
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err := &etcdErr.Error{}
|
||||
decoder.Decode(err)
|
||||
return *err
|
||||
} else {
|
||||
return fmt.Errorf("Unable to join")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PeerServer) Stats() []byte {
|
||||
s.serverStats.LeaderInfo.Uptime = time.Now().Sub(s.serverStats.LeaderInfo.startTime).String()
|
||||
|
||||
queue := s.serverStats.sendRateQueue
|
||||
|
||||
s.serverStats.SendingPkgRate, s.serverStats.SendingBandwidthRate = queue.Rate()
|
||||
|
||||
queue = s.serverStats.recvRateQueue
|
||||
|
||||
s.serverStats.RecvingPkgRate, s.serverStats.RecvingBandwidthRate = queue.Rate()
|
||||
|
||||
b, _ := json.Marshal(s.serverStats)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *PeerServer) PeerStats() []byte {
|
||||
if s.State() == raft.Leader {
|
||||
b, _ := json.Marshal(s.followersStats)
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PeerServer) monitorSnapshot() {
|
||||
for {
|
||||
time.Sleep(s.snapConf.checkingInterval)
|
||||
currentWrites := 0
|
||||
if uint64(currentWrites) > s.snapConf.writesThr {
|
||||
s.TakeSnapshot()
|
||||
s.snapConf.lastWrites = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PeerServer) dispatch(c raft.Command, w http.ResponseWriter, req *http.Request) error {
|
||||
if s.State() == raft.Leader {
|
||||
if response, err := s.Do(c); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if response == nil {
|
||||
return etcdErr.NewError(300, "Empty response from raft", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
event, ok := response.(*store.Event)
|
||||
if ok {
|
||||
bytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(event.Index))
|
||||
w.Header().Add("X-Etcd-Term", fmt.Sprint(event.Term))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bytes, _ := response.([]byte)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
leader := s.Leader()
|
||||
// current no leader
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
url, _ := s.registry.PeerURL(leader)
|
||||
|
||||
log.Debugf("Not leader; Current leader: %s; redirect: %s", leader, url)
|
||||
redirect(url, w, req)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
56
server/raft_follower_stats.go
Normal file
56
server/raft_follower_stats.go
Normal file
@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type raftFollowersStats struct {
|
||||
Leader string `json:"leader"`
|
||||
Followers map[string]*raftFollowerStats `json:"followers"`
|
||||
}
|
||||
|
||||
type raftFollowerStats struct {
|
||||
Latency struct {
|
||||
Current float64 `json:"current"`
|
||||
Average float64 `json:"average"`
|
||||
averageSquare float64
|
||||
StandardDeviation float64 `json:"standardDeviation"`
|
||||
Minimum float64 `json:"minimum"`
|
||||
Maximum float64 `json:"maximum"`
|
||||
} `json:"latency"`
|
||||
|
||||
Counts struct {
|
||||
Fail uint64 `json:"fail"`
|
||||
Success uint64 `json:"success"`
|
||||
} `json:"counts"`
|
||||
}
|
||||
|
||||
// Succ function update the raftFollowerStats with a successful send
|
||||
func (ps *raftFollowerStats) Succ(d time.Duration) {
|
||||
total := float64(ps.Counts.Success) * ps.Latency.Average
|
||||
totalSquare := float64(ps.Counts.Success) * ps.Latency.averageSquare
|
||||
|
||||
ps.Counts.Success++
|
||||
|
||||
ps.Latency.Current = float64(d) / (1000000.0)
|
||||
|
||||
if ps.Latency.Current > ps.Latency.Maximum {
|
||||
ps.Latency.Maximum = ps.Latency.Current
|
||||
}
|
||||
|
||||
if ps.Latency.Current < ps.Latency.Minimum {
|
||||
ps.Latency.Minimum = ps.Latency.Current
|
||||
}
|
||||
|
||||
ps.Latency.Average = (total + ps.Latency.Current) / float64(ps.Counts.Success)
|
||||
ps.Latency.averageSquare = (totalSquare + ps.Latency.Current*ps.Latency.Current) / float64(ps.Counts.Success)
|
||||
|
||||
// sdv = sqrt(avg(x^2) - avg(x)^2)
|
||||
ps.Latency.StandardDeviation = math.Sqrt(ps.Latency.averageSquare - ps.Latency.Average*ps.Latency.Average)
|
||||
}
|
||||
|
||||
// Fail function update the raftFollowerStats with a unsuccessful send
|
||||
func (ps *raftFollowerStats) Fail() {
|
||||
ps.Counts.Fail++
|
||||
}
|
55
server/raft_server_stats.go
Normal file
55
server/raft_server_stats.go
Normal file
@ -0,0 +1,55 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
type raftServerStats struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
|
||||
LeaderInfo struct {
|
||||
Name string `json:"leader"`
|
||||
Uptime string `json:"uptime"`
|
||||
startTime time.Time
|
||||
} `json:"leaderInfo"`
|
||||
|
||||
RecvAppendRequestCnt uint64 `json:"recvAppendRequestCnt,"`
|
||||
RecvingPkgRate float64 `json:"recvPkgRate,omitempty"`
|
||||
RecvingBandwidthRate float64 `json:"recvBandwidthRate,omitempty"`
|
||||
|
||||
SendAppendRequestCnt uint64 `json:"sendAppendRequestCnt"`
|
||||
SendingPkgRate float64 `json:"sendPkgRate,omitempty"`
|
||||
SendingBandwidthRate float64 `json:"sendBandwidthRate,omitempty"`
|
||||
|
||||
sendRateQueue *statsQueue
|
||||
recvRateQueue *statsQueue
|
||||
}
|
||||
|
||||
func (ss *raftServerStats) RecvAppendReq(leaderName string, pkgSize int) {
|
||||
ss.State = raft.Follower
|
||||
if leaderName != ss.LeaderInfo.Name {
|
||||
ss.LeaderInfo.Name = leaderName
|
||||
ss.LeaderInfo.startTime = time.Now()
|
||||
}
|
||||
|
||||
ss.recvRateQueue.Insert(NewPackageStats(time.Now(), pkgSize))
|
||||
ss.RecvAppendRequestCnt++
|
||||
}
|
||||
|
||||
func (ss *raftServerStats) SendAppendReq(pkgSize int) {
|
||||
now := time.Now()
|
||||
|
||||
if ss.State != raft.Leader {
|
||||
ss.State = raft.Leader
|
||||
ss.LeaderInfo.Name = ss.Name
|
||||
ss.LeaderInfo.startTime = now
|
||||
}
|
||||
|
||||
ss.sendRateQueue.Insert(NewPackageStats(now, pkgSize))
|
||||
|
||||
ss.SendAppendRequestCnt++
|
||||
}
|
197
server/registry.go
Normal file
197
server/registry.go
Normal file
@ -0,0 +1,197 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/store"
|
||||
)
|
||||
|
||||
// The location of the machine URL data.
|
||||
const RegistryKey = "/_etcd/machines"
|
||||
|
||||
// The Registry stores URL information for nodes.
|
||||
type Registry struct {
|
||||
sync.Mutex
|
||||
store *store.Store
|
||||
nodes map[string]*node
|
||||
}
|
||||
|
||||
// The internal storage format of the registry.
|
||||
type node struct {
|
||||
peerVersion string
|
||||
peerURL string
|
||||
url string
|
||||
}
|
||||
|
||||
// Creates a new Registry.
|
||||
func NewRegistry(s *store.Store) *Registry {
|
||||
return &Registry{
|
||||
store: s,
|
||||
nodes: make(map[string]*node),
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a node to the registry.
|
||||
func (r *Registry) Register(name string, peerVersion string, peerURL string, url string, commitIndex uint64, term uint64) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Write data to store.
|
||||
key := path.Join(RegistryKey, name)
|
||||
value := fmt.Sprintf("raft=%s&etcd=%s&raftVersion=%s", peerURL, url, peerVersion)
|
||||
_, err := r.store.Create(key, value, false, false, store.Permanent, commitIndex, term)
|
||||
log.Debugf("Register: %s (%v)", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Removes a node from the registry.
|
||||
func (r *Registry) Unregister(name string, commitIndex uint64, term uint64) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Remove from cache.
|
||||
// delete(r.nodes, name)
|
||||
|
||||
// Remove the key from the store.
|
||||
_, err := r.store.Delete(path.Join(RegistryKey, name), false, commitIndex, term)
|
||||
log.Debugf("Unregister: %s (%v)", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns the number of nodes in the cluster.
|
||||
func (r *Registry) Count() int {
|
||||
e, err := r.store.Get(RegistryKey, false, false, 0, 0)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(e.KVPairs)
|
||||
}
|
||||
|
||||
// Retrieves the URL for a given node by name.
|
||||
func (r *Registry) URL(name string) (string, bool) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.url(name)
|
||||
}
|
||||
|
||||
func (r *Registry) url(name string) (string, bool) {
|
||||
if r.nodes[name] == nil {
|
||||
r.load(name)
|
||||
}
|
||||
|
||||
if node := r.nodes[name]; node != nil {
|
||||
return node.url, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Retrieves the URLs for all nodes.
|
||||
func (r *Registry) URLs(leaderName, selfName string) []string {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Build list including the leader and self.
|
||||
urls := make([]string, 0)
|
||||
if url, _ := r.url(leaderName); len(url) > 0 {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
|
||||
// Retrieve a list of all nodes.
|
||||
if e, _ := r.store.Get(RegistryKey, false, false, 0, 0); e != nil {
|
||||
// Lookup the URL for each one.
|
||||
for _, pair := range e.KVPairs {
|
||||
_, name := filepath.Split(pair.Key)
|
||||
if url, _ := r.url(name); len(url) > 0 && name != leaderName {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("URLs: %s / %s (%s)", leaderName, selfName, strings.Join(urls, ","))
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
// Retrieves the peer URL for a given node by name.
|
||||
func (r *Registry) PeerURL(name string) (string, bool) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.peerURL(name)
|
||||
}
|
||||
|
||||
func (r *Registry) peerURL(name string) (string, bool) {
|
||||
if r.nodes[name] == nil {
|
||||
r.load(name)
|
||||
}
|
||||
|
||||
if node := r.nodes[name]; node != nil {
|
||||
return node.peerURL, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Retrieves the peer URLs for all nodes.
|
||||
func (r *Registry) PeerURLs(leaderName, selfName string) []string {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Build list including the leader and self.
|
||||
urls := make([]string, 0)
|
||||
if url, _ := r.peerURL(leaderName); len(url) > 0 {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
|
||||
// Retrieve a list of all nodes.
|
||||
if e, _ := r.store.Get(RegistryKey, false, false, 0, 0); e != nil {
|
||||
// Lookup the URL for each one.
|
||||
for _, pair := range e.KVPairs {
|
||||
_, name := filepath.Split(pair.Key)
|
||||
if url, _ := r.peerURL(name); len(url) > 0 && name != leaderName {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("PeerURLs: %s / %s (%s)", leaderName, selfName, strings.Join(urls, ","))
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
// Removes a node from the cache.
|
||||
func (r *Registry) Invalidate(name string) {
|
||||
delete(r.nodes, name)
|
||||
}
|
||||
|
||||
// Loads the given node by name from the store into the cache.
|
||||
func (r *Registry) load(name string) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve from store.
|
||||
e, err := r.store.Get(path.Join(RegistryKey, name), false, false, 0, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse as a query string.
|
||||
m, err := url.ParseQuery(e.Value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to parse machines entry: %s", name))
|
||||
}
|
||||
|
||||
// Create node.
|
||||
r.nodes[name] = &node{
|
||||
url: m["etcd"][0],
|
||||
peerURL: m["raft"][0],
|
||||
peerVersion: m["raftVersion"][0],
|
||||
}
|
||||
}
|
67
server/remove_command.go
Normal file
67
server/remove_command.go
Normal file
@ -0,0 +1,67 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&RemoveCommand{})
|
||||
}
|
||||
|
||||
// The RemoveCommand removes a server from the cluster.
|
||||
type RemoveCommand struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// The name of the remove command in the log
|
||||
func (c *RemoveCommand) CommandName() string {
|
||||
return "etcd:remove"
|
||||
}
|
||||
|
||||
// Remove a server from the cluster
|
||||
func (c *RemoveCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
ps, _ := server.Context().(*PeerServer)
|
||||
|
||||
// Remove node from the shared registry.
|
||||
err := ps.registry.Unregister(c.Name, server.CommitIndex(), server.Term())
|
||||
|
||||
// Delete from stats
|
||||
delete(ps.followersStats.Followers, c.Name)
|
||||
|
||||
if err != nil {
|
||||
log.Debugf("Error while unregistering: %s (%v)", c.Name, err)
|
||||
return []byte{0}, err
|
||||
}
|
||||
|
||||
// Remove peer in raft
|
||||
err = server.RemovePeer(c.Name)
|
||||
if err != nil {
|
||||
log.Debugf("Unable to remove peer: %s (%v)", c.Name, err)
|
||||
return []byte{0}, err
|
||||
}
|
||||
|
||||
if c.Name == server.Name() {
|
||||
// the removed node is this node
|
||||
|
||||
// if the node is not replaying the previous logs
|
||||
// and the node has sent out a join request in this
|
||||
// start. It is sure that this node received a new remove
|
||||
// command and need to be removed
|
||||
if server.CommitIndex() > ps.joinIndex && ps.joinIndex != 0 {
|
||||
log.Debugf("server [%s] is removed", server.Name())
|
||||
os.Exit(0)
|
||||
} else {
|
||||
// else ignore remove
|
||||
log.Debugf("ignore previous remove command.")
|
||||
}
|
||||
}
|
||||
|
||||
b := make([]byte, 8)
|
||||
binary.PutUvarint(b, server.CommitIndex())
|
||||
|
||||
return b, err
|
||||
}
|
320
server/server.go
Normal file
320
server/server.go
Normal file
@ -0,0 +1,320 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/server/v1"
|
||||
"github.com/coreos/etcd/server/v2"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// This is the default implementation of the Server interface.
|
||||
type Server struct {
|
||||
http.Server
|
||||
peerServer *PeerServer
|
||||
registry *Registry
|
||||
store *store.Store
|
||||
name string
|
||||
url string
|
||||
tlsConf *TLSConfig
|
||||
tlsInfo *TLSInfo
|
||||
corsOrigins map[string]bool
|
||||
}
|
||||
|
||||
// Creates a new Server.
|
||||
func New(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, peerServer *PeerServer, registry *Registry, store *store.Store) *Server {
|
||||
s := &Server{
|
||||
Server: http.Server{
|
||||
Handler: mux.NewRouter(),
|
||||
TLSConfig: &tlsConf.Server,
|
||||
Addr: listenHost,
|
||||
},
|
||||
name: name,
|
||||
store: store,
|
||||
registry: registry,
|
||||
url: urlStr,
|
||||
tlsConf: tlsConf,
|
||||
tlsInfo: tlsInfo,
|
||||
peerServer: peerServer,
|
||||
}
|
||||
|
||||
// Install the routes.
|
||||
s.handleFunc("/version", s.GetVersionHandler).Methods("GET")
|
||||
s.installV1()
|
||||
s.installV2()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// The current state of the server in the cluster.
|
||||
func (s *Server) State() string {
|
||||
return s.peerServer.State()
|
||||
}
|
||||
|
||||
// The node name of the leader in the cluster.
|
||||
func (s *Server) Leader() string {
|
||||
return s.peerServer.Leader()
|
||||
}
|
||||
|
||||
// The current Raft committed index.
|
||||
func (s *Server) CommitIndex() uint64 {
|
||||
return s.peerServer.CommitIndex()
|
||||
}
|
||||
|
||||
// The current Raft term.
|
||||
func (s *Server) Term() uint64 {
|
||||
return s.peerServer.Term()
|
||||
}
|
||||
|
||||
// The server URL.
|
||||
func (s *Server) URL() string {
|
||||
return s.url
|
||||
}
|
||||
|
||||
// Retrives the Peer URL for a given node name.
|
||||
func (s *Server) PeerURL(name string) (string, bool) {
|
||||
return s.registry.PeerURL(name)
|
||||
}
|
||||
|
||||
// Returns a reference to the Store.
|
||||
func (s *Server) Store() *store.Store {
|
||||
return s.store
|
||||
}
|
||||
|
||||
func (s *Server) installV1() {
|
||||
s.handleFuncV1("/v1/keys/{key:.*}", v1.GetKeyHandler).Methods("GET")
|
||||
s.handleFuncV1("/v1/keys/{key:.*}", v1.SetKeyHandler).Methods("POST", "PUT")
|
||||
s.handleFuncV1("/v1/keys/{key:.*}", v1.DeleteKeyHandler).Methods("DELETE")
|
||||
s.handleFuncV1("/v1/watch/{key:.*}", v1.WatchKeyHandler).Methods("GET", "POST")
|
||||
s.handleFunc("/v1/leader", s.GetLeaderHandler).Methods("GET")
|
||||
s.handleFunc("/v1/machines", s.GetMachinesHandler).Methods("GET")
|
||||
s.handleFunc("/v1/stats/self", s.GetStatsHandler).Methods("GET")
|
||||
s.handleFunc("/v1/stats/leader", s.GetLeaderStatsHandler).Methods("GET")
|
||||
s.handleFunc("/v1/stats/store", s.GetStoreStatsHandler).Methods("GET")
|
||||
}
|
||||
|
||||
func (s *Server) installV2() {
|
||||
s.handleFuncV2("/v2/keys/{key:.*}", v2.GetKeyHandler).Methods("GET")
|
||||
s.handleFuncV2("/v2/keys/{key:.*}", v2.CreateKeyHandler).Methods("POST")
|
||||
s.handleFuncV2("/v2/keys/{key:.*}", v2.UpdateKeyHandler).Methods("PUT")
|
||||
s.handleFuncV2("/v2/keys/{key:.*}", v2.DeleteKeyHandler).Methods("DELETE")
|
||||
s.handleFunc("/v2/leader", s.GetLeaderHandler).Methods("GET")
|
||||
s.handleFunc("/v2/machines", s.GetMachinesHandler).Methods("GET")
|
||||
s.handleFunc("/v2/stats/self", s.GetStatsHandler).Methods("GET")
|
||||
s.handleFunc("/v2/stats/leader", s.GetLeaderStatsHandler).Methods("GET")
|
||||
s.handleFunc("/v2/stats/store", s.GetStoreStatsHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// Adds a v1 server handler to the router.
|
||||
func (s *Server) handleFuncV1(path string, f func(http.ResponseWriter, *http.Request, v1.Server) error) *mux.Route {
|
||||
return s.handleFunc(path, func(w http.ResponseWriter, req *http.Request) error {
|
||||
return f(w, req, s)
|
||||
})
|
||||
}
|
||||
|
||||
// Adds a v2 server handler to the router.
|
||||
func (s *Server) handleFuncV2(path string, f func(http.ResponseWriter, *http.Request, v2.Server) error) *mux.Route {
|
||||
return s.handleFunc(path, func(w http.ResponseWriter, req *http.Request) error {
|
||||
return f(w, req, s)
|
||||
})
|
||||
}
|
||||
|
||||
// Adds a server handler to the router.
|
||||
func (s *Server) handleFunc(path string, f func(http.ResponseWriter, *http.Request) error) *mux.Route {
|
||||
r := s.Handler.(*mux.Router)
|
||||
|
||||
// Wrap the standard HandleFunc interface to pass in the server reference.
|
||||
return r.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
|
||||
// Log request.
|
||||
log.Debugf("[recv] %s %s %s [%s]", req.Method, s.url, req.URL.Path, req.RemoteAddr)
|
||||
|
||||
// Write CORS header.
|
||||
if s.OriginAllowed("*") {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
} else if origin := req.Header.Get("Origin"); s.OriginAllowed(origin) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// Execute handler function and return error if necessary.
|
||||
if err := f(w, req); err != nil {
|
||||
if etcdErr, ok := err.(*etcdErr.Error); ok {
|
||||
log.Debug("Return error: ", (*etcdErr).Error())
|
||||
etcdErr.Write(w)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Start to listen and response etcd client command
|
||||
func (s *Server) ListenAndServe() {
|
||||
log.Infof("etcd server [name %s, listen on %s, advertised url %s]", s.name, s.Server.Addr, s.url)
|
||||
|
||||
if s.tlsConf.Scheme == "http" {
|
||||
log.Fatal(s.Server.ListenAndServe())
|
||||
} else {
|
||||
log.Fatal(s.Server.ListenAndServeTLS(s.tlsInfo.CertFile, s.tlsInfo.KeyFile))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Dispatch(c raft.Command, w http.ResponseWriter, req *http.Request) error {
|
||||
if s.peerServer.State() == raft.Leader {
|
||||
event, err := s.peerServer.Do(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event == nil {
|
||||
return etcdErr.NewError(300, "Empty result from raft", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
if b, ok := event.([]byte); ok {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if strings.HasPrefix(req.URL.Path, "/v1") {
|
||||
b, _ = json.Marshal(event.(*store.Event).Response())
|
||||
} else {
|
||||
b, _ = json.Marshal(event.(*store.Event))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
|
||||
return nil
|
||||
|
||||
} else {
|
||||
leader := s.peerServer.Leader()
|
||||
|
||||
// No leader available.
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
var url string
|
||||
switch c.(type) {
|
||||
case *JoinCommand, *RemoveCommand:
|
||||
url, _ = s.registry.PeerURL(leader)
|
||||
default:
|
||||
url, _ = s.registry.URL(leader)
|
||||
}
|
||||
redirect(url, w, req)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Sets a comma-delimited list of origins that are allowed.
|
||||
func (s *Server) AllowOrigins(origins string) error {
|
||||
// Construct a lookup of all origins.
|
||||
m := make(map[string]bool)
|
||||
for _, v := range strings.Split(origins, ",") {
|
||||
if v != "*" {
|
||||
if _, err := url.Parse(v); err != nil {
|
||||
return fmt.Errorf("Invalid CORS origin: %s", err)
|
||||
}
|
||||
}
|
||||
m[v] = true
|
||||
}
|
||||
s.corsOrigins = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determines whether the server will allow a given CORS origin.
|
||||
func (s *Server) OriginAllowed(origin string) bool {
|
||||
return s.corsOrigins["*"] || s.corsOrigins[origin]
|
||||
}
|
||||
|
||||
// Handler to return the current version of etcd.
|
||||
func (s *Server) GetVersionHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "etcd %s", releaseVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler to return the current leader's raft address
|
||||
func (s *Server) GetLeaderHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
leader := s.peerServer.Leader()
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(etcdErr.EcodeLeaderElect, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
url, _ := s.registry.PeerURL(leader)
|
||||
w.Write([]byte(url))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler to return all the known machines in the current cluster.
|
||||
func (s *Server) GetMachinesHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
machines := s.registry.URLs(s.peerServer.Leader(), s.name)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(strings.Join(machines, ", ")))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieves stats on the Raft server.
|
||||
func (s *Server) GetStatsHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
w.Write(s.peerServer.Stats())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieves stats on the leader.
|
||||
func (s *Server) GetLeaderStatsHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
if s.peerServer.State() == raft.Leader {
|
||||
w.Write(s.peerServer.PeerStats())
|
||||
return nil
|
||||
}
|
||||
|
||||
leader := s.peerServer.Leader()
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
hostname, _ := s.registry.URL(leader)
|
||||
redirect(hostname, w, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieves stats on the leader.
|
||||
func (s *Server) GetStoreStatsHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
w.Write(s.store.JsonStats())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Executes a speed test to evaluate the performance of update replication.
|
||||
func (s *Server) SpeedTestHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
count := 1000
|
||||
c := make(chan bool, count)
|
||||
for i := 0; i < count; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 10; j++ {
|
||||
c := &store.UpdateCommand{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
ExpireTime: time.Unix(0, 0),
|
||||
}
|
||||
s.peerServer.Do(c)
|
||||
}
|
||||
c <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
<-c
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("speed test success"))
|
||||
return nil
|
||||
}
|
89
server/stats_queue.go
Normal file
89
server/stats_queue.go
Normal file
@ -0,0 +1,89 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
queueCapacity = 200
|
||||
)
|
||||
|
||||
type statsQueue struct {
|
||||
items [queueCapacity]*packageStats
|
||||
size int
|
||||
front int
|
||||
back int
|
||||
totalPkgSize int
|
||||
rwl sync.RWMutex
|
||||
}
|
||||
|
||||
func (q *statsQueue) Len() int {
|
||||
return q.size
|
||||
}
|
||||
|
||||
func (q *statsQueue) PkgSize() int {
|
||||
return q.totalPkgSize
|
||||
}
|
||||
|
||||
// FrontAndBack gets the front and back elements in the queue
|
||||
// We must grab front and back together with the protection of the lock
|
||||
func (q *statsQueue) frontAndBack() (*packageStats, *packageStats) {
|
||||
q.rwl.RLock()
|
||||
defer q.rwl.RUnlock()
|
||||
if q.size != 0 {
|
||||
return q.items[q.front], q.items[q.back]
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Insert function insert a packageStats into the queue and update the records
|
||||
func (q *statsQueue) Insert(p *packageStats) {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
|
||||
q.back = (q.back + 1) % queueCapacity
|
||||
|
||||
if q.size == queueCapacity { //dequeue
|
||||
q.totalPkgSize -= q.items[q.front].size
|
||||
q.front = (q.back + 1) % queueCapacity
|
||||
} else {
|
||||
q.size++
|
||||
}
|
||||
|
||||
q.items[q.back] = p
|
||||
q.totalPkgSize += q.items[q.back].size
|
||||
|
||||
}
|
||||
|
||||
// Rate function returns the package rate and byte rate
|
||||
func (q *statsQueue) Rate() (float64, float64) {
|
||||
front, back := q.frontAndBack()
|
||||
|
||||
if front == nil || back == nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if time.Now().Sub(back.Time()) > time.Second {
|
||||
q.Clear()
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
sampleDuration := back.Time().Sub(front.Time())
|
||||
|
||||
pr := float64(q.Len()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
br := float64(q.PkgSize()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
return pr, br
|
||||
}
|
||||
|
||||
// Clear function clear up the statsQueue
|
||||
func (q *statsQueue) Clear() {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
q.back = -1
|
||||
q.front = 0
|
||||
q.size = 0
|
||||
q.totalPkgSize = 0
|
||||
}
|
15
server/timeout.go
Normal file
15
server/timeout.go
Normal file
@ -0,0 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// The amount of time to elapse without a heartbeat before becoming a candidate.
|
||||
ElectionTimeout = 200 * time.Millisecond
|
||||
|
||||
// The frequency by which heartbeats are sent to followers.
|
||||
HeartbeatTimeout = 50 * time.Millisecond
|
||||
|
||||
RetryInterval = 10
|
||||
)
|
11
server/tls_config.go
Normal file
11
server/tls_config.go
Normal file
@ -0,0 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
type TLSConfig struct {
|
||||
Scheme string
|
||||
Server tls.Config
|
||||
Client tls.Config
|
||||
}
|
7
server/tls_info.go
Normal file
7
server/tls_info.go
Normal file
@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
type TLSInfo struct {
|
||||
CertFile string `json:"CertFile"`
|
||||
KeyFile string `json:"KeyFile"`
|
||||
CAFile string `json:"CAFile"`
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
@ -29,13 +30,13 @@ var tranTimeout = ElectionTimeout
|
||||
type transporter struct {
|
||||
client *http.Client
|
||||
transport *http.Transport
|
||||
raftServer *raftServer
|
||||
peerServer *PeerServer
|
||||
}
|
||||
|
||||
// Create transporter using by raft server
|
||||
// Create http or https transporter based on
|
||||
// whether the user give the server cert and key
|
||||
func newTransporter(scheme string, tlsConf tls.Config, raftServer *raftServer) *transporter {
|
||||
func newTransporter(scheme string, tlsConf tls.Config, peerServer *PeerServer) *transporter {
|
||||
t := transporter{}
|
||||
|
||||
tr := &http.Transport{
|
||||
@ -50,7 +51,7 @@ func newTransporter(scheme string, tlsConf tls.Config, raftServer *raftServer) *
|
||||
|
||||
t.client = &http.Client{Transport: tr}
|
||||
t.transport = tr
|
||||
t.raftServer = raftServer
|
||||
t.peerServer = peerServer
|
||||
|
||||
return &t
|
||||
}
|
||||
@ -69,18 +70,18 @@ func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.P
|
||||
|
||||
size := b.Len()
|
||||
|
||||
t.raftServer.serverStats.SendAppendReq(size)
|
||||
t.peerServer.serverStats.SendAppendReq(size)
|
||||
|
||||
u, _ := nameToRaftURL(peer.Name)
|
||||
u, _ := t.peerServer.registry.PeerURL(peer.Name)
|
||||
|
||||
debugf("Send LogEntries to %s ", u)
|
||||
log.Debugf("Send LogEntries to %s ", u)
|
||||
|
||||
thisFollowerStats, ok := t.raftServer.followersStats.Followers[peer.Name]
|
||||
thisFollowerStats, ok := t.peerServer.followersStats.Followers[peer.Name]
|
||||
|
||||
if !ok { //this is the first time this follower has been seen
|
||||
thisFollowerStats = &raftFollowerStats{}
|
||||
thisFollowerStats.Latency.Minimum = 1 << 63
|
||||
t.raftServer.followersStats.Followers[peer.Name] = thisFollowerStats
|
||||
t.peerServer.followersStats.Followers[peer.Name] = thisFollowerStats
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
@ -90,7 +91,7 @@ func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.P
|
||||
end := time.Now()
|
||||
|
||||
if err != nil {
|
||||
debugf("Cannot send AppendEntriesRequest to %s: %s", u, err)
|
||||
log.Debugf("Cannot send AppendEntriesRequest to %s: %s", u, err)
|
||||
if ok {
|
||||
thisFollowerStats.Fail()
|
||||
}
|
||||
@ -121,13 +122,13 @@ func (t *transporter) SendVoteRequest(server *raft.Server, peer *raft.Peer, req
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(req)
|
||||
|
||||
u, _ := nameToRaftURL(peer.Name)
|
||||
debugf("Send Vote to %s", u)
|
||||
u, _ := t.peerServer.registry.PeerURL(peer.Name)
|
||||
log.Debugf("Send Vote from %s to %s", server.Name(), u)
|
||||
|
||||
resp, httpRequest, err := t.Post(fmt.Sprintf("%s/vote", u), &b)
|
||||
|
||||
if err != nil {
|
||||
debugf("Cannot send VoteRequest to %s : %s", u, err)
|
||||
log.Debugf("Cannot send VoteRequest to %s : %s", u, err)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
@ -150,14 +151,14 @@ func (t *transporter) SendSnapshotRequest(server *raft.Server, peer *raft.Peer,
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(req)
|
||||
|
||||
u, _ := nameToRaftURL(peer.Name)
|
||||
debugf("Send Snapshot to %s [Last Term: %d, LastIndex %d]", u,
|
||||
u, _ := t.peerServer.registry.PeerURL(peer.Name)
|
||||
log.Debugf("Send Snapshot to %s [Last Term: %d, LastIndex %d]", u,
|
||||
req.LastTerm, req.LastIndex)
|
||||
|
||||
resp, httpRequest, err := t.Post(fmt.Sprintf("%s/snapshot", u), &b)
|
||||
|
||||
if err != nil {
|
||||
debugf("Cannot send SendSnapshotRequest to %s : %s", u, err)
|
||||
log.Debugf("Cannot send SendSnapshotRequest to %s : %s", u, err)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
@ -181,14 +182,14 @@ func (t *transporter) SendSnapshotRecoveryRequest(server *raft.Server, peer *raf
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(req)
|
||||
|
||||
u, _ := nameToRaftURL(peer.Name)
|
||||
debugf("Send SnapshotRecovery to %s [Last Term: %d, LastIndex %d]", u,
|
||||
u, _ := t.peerServer.registry.PeerURL(peer.Name)
|
||||
log.Debugf("Send SnapshotRecovery to %s [Last Term: %d, LastIndex %d]", u,
|
||||
req.LastTerm, req.LastIndex)
|
||||
|
||||
resp, _, err := t.Post(fmt.Sprintf("%s/snapshotRecovery", u), &b)
|
||||
|
||||
if err != nil {
|
||||
debugf("Cannot send SendSnapshotRecoveryRequest to %s : %s", u, err)
|
||||
log.Debugf("Cannot send SendSnapshotRecoveryRequest to %s : %s", u, err)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
@ -205,26 +206,19 @@ func (t *transporter) SendSnapshotRecoveryRequest(server *raft.Server, peer *raf
|
||||
|
||||
// Send server side POST request
|
||||
func (t *transporter) Post(urlStr string, body io.Reader) (*http.Response, *http.Request, error) {
|
||||
|
||||
req, _ := http.NewRequest("POST", urlStr, body)
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
|
||||
return resp, req, err
|
||||
|
||||
}
|
||||
|
||||
// Send server side GET request
|
||||
func (t *transporter) Get(urlStr string) (*http.Response, *http.Request, error) {
|
||||
|
||||
req, _ := http.NewRequest("GET", urlStr, nil)
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
|
||||
return resp, req, err
|
||||
}
|
||||
|
||||
// Cancel the on fly HTTP transaction when timeout happens
|
||||
// Cancel the on fly HTTP transaction when timeout happens.
|
||||
func (t *transporter) CancelWhenTimeout(req *http.Request) {
|
||||
go func() {
|
||||
time.Sleep(ElectionTimeout)
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
26
server/util.go
Normal file
26
server/util.go
Normal file
@ -0,0 +1,26 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
)
|
||||
|
||||
func decodeJsonRequest(req *http.Request, data interface{}) error {
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
if err := decoder.Decode(&data); err != nil && err != io.EOF {
|
||||
log.Warnf("Malformed json request: %v", err)
|
||||
return fmt.Errorf("Malformed json request: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func redirect(hostname string, w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
url := hostname + path
|
||||
log.Debugf("Redirect to %s", url)
|
||||
http.Redirect(w, req, url, http.StatusTemporaryRedirect)
|
||||
}
|
15
server/v1/delete_key_handler.go
Normal file
15
server/v1/delete_key_handler.go
Normal file
@ -0,0 +1,15 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Removes a key from the store.
|
||||
func DeleteKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
c := &store.DeleteCommand{Key: key}
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
27
server/v1/get_key_handler.go
Normal file
27
server/v1/get_key_handler.go
Normal file
@ -0,0 +1,27 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Retrieves the value for a given key.
|
||||
func GetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
// Retrieve the key from the store.
|
||||
event, err := s.Store().Get(key, false, false, s.CommitIndex(), s.Term())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert event to a response and write to client.
|
||||
b, _ := json.Marshal(event.Response())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
|
||||
return nil
|
||||
}
|
51
server/v1/set_key_handler.go
Normal file
51
server/v1/set_key_handler.go
Normal file
@ -0,0 +1,51 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Sets the value for a given key.
|
||||
func SetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
req.ParseForm()
|
||||
|
||||
// Parse non-blank value.
|
||||
value := req.Form.Get("value")
|
||||
if len(value) == 0 {
|
||||
return etcdErr.NewError(200, "Set", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
// Convert time-to-live to an expiration time.
|
||||
expireTime, err := store.TTL(req.Form.Get("ttl"))
|
||||
if err != nil {
|
||||
return etcdErr.NewError(202, "Set", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
// If the "prevValue" is specified then test-and-set. Otherwise create a new key.
|
||||
var c raft.Command
|
||||
if prevValueArr, ok := req.Form["prevValue"]; ok && len(prevValueArr) > 0 {
|
||||
c = &store.TestAndSetCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
PrevValue: prevValueArr[0],
|
||||
ExpireTime: expireTime,
|
||||
}
|
||||
|
||||
} else {
|
||||
c = &store.CreateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
Force: true,
|
||||
}
|
||||
}
|
||||
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
15
server/v1/v1.go
Normal file
15
server/v1/v1.go
Normal file
@ -0,0 +1,15 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// The Server interface provides all the methods required for the v1 API.
|
||||
type Server interface {
|
||||
CommitIndex() uint64
|
||||
Term() uint64
|
||||
Store() *store.Store
|
||||
Dispatch(raft.Command, http.ResponseWriter, *http.Request) error
|
||||
}
|
40
server/v1/watch_key_handler.go
Normal file
40
server/v1/watch_key_handler.go
Normal file
@ -0,0 +1,40 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Watches a given key prefix for changes.
|
||||
func WatchKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
var err error
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
// Create a command to watch from a given index (default 0).
|
||||
var sinceIndex uint64 = 0
|
||||
if req.Method == "POST" {
|
||||
sinceIndex, err = strconv.ParseUint(string(req.FormValue("index")), 10, 64)
|
||||
if err != nil {
|
||||
return etcdErr.NewError(203, "Watch From Index", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the watcher on the store.
|
||||
c, err := s.Store().Watch(key, false, sinceIndex, s.CommitIndex(), s.Term())
|
||||
if err != nil {
|
||||
return etcdErr.NewError(500, key, store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
event := <-c
|
||||
|
||||
b, _ := json.Marshal(event.Response())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
|
||||
return nil
|
||||
}
|
29
server/v2/create_key_handler.go
Normal file
29
server/v2/create_key_handler.go
Normal file
@ -0,0 +1,29 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func CreateKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
value := req.FormValue("value")
|
||||
expireTime, err := store.TTL(req.FormValue("ttl"))
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Create", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
c := &store.CreateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
IncrementalSuffix: (req.FormValue("incremental") == "true"),
|
||||
}
|
||||
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
20
server/v2/delete_key_handler.go
Normal file
20
server/v2/delete_key_handler.go
Normal file
@ -0,0 +1,20 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func DeleteKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
c := &store.DeleteCommand{
|
||||
Key: key,
|
||||
Recursive: (req.FormValue("recursive") == "true"),
|
||||
}
|
||||
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
69
server/v2/get_key_handler.go
Normal file
69
server/v2/get_key_handler.go
Normal file
@ -0,0 +1,69 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func GetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
var err error
|
||||
var event *store.Event
|
||||
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
// Help client to redirect the request to the current leader
|
||||
if req.FormValue("consistent") == "true" && s.State() != raft.Leader {
|
||||
leader := s.Leader()
|
||||
hostname, _ := s.PeerURL(leader)
|
||||
url := hostname + req.URL.Path
|
||||
log.Debugf("Redirect to %s", url)
|
||||
http.Redirect(w, req, url, http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}
|
||||
|
||||
recursive := (req.FormValue("recursive") == "true")
|
||||
sorted := (req.FormValue("sorted") == "true")
|
||||
|
||||
if req.FormValue("wait") == "true" { // watch
|
||||
// Create a command to watch from a given index (default 0).
|
||||
var sinceIndex uint64 = 0
|
||||
if req.Method == "POST" {
|
||||
sinceIndex, err = strconv.ParseUint(string(req.FormValue("wait_index")), 10, 64)
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Watch From Index", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the watcher on the store.
|
||||
c, err := s.Store().Watch(key, recursive, sinceIndex, s.CommitIndex(), s.Term())
|
||||
if err != nil {
|
||||
return etcdErr.NewError(500, key, store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
event = <-c
|
||||
|
||||
} else { //get
|
||||
// Retrieve the key from the store.
|
||||
event, err = s.Store().Get(key, recursive, sorted, s.CommitIndex(), s.Term())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(event.Index))
|
||||
w.Header().Add("X-Etcd-Term", fmt.Sprint(event.Term))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
b, _ := json.Marshal(event)
|
||||
w.Write(b)
|
||||
|
||||
return nil
|
||||
}
|
64
server/v2/update_key_handler.go
Normal file
64
server/v2/update_key_handler.go
Normal file
@ -0,0 +1,64 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func UpdateKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
|
||||
vars := mux.Vars(req)
|
||||
key := "/" + vars["key"]
|
||||
|
||||
req.ParseForm()
|
||||
|
||||
value := req.Form.Get("value")
|
||||
expireTime, err := store.TTL(req.Form.Get("ttl"))
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
// Update should give at least one option
|
||||
if value == "" && expireTime.Sub(store.Permanent) == 0 {
|
||||
return etcdErr.NewError(etcdErr.EcodeValueOrTTLRequired, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
prevValue, valueOk := req.Form["prevValue"]
|
||||
prevIndexStr, indexOk := req.Form["prevIndex"]
|
||||
|
||||
var c raft.Command
|
||||
if !valueOk && !indexOk { // update without test
|
||||
c = &store.UpdateCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
ExpireTime: expireTime,
|
||||
}
|
||||
|
||||
} else { // update with test
|
||||
var prevIndex uint64
|
||||
|
||||
if indexOk {
|
||||
prevIndex, err = strconv.ParseUint(prevIndexStr[0], 10, 64)
|
||||
|
||||
// bad previous index
|
||||
if err != nil {
|
||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Update", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
} else {
|
||||
prevIndex = 0
|
||||
}
|
||||
|
||||
c = &store.TestAndSetCommand{
|
||||
Key: key,
|
||||
Value: value,
|
||||
PrevValue: prevValue[0],
|
||||
PrevIndex: prevIndex,
|
||||
}
|
||||
}
|
||||
|
||||
return s.Dispatch(c, w, req)
|
||||
}
|
18
server/v2/v2.go
Normal file
18
server/v2/v2.go
Normal file
@ -0,0 +1,18 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-raft"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// The Server interface provides all the methods required for the v2 API.
|
||||
type Server interface {
|
||||
State() string
|
||||
Leader() string
|
||||
CommitIndex() uint64
|
||||
Term() uint64
|
||||
PeerURL(string) (string, bool)
|
||||
Store() *store.Store
|
||||
Dispatch(raft.Command, http.ResponseWriter, *http.Request) error
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package main
|
||||
package server
|
||||
|
||||
const version = "v2"
|
||||
const Version = "v2"
|
||||
|
||||
// TODO: The release version (generated from the git tag) will be the raft
|
||||
// protocol version for now. When things settle down we will fix it like the
|
||||
// client API above.
|
||||
const raftVersion = releaseVersion
|
||||
const PeerVersion = releaseVersion
|
36
snapshot.go
36
snapshot.go
@ -1,36 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// basic conf.
|
||||
// TODO: find a good policy to do snapshot
|
||||
type snapshotConf struct {
|
||||
// Etcd will check if snapshot is need every checkingInterval
|
||||
checkingInterval time.Duration
|
||||
// The number of writes when the last snapshot happened
|
||||
lastWrites uint64
|
||||
// If the incremental number of writes since the last snapshot
|
||||
// exceeds the write Threshold, etcd will do a snapshot
|
||||
writesThr uint64
|
||||
}
|
||||
|
||||
var snapConf *snapshotConf
|
||||
|
||||
func (r *raftServer) newSnapshotConf() *snapshotConf {
|
||||
// check snapshot every 3 seconds and the threshold is 20K
|
||||
return &snapshotConf{time.Second * 3, 0, 20 * 1000}
|
||||
}
|
||||
|
||||
func (r *raftServer) monitorSnapshot() {
|
||||
for {
|
||||
time.Sleep(snapConf.checkingInterval)
|
||||
//currentWrites := etcdStore.TotalWrites() - snapConf.lastWrites
|
||||
currentWrites := 0
|
||||
if uint64(currentWrites) > snapConf.writesThr {
|
||||
r.TakeSnapshot()
|
||||
snapConf.lastWrites = 0
|
||||
}
|
||||
}
|
||||
}
|
39
store/create_command.go
Normal file
39
store/create_command.go
Normal file
@ -0,0 +1,39 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&CreateCommand{})
|
||||
}
|
||||
|
||||
// Create command
|
||||
type CreateCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
IncrementalSuffix bool `json:"incrementalSuffix"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
// The name of the create command in the log
|
||||
func (c *CreateCommand) CommandName() string {
|
||||
return "etcd:create"
|
||||
}
|
||||
|
||||
// Create node
|
||||
func (c *CreateCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*Store)
|
||||
|
||||
e, err := s.Create(c.Key, c.Value, c.IncrementalSuffix, c.Force, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
35
store/delete_command.go
Normal file
35
store/delete_command.go
Normal file
@ -0,0 +1,35 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&DeleteCommand{})
|
||||
}
|
||||
|
||||
// The DeleteCommand removes a key from the Store.
|
||||
type DeleteCommand struct {
|
||||
Key string `json:"key"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// The name of the delete command in the log
|
||||
func (c *DeleteCommand) CommandName() string {
|
||||
return "etcd:delete"
|
||||
}
|
||||
|
||||
// Delete the key
|
||||
func (c *DeleteCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*Store)
|
||||
|
||||
e, err := s.Delete(c.Key, c.Recursive, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
153
store/event.go
153
store/event.go
@ -1,12 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -46,129 +41,39 @@ func newEvent(action string, key string, index uint64, term uint64) *Event {
|
||||
}
|
||||
}
|
||||
|
||||
type eventQueue struct {
|
||||
Events []*Event
|
||||
Size int
|
||||
Front int
|
||||
Capacity int
|
||||
}
|
||||
// Converts an event object into a response object.
|
||||
func (event *Event) Response() interface{} {
|
||||
if !event.Dir {
|
||||
response := &Response{
|
||||
Action: event.Action,
|
||||
Key: event.Key,
|
||||
Value: event.Value,
|
||||
PrevValue: event.PrevValue,
|
||||
Index: event.Index,
|
||||
TTL: event.TTL,
|
||||
Expiration: event.Expiration,
|
||||
}
|
||||
|
||||
func (eq *eventQueue) back() int {
|
||||
return (eq.Front + eq.Size - 1 + eq.Capacity) % eq.Capacity
|
||||
}
|
||||
if response.Action == Create || response.Action == Update {
|
||||
response.Action = "set"
|
||||
if response.PrevValue == "" {
|
||||
response.NewKey = true
|
||||
}
|
||||
}
|
||||
|
||||
func (eq *eventQueue) insert(e *Event) {
|
||||
index := (eq.back() + 1) % eq.Capacity
|
||||
|
||||
eq.Events[index] = e
|
||||
|
||||
if eq.Size == eq.Capacity { //dequeue
|
||||
eq.Front = (index + 1) % eq.Capacity
|
||||
return response
|
||||
} else {
|
||||
eq.Size++
|
||||
}
|
||||
responses := make([]*Response, len(event.KVPairs))
|
||||
|
||||
}
|
||||
|
||||
type EventHistory struct {
|
||||
Queue eventQueue
|
||||
StartIndex uint64
|
||||
LastIndex uint64
|
||||
LastTerm uint64
|
||||
DupCnt uint64 // help to compute the watching point with duplicated indexes in the queue
|
||||
rwl sync.RWMutex
|
||||
}
|
||||
|
||||
func newEventHistory(capacity int) *EventHistory {
|
||||
return &EventHistory{
|
||||
Queue: eventQueue{
|
||||
Capacity: capacity,
|
||||
Events: make([]*Event, capacity),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// addEvent function adds event into the eventHistory
|
||||
func (eh *EventHistory) addEvent(e *Event) *Event {
|
||||
eh.rwl.Lock()
|
||||
defer eh.rwl.Unlock()
|
||||
|
||||
var duped uint64
|
||||
|
||||
if e.Index == UndefIndex {
|
||||
e.Index = eh.LastIndex
|
||||
e.Term = eh.LastTerm
|
||||
duped = 1
|
||||
}
|
||||
|
||||
eh.Queue.insert(e)
|
||||
|
||||
eh.LastIndex = e.Index
|
||||
eh.LastTerm = e.Term
|
||||
eh.DupCnt += duped
|
||||
|
||||
eh.StartIndex = eh.Queue.Events[eh.Queue.Front].Index
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// scan function is enumerating events from the index in history and
|
||||
// stops till the first point where the key has identified prefix
|
||||
func (eh *EventHistory) scan(prefix string, index uint64) (*Event, *etcdErr.Error) {
|
||||
eh.rwl.RLock()
|
||||
defer eh.rwl.RUnlock()
|
||||
|
||||
start := index - eh.StartIndex
|
||||
|
||||
// the index should locate after the event history's StartIndex
|
||||
if start < 0 {
|
||||
return nil,
|
||||
etcdErr.NewError(etcdErr.EcodeEventIndexCleared,
|
||||
fmt.Sprintf("the requested history has been cleared [%v/%v]",
|
||||
eh.StartIndex, index), UndefIndex, UndefTerm)
|
||||
}
|
||||
|
||||
// the index should locate before the size of the queue minus the duplicate count
|
||||
if start >= (uint64(eh.Queue.Size) - eh.DupCnt) { // future index
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i := int((start + uint64(eh.Queue.Front)) % uint64(eh.Queue.Capacity))
|
||||
|
||||
for {
|
||||
e := eh.Queue.Events[i]
|
||||
if strings.HasPrefix(e.Key, prefix) && index <= e.Index { // make sure we bypass the smaller one
|
||||
return e, nil
|
||||
}
|
||||
|
||||
i = (i + 1) % eh.Queue.Capacity
|
||||
|
||||
if i == eh.Queue.back() { // find nothing, return and watch from current index
|
||||
return nil, nil
|
||||
for i, kv := range event.KVPairs {
|
||||
responses[i] = &Response{
|
||||
Action: event.Action,
|
||||
Key: kv.Key,
|
||||
Value: kv.Value,
|
||||
Dir: kv.Dir,
|
||||
Index: event.Index,
|
||||
}
|
||||
}
|
||||
return responses
|
||||
}
|
||||
}
|
||||
|
||||
// clone will be protected by a stop-world lock
|
||||
// do not need to obtain internal lock
|
||||
func (eh *EventHistory) clone() *EventHistory {
|
||||
clonedQueue := eventQueue{
|
||||
Capacity: eh.Queue.Capacity,
|
||||
Events: make([]*Event, eh.Queue.Capacity),
|
||||
Size: eh.Queue.Size,
|
||||
Front: eh.Queue.Front,
|
||||
}
|
||||
|
||||
for i, e := range eh.Queue.Events {
|
||||
clonedQueue.Events[i] = e
|
||||
}
|
||||
|
||||
return &EventHistory{
|
||||
StartIndex: eh.StartIndex,
|
||||
Queue: clonedQueue,
|
||||
LastIndex: eh.LastIndex,
|
||||
LastTerm: eh.LastTerm,
|
||||
DupCnt: eh.DupCnt,
|
||||
}
|
||||
|
||||
}
|
||||
|
112
store/event_history.go
Normal file
112
store/event_history.go
Normal file
@ -0,0 +1,112 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
)
|
||||
|
||||
type EventHistory struct {
|
||||
Queue eventQueue
|
||||
StartIndex uint64
|
||||
LastIndex uint64
|
||||
LastTerm uint64
|
||||
DupCnt uint64 // help to compute the watching point with duplicated indexes in the queue
|
||||
rwl sync.RWMutex
|
||||
}
|
||||
|
||||
func newEventHistory(capacity int) *EventHistory {
|
||||
return &EventHistory{
|
||||
Queue: eventQueue{
|
||||
Capacity: capacity,
|
||||
Events: make([]*Event, capacity),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// addEvent function adds event into the eventHistory
|
||||
func (eh *EventHistory) addEvent(e *Event) *Event {
|
||||
eh.rwl.Lock()
|
||||
defer eh.rwl.Unlock()
|
||||
|
||||
var duped uint64
|
||||
|
||||
if e.Index == UndefIndex {
|
||||
e.Index = eh.LastIndex
|
||||
e.Term = eh.LastTerm
|
||||
duped = 1
|
||||
}
|
||||
|
||||
eh.Queue.insert(e)
|
||||
|
||||
eh.LastIndex = e.Index
|
||||
eh.LastTerm = e.Term
|
||||
eh.DupCnt += duped
|
||||
|
||||
eh.StartIndex = eh.Queue.Events[eh.Queue.Front].Index
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// scan function is enumerating events from the index in history and
|
||||
// stops till the first point where the key has identified prefix
|
||||
func (eh *EventHistory) scan(prefix string, index uint64) (*Event, *etcdErr.Error) {
|
||||
eh.rwl.RLock()
|
||||
defer eh.rwl.RUnlock()
|
||||
|
||||
start := index - eh.StartIndex
|
||||
|
||||
// the index should locate after the event history's StartIndex
|
||||
if start < 0 {
|
||||
return nil,
|
||||
etcdErr.NewError(etcdErr.EcodeEventIndexCleared,
|
||||
fmt.Sprintf("the requested history has been cleared [%v/%v]",
|
||||
eh.StartIndex, index), UndefIndex, UndefTerm)
|
||||
}
|
||||
|
||||
// the index should locate before the size of the queue minus the duplicate count
|
||||
if start >= (uint64(eh.Queue.Size) - eh.DupCnt) { // future index
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i := int((start + uint64(eh.Queue.Front)) % uint64(eh.Queue.Capacity))
|
||||
|
||||
for {
|
||||
e := eh.Queue.Events[i]
|
||||
if strings.HasPrefix(e.Key, prefix) && index <= e.Index { // make sure we bypass the smaller one
|
||||
return e, nil
|
||||
}
|
||||
|
||||
i = (i + 1) % eh.Queue.Capacity
|
||||
|
||||
if i == eh.Queue.back() { // find nothing, return and watch from current index
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clone will be protected by a stop-world lock
|
||||
// do not need to obtain internal lock
|
||||
func (eh *EventHistory) clone() *EventHistory {
|
||||
clonedQueue := eventQueue{
|
||||
Capacity: eh.Queue.Capacity,
|
||||
Events: make([]*Event, eh.Queue.Capacity),
|
||||
Size: eh.Queue.Size,
|
||||
Front: eh.Queue.Front,
|
||||
}
|
||||
|
||||
for i, e := range eh.Queue.Events {
|
||||
clonedQueue.Events[i] = e
|
||||
}
|
||||
|
||||
return &EventHistory{
|
||||
StartIndex: eh.StartIndex,
|
||||
Queue: clonedQueue,
|
||||
LastIndex: eh.LastIndex,
|
||||
LastTerm: eh.LastTerm,
|
||||
DupCnt: eh.DupCnt,
|
||||
}
|
||||
|
||||
}
|
25
store/event_queue.go
Normal file
25
store/event_queue.go
Normal file
@ -0,0 +1,25 @@
|
||||
package store
|
||||
|
||||
type eventQueue struct {
|
||||
Events []*Event
|
||||
Size int
|
||||
Front int
|
||||
Capacity int
|
||||
}
|
||||
|
||||
func (eq *eventQueue) back() int {
|
||||
return (eq.Front + eq.Size - 1 + eq.Capacity) % eq.Capacity
|
||||
}
|
||||
|
||||
func (eq *eventQueue) insert(e *Event) {
|
||||
index := (eq.back() + 1) % eq.Capacity
|
||||
|
||||
eq.Events[index] = e
|
||||
|
||||
if eq.Size == eq.Capacity { //dequeue
|
||||
eq.Front = (index + 1) % eq.Capacity
|
||||
} else {
|
||||
eq.Size++
|
||||
}
|
||||
|
||||
}
|
41
store/test_and_set_command.go
Normal file
41
store/test_and_set_command.go
Normal file
@ -0,0 +1,41 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&TestAndSetCommand{})
|
||||
}
|
||||
|
||||
// The TestAndSetCommand performs a conditional update on a key in the store.
|
||||
type TestAndSetCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
PrevValue string `json: prevValue`
|
||||
PrevIndex uint64 `json: prevIndex`
|
||||
}
|
||||
|
||||
// The name of the testAndSet command in the log
|
||||
func (c *TestAndSetCommand) CommandName() string {
|
||||
return "etcd:testAndSet"
|
||||
}
|
||||
|
||||
// Set the key-value pair if the current value of the key equals to the given prevValue
|
||||
func (c *TestAndSetCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*Store)
|
||||
|
||||
e, err := s.TestAndSet(c.Key, c.PrevValue, c.PrevIndex,
|
||||
c.Value, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
20
store/ttl.go
Normal file
20
store/ttl.go
Normal file
@ -0,0 +1,20 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Convert string duration to time format
|
||||
func TTL(duration string) (time.Time, error) {
|
||||
if duration != "" {
|
||||
duration, err := strconv.Atoi(duration)
|
||||
if err != nil {
|
||||
return Permanent, err
|
||||
}
|
||||
return time.Now().Add(time.Second * (time.Duration)(duration)), nil
|
||||
|
||||
} else {
|
||||
return Permanent, nil
|
||||
}
|
||||
}
|
38
store/update_command.go
Normal file
38
store/update_command.go
Normal file
@ -0,0 +1,38 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/go-raft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.RegisterCommand(&UpdateCommand{})
|
||||
}
|
||||
|
||||
// The UpdateCommand updates the value of a key in the Store.
|
||||
type UpdateCommand struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
}
|
||||
|
||||
// The name of the update command in the log
|
||||
func (c *UpdateCommand) CommandName() string {
|
||||
return "etcd:update"
|
||||
}
|
||||
|
||||
// Update node
|
||||
func (c *UpdateCommand) Apply(server *raft.Server) (interface{}, error) {
|
||||
s, _ := server.StateMachine().(*Store)
|
||||
|
||||
e, err := s.Update(c.Key, c.Value, c.ExpireTime, server.CommitIndex(), server.Term())
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
2
third_party/deps
vendored
2
third_party/deps
vendored
@ -3,6 +3,8 @@ packages="
|
||||
github.com/coreos/go-etcd
|
||||
github.com/coreos/go-log/log
|
||||
github.com/coreos/go-systemd
|
||||
github.com/gorilla/context
|
||||
github.com/gorilla/mux
|
||||
bitbucket.org/kardianos/osext
|
||||
code.google.com/p/go.net
|
||||
code.google.com/p/goprotobuf
|
||||
|
@ -136,7 +136,6 @@ func (c *Client) internalSyncCluster(machines []string) bool {
|
||||
logger.Debugf("update.leader[%s,%s]", c.cluster.Leader, c.cluster.Machines[0])
|
||||
c.cluster.Leader = c.cluster.Machines[0]
|
||||
|
||||
logger.Debug("sync.machines ", c.cluster.Machines)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
27
third_party/github.com/gorilla/context/LICENSE
vendored
Normal file
27
third_party/github.com/gorilla/context/LICENSE
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
6
third_party/github.com/gorilla/context/README.md
vendored
Normal file
6
third_party/github.com/gorilla/context/README.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
context
|
||||
=======
|
||||
|
||||
gorilla/context is a general purpose registry for global request variables.
|
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
|
112
third_party/github.com/gorilla/context/context.go
vendored
Normal file
112
third_party/github.com/gorilla/context/context.go
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
)
|
||||
|
||||
// Set stores a value for a given key in a given request.
|
||||
func Set(r *http.Request, key, val interface{}) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if data[r] == nil {
|
||||
data[r] = make(map[interface{}]interface{})
|
||||
datat[r] = time.Now().Unix()
|
||||
}
|
||||
data[r][key] = val
|
||||
}
|
||||
|
||||
// Get returns a value stored for a given key in a given request.
|
||||
func Get(r *http.Request, key interface{}) interface{} {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if data[r] != nil {
|
||||
return data[r][key]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOk returns stored value and presence state like multi-value return of map access.
|
||||
func GetOk(r *http.Request, key interface{}) (interface{}, bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if _, ok := data[r]; ok {
|
||||
value, ok := data[r][key]
|
||||
return value, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Delete removes a value stored for a given key in a given request.
|
||||
func Delete(r *http.Request, key interface{}) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if data[r] != nil {
|
||||
delete(data[r], key)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all values stored for a given request.
|
||||
//
|
||||
// This is usually called by a handler wrapper to clean up request
|
||||
// variables at the end of a request lifetime. See ClearHandler().
|
||||
func Clear(r *http.Request) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
clear(r)
|
||||
}
|
||||
|
||||
// clear is Clear without the lock.
|
||||
func clear(r *http.Request) {
|
||||
delete(data, r)
|
||||
delete(datat, r)
|
||||
}
|
||||
|
||||
// Purge removes request data stored for longer than maxAge, in seconds.
|
||||
// It returns the amount of requests removed.
|
||||
//
|
||||
// If maxAge <= 0, all request data is removed.
|
||||
//
|
||||
// This is only used for sanity check: in case context cleaning was not
|
||||
// properly set some request data can be kept forever, consuming an increasing
|
||||
// amount of memory. In case this is detected, Purge() must be called
|
||||
// periodically until the problem is fixed.
|
||||
func Purge(maxAge int) int {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
count := 0
|
||||
if maxAge <= 0 {
|
||||
count = len(data)
|
||||
data = make(map[*http.Request]map[interface{}]interface{})
|
||||
datat = make(map[*http.Request]int64)
|
||||
} else {
|
||||
min := time.Now().Unix() - int64(maxAge)
|
||||
for r, _ := range data {
|
||||
if datat[r] < min {
|
||||
clear(r)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ClearHandler wraps an http.Handler and clears request values at the end
|
||||
// of a request lifetime.
|
||||
func ClearHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer Clear(r)
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
66
third_party/github.com/gorilla/context/context_test.go
vendored
Normal file
66
third_party/github.com/gorilla/context/context_test.go
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type keyType int
|
||||
|
||||
const (
|
||||
key1 keyType = iota
|
||||
key2
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
assertEqual := func(val interface{}, exp interface{}) {
|
||||
if val != exp {
|
||||
t.Errorf("Expected %v, got %v.", exp, val)
|
||||
}
|
||||
}
|
||||
|
||||
r, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
|
||||
// Get()
|
||||
assertEqual(Get(r, key1), nil)
|
||||
|
||||
// Set()
|
||||
Set(r, key1, "1")
|
||||
assertEqual(Get(r, key1), "1")
|
||||
assertEqual(len(data[r]), 1)
|
||||
|
||||
Set(r, key2, "2")
|
||||
assertEqual(Get(r, key2), "2")
|
||||
assertEqual(len(data[r]), 2)
|
||||
|
||||
//GetOk
|
||||
value, ok := GetOk(r, key1)
|
||||
assertEqual(value, "1")
|
||||
assertEqual(ok, true)
|
||||
|
||||
value, ok = GetOk(r, "not exists")
|
||||
assertEqual(value, nil)
|
||||
assertEqual(ok, false)
|
||||
|
||||
Set(r, "nil value", nil)
|
||||
value, ok = GetOk(r, "nil value")
|
||||
assertEqual(value, nil)
|
||||
assertEqual(ok, true)
|
||||
|
||||
// Delete()
|
||||
Delete(r, key1)
|
||||
assertEqual(Get(r, key1), nil)
|
||||
assertEqual(len(data[r]), 2)
|
||||
|
||||
Delete(r, key2)
|
||||
assertEqual(Get(r, key2), nil)
|
||||
assertEqual(len(data[r]), 1)
|
||||
|
||||
// Clear()
|
||||
Clear(r)
|
||||
assertEqual(len(data), 0)
|
||||
}
|
82
third_party/github.com/gorilla/context/doc.go
vendored
Normal file
82
third_party/github.com/gorilla/context/doc.go
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package gorilla/context stores values shared during a request lifetime.
|
||||
|
||||
For example, a router can set variables extracted from the URL and later
|
||||
application handlers can access those values, or it can be used to store
|
||||
sessions values to be saved at the end of a request. There are several
|
||||
others common uses.
|
||||
|
||||
The idea was posted by Brad Fitzpatrick to the go-nuts mailing list:
|
||||
|
||||
http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53
|
||||
|
||||
Here's the basic usage: first define the keys that you will need. The key
|
||||
type is interface{} so a key can be of any type that supports equality.
|
||||
Here we define a key using a custom int type to avoid name collisions:
|
||||
|
||||
package foo
|
||||
|
||||
import (
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const MyKey key = 0
|
||||
|
||||
Then set a variable. Variables are bound to an http.Request object, so you
|
||||
need a request instance to set a value:
|
||||
|
||||
context.Set(r, MyKey, "bar")
|
||||
|
||||
The application can later access the variable using the same key you provided:
|
||||
|
||||
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// val is "bar".
|
||||
val := context.Get(r, foo.MyKey)
|
||||
|
||||
// returns ("bar", true)
|
||||
val, ok := context.GetOk(r, foo.MyKey)
|
||||
// ...
|
||||
}
|
||||
|
||||
And that's all about the basic usage. We discuss some other ideas below.
|
||||
|
||||
Any type can be stored in the context. To enforce a given type, make the key
|
||||
private and wrap Get() and Set() to accept and return values of a specific
|
||||
type:
|
||||
|
||||
type key int
|
||||
|
||||
const mykey key = 0
|
||||
|
||||
// GetMyKey returns a value for this package from the request values.
|
||||
func GetMyKey(r *http.Request) SomeType {
|
||||
if rv := context.Get(r, mykey); rv != nil {
|
||||
return rv.(SomeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMyKey sets a value for this package in the request values.
|
||||
func SetMyKey(r *http.Request, val SomeType) {
|
||||
context.Set(r, mykey, val)
|
||||
}
|
||||
|
||||
Variables must be cleared at the end of a request, to remove all values
|
||||
that were stored. This can be done in an http.Handler, after a request was
|
||||
served. Just call Clear() passing the request:
|
||||
|
||||
context.Clear(r)
|
||||
|
||||
...or use ClearHandler(), which conveniently wraps an http.Handler to clear
|
||||
variables at the end of a request lifetime.
|
||||
|
||||
The Routers from the packages gorilla/mux and gorilla/pat call Clear()
|
||||
so if you are using either of them you don't need to clear the context manually.
|
||||
*/
|
||||
package context
|
27
third_party/github.com/gorilla/mux/LICENSE
vendored
Normal file
27
third_party/github.com/gorilla/mux/LICENSE
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
6
third_party/github.com/gorilla/mux/README.md
vendored
Normal file
6
third_party/github.com/gorilla/mux/README.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
mux
|
||||
===
|
||||
|
||||
gorilla/mux is a powerful URL router and dispatcher.
|
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux
|
21
third_party/github.com/gorilla/mux/bench_test.go
vendored
Normal file
21
third_party/github.com/gorilla/mux/bench_test.go
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkMux(b *testing.B) {
|
||||
router := new(Router)
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {}
|
||||
router.HandleFunc("/v1/{v1}", handler)
|
||||
|
||||
request, _ := http.NewRequest("GET", "/v1/anything", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.ServeHTTP(nil, request)
|
||||
}
|
||||
}
|
199
third_party/github.com/gorilla/mux/doc.go
vendored
Normal file
199
third_party/github.com/gorilla/mux/doc.go
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package gorilla/mux implements a request router and dispatcher.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard
|
||||
http.ServeMux, mux.Router matches incoming requests against a list of
|
||||
registered routes and calls a handler for the route that matches the URL
|
||||
or other conditions. The main features are:
|
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes,
|
||||
header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts and paths can have variables with an optional regular
|
||||
expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining
|
||||
references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the
|
||||
parent route matches. This is useful to define groups of routes that
|
||||
share common conditions like a host, a path prefix or other repeated
|
||||
attributes. As a bonus, this optimizes request matching.
|
||||
* It implements the http.Handler interface so it is compatible with the
|
||||
standard http.ServeMux.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is
|
||||
equivalent to how http.HandleFunc() works: if an incoming request URL matches
|
||||
one of the paths, the corresponding handler is called passing
|
||||
(http.ResponseWriter, *http.Request) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format {name} or
|
||||
{name:pattern}. If a regular expression pattern is not defined, the matched
|
||||
variable will be anything until the next slash. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved
|
||||
calling mux.Vars():
|
||||
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options
|
||||
are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host
|
||||
pattern to be matched. They can also have variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.domain.com".
|
||||
r.Host("www.domain.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
r.PathPrefix("/products/")
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
r.Methods("GET", "POST")
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
r.Schemes("https")
|
||||
|
||||
...or header values:
|
||||
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
...or query values:
|
||||
|
||||
r.Queries("key", "value")
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.domain.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have
|
||||
a way to group several routes that share the same requirements.
|
||||
We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the
|
||||
host is "www.domain.com". Create a route for that host and get a "subrouter"
|
||||
from it:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.domain.com").Subrouter()
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is
|
||||
"www.domain.com", because the subrouter is tested first. This is not
|
||||
only convenient, but also optimizes request matching. You can create
|
||||
subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define
|
||||
subrouters in a central place and then parts of the app can register its
|
||||
paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix,
|
||||
the inner routes use it as base for their paths:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details"), ProductDetailsHandler)
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built,
|
||||
or "reversed". We define a name calling Name() on a route. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
To build a URL, get the route and call the URL() method, passing a sequence of
|
||||
key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
|
||||
...and the result will be a url.URL with the following path:
|
||||
|
||||
"/articles/technology/42"
|
||||
|
||||
This also works for host variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
|
||||
All variables defined in the route are required, and their values must
|
||||
conform to the corresponding patterns. These requirements guarantee that a
|
||||
generated URL will always match a registered route -- the only exception is
|
||||
for explicitly defined "build-only" routes which never match.
|
||||
|
||||
There's also a way to build only the URL host or path for a route:
|
||||
use the methods URLHost() or URLPath() instead. For the previous route,
|
||||
we would do:
|
||||
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built
|
||||
as well:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
*/
|
||||
package mux
|
339
third_party/github.com/gorilla/mux/mux.go
vendored
Normal file
339
third_party/github.com/gorilla/mux/mux.go
vendored
Normal file
@ -0,0 +1,339 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
// NewRouter returns a new router instance.
|
||||
func NewRouter() *Router {
|
||||
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
|
||||
}
|
||||
|
||||
// Router registers routes to be matched and dispatches a handler.
|
||||
//
|
||||
// It implements the http.Handler interface, so it can be registered to serve
|
||||
// requests:
|
||||
//
|
||||
// var router = mux.NewRouter()
|
||||
//
|
||||
// func main() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// Or, for Google App Engine, register it in a init() function:
|
||||
//
|
||||
// func init() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// This will send all incoming requests to the router.
|
||||
type Router struct {
|
||||
// Configurable Handler to be used when no route matches.
|
||||
NotFoundHandler http.Handler
|
||||
// Parent route, if this is a subrouter.
|
||||
parent parentRoute
|
||||
// Routes to be matched, in order.
|
||||
routes []*Route
|
||||
// Routes by name for URL building.
|
||||
namedRoutes map[string]*Route
|
||||
// See Router.StrictSlash(). This defines the flag for new routes.
|
||||
strictSlash bool
|
||||
// If true, do not clear the request context after handling the request
|
||||
KeepContext bool
|
||||
}
|
||||
|
||||
// Match matches registered routes against the request.
|
||||
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||
for _, route := range r.routes {
|
||||
if route.Match(req, match) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ServeHTTP dispatches the handler registered in the matched route.
|
||||
//
|
||||
// When there is a match, the route variables can be retrieved calling
|
||||
// mux.Vars(request).
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Clean path to canonical form and redirect.
|
||||
if p := cleanPath(req.URL.Path); p != req.URL.Path {
|
||||
w.Header().Set("Location", p)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
var match RouteMatch
|
||||
var handler http.Handler
|
||||
if r.Match(req, &match) {
|
||||
handler = match.Handler
|
||||
setVars(req, match.Vars)
|
||||
setCurrentRoute(req, match.Route)
|
||||
}
|
||||
if handler == nil {
|
||||
if r.NotFoundHandler == nil {
|
||||
r.NotFoundHandler = http.NotFoundHandler()
|
||||
}
|
||||
handler = r.NotFoundHandler
|
||||
}
|
||||
if !r.KeepContext {
|
||||
defer context.Clear(req)
|
||||
}
|
||||
handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Get returns a route registered with the given name.
|
||||
func (r *Router) Get(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// GetRoute returns a route registered with the given name. This method
|
||||
// was renamed to Get() and remains here for backwards compatibility.
|
||||
func (r *Router) GetRoute(name string) *Route {
|
||||
return r.getNamedRoutes()[name]
|
||||
}
|
||||
|
||||
// StrictSlash defines the slash behavior for new routes.
|
||||
//
|
||||
// When true, if the route path is "/path/", accessing "/path" will redirect
|
||||
// to the former and vice versa.
|
||||
//
|
||||
// Special case: when a route sets a path prefix, strict slash is
|
||||
// automatically set to false for that route because the redirect behavior
|
||||
// can't be determined for prefixes.
|
||||
func (r *Router) StrictSlash(value bool) *Router {
|
||||
r.strictSlash = value
|
||||
return r
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Router) getNamedRoutes() map[string]*Route {
|
||||
if r.namedRoutes == nil {
|
||||
if r.parent != nil {
|
||||
r.namedRoutes = r.parent.getNamedRoutes()
|
||||
} else {
|
||||
r.namedRoutes = make(map[string]*Route)
|
||||
}
|
||||
}
|
||||
return r.namedRoutes
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from the parent route, if any.
|
||||
func (r *Router) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.parent != nil {
|
||||
return r.parent.getRegexpGroup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route factories
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewRoute registers an empty route.
|
||||
func (r *Router) NewRoute() *Route {
|
||||
route := &Route{parent: r, strictSlash: r.strictSlash}
|
||||
r.routes = append(r.routes, route)
|
||||
return route
|
||||
}
|
||||
|
||||
// Handle registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.Handler().
|
||||
func (r *Router) Handle(path string, handler http.Handler) *Route {
|
||||
return r.NewRoute().Path(path).Handler(handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
|
||||
*http.Request)) *Route {
|
||||
return r.NewRoute().Path(path).HandlerFunc(f)
|
||||
}
|
||||
|
||||
// Headers registers a new route with a matcher for request header values.
|
||||
// See Route.Headers().
|
||||
func (r *Router) Headers(pairs ...string) *Route {
|
||||
return r.NewRoute().Headers(pairs...)
|
||||
}
|
||||
|
||||
// Host registers a new route with a matcher for the URL host.
|
||||
// See Route.Host().
|
||||
func (r *Router) Host(tpl string) *Route {
|
||||
return r.NewRoute().Host(tpl)
|
||||
}
|
||||
|
||||
// MatcherFunc registers a new route with a custom matcher function.
|
||||
// See Route.MatcherFunc().
|
||||
func (r *Router) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.NewRoute().MatcherFunc(f)
|
||||
}
|
||||
|
||||
// Methods registers a new route with a matcher for HTTP methods.
|
||||
// See Route.Methods().
|
||||
func (r *Router) Methods(methods ...string) *Route {
|
||||
return r.NewRoute().Methods(methods...)
|
||||
}
|
||||
|
||||
// Path registers a new route with a matcher for the URL path.
|
||||
// See Route.Path().
|
||||
func (r *Router) Path(tpl string) *Route {
|
||||
return r.NewRoute().Path(tpl)
|
||||
}
|
||||
|
||||
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||
// See Route.PathPrefix().
|
||||
func (r *Router) PathPrefix(tpl string) *Route {
|
||||
return r.NewRoute().PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Queries registers a new route with a matcher for URL query values.
|
||||
// See Route.Queries().
|
||||
func (r *Router) Queries(pairs ...string) *Route {
|
||||
return r.NewRoute().Queries(pairs...)
|
||||
}
|
||||
|
||||
// Schemes registers a new route with a matcher for URL schemes.
|
||||
// See Route.Schemes().
|
||||
func (r *Router) Schemes(schemes ...string) *Route {
|
||||
return r.NewRoute().Schemes(schemes...)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RouteMatch stores information about a matched route.
|
||||
type RouteMatch struct {
|
||||
Route *Route
|
||||
Handler http.Handler
|
||||
Vars map[string]string
|
||||
}
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
varsKey contextKey = iota
|
||||
routeKey
|
||||
)
|
||||
|
||||
// Vars returns the route variables for the current request, if any.
|
||||
func Vars(r *http.Request) map[string]string {
|
||||
if rv := context.Get(r, varsKey); rv != nil {
|
||||
return rv.(map[string]string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
func CurrentRoute(r *http.Request) *Route {
|
||||
if rv := context.Get(r, routeKey); rv != nil {
|
||||
return rv.(*Route)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setVars(r *http.Request, val interface{}) {
|
||||
context.Set(r, varsKey, val)
|
||||
}
|
||||
|
||||
func setCurrentRoute(r *http.Request, val interface{}) {
|
||||
context.Set(r, routeKey, val)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||
// Borrowed from the net/http package.
|
||||
func cleanPath(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if p[0] != '/' {
|
||||
p = "/" + p
|
||||
}
|
||||
np := path.Clean(p)
|
||||
// path.Clean removes trailing slash except for root;
|
||||
// put the trailing slash back if necessary.
|
||||
if p[len(p)-1] == '/' && np != "/" {
|
||||
np += "/"
|
||||
}
|
||||
return np
|
||||
}
|
||||
|
||||
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||
func uniqueVars(s1, s2 []string) error {
|
||||
for _, v1 := range s1 {
|
||||
for _, v2 := range s2 {
|
||||
if v1 == v2 {
|
||||
return fmt.Errorf("mux: duplicated route variable %q", v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapFromPairs converts variadic string parameters to a string map.
|
||||
func mapFromPairs(pairs ...string) (map[string]string, error) {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
}
|
||||
m := make(map[string]string, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool {
|
||||
for _, v := range arr {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMap returns true if the given key/value pairs exist in a given map.
|
||||
func matchMap(toCheck map[string]string, toMatch map[string][]string,
|
||||
canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != "" {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v == value {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
755
third_party/github.com/gorilla/mux/mux_test.go
vendored
Normal file
755
third_party/github.com/gorilla/mux/mux_test.go
vendored
Normal file
@ -0,0 +1,755 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
type routeTest struct {
|
||||
title string // title of the test
|
||||
route *Route // the route being tested
|
||||
request *http.Request // a request to test the route
|
||||
vars map[string]string // the expected vars of the match
|
||||
host string // the expected host of the match
|
||||
path string // the expected path of the match
|
||||
shouldMatch bool // whether the request is expected to match the route at all
|
||||
}
|
||||
|
||||
func TestHost(t *testing.T) {
|
||||
// newRequestHost a new request with a method, url, and host header
|
||||
newRequestHost := func(method, url, host string) *http.Request {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Host = host
|
||||
return req
|
||||
}
|
||||
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Host route match",
|
||||
route: new(Route).Host("aaa.bbb.ccc"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route, wrong host in request URL",
|
||||
route: new(Route).Host("aaa.bbb.ccc"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host route with port, match",
|
||||
route: new(Route).Host("aaa.bbb.ccc:1234"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc:1234",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with port, wrong port in request URL",
|
||||
route: new(Route).Host("aaa.bbb.ccc:1234"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc:1234",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host route, match with host in request header",
|
||||
route: new(Route).Host("aaa.bbb.ccc"),
|
||||
request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route, wrong host in request header",
|
||||
route: new(Route).Host("aaa.bbb.ccc"),
|
||||
request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
// BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true},
|
||||
{
|
||||
title: "Host route with port, wrong host in request header",
|
||||
route: new(Route).Host("aaa.bbb.ccc:1234"),
|
||||
request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"),
|
||||
vars: map[string]string{},
|
||||
host: "aaa.bbb.ccc:1234",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host route with pattern, match",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "bbb"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with pattern, wrong host in request URL",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "bbb"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host route with multiple patterns, match",
|
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host route with multiple patterns, wrong host in request URL",
|
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Path route, match",
|
||||
route: new(Route).Path("/111/222/333"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route, wrong path in request in request URL",
|
||||
route: new(Route).Path("/111/222/333"),
|
||||
request: newRequest("GET", "http://localhost/1/2/3"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Path route with pattern, match",
|
||||
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v1": "222"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with pattern, URL in request does not match",
|
||||
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
|
||||
request: newRequest("GET", "http://localhost/111/aaa/333"),
|
||||
vars: map[string]string{"v1": "222"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Path route with multiple patterns, match",
|
||||
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Path route with multiple patterns, URL in request does not match",
|
||||
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/aaa/333"),
|
||||
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
|
||||
host: "",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathPrefix(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "PathPrefix route, match",
|
||||
route: new(Route).PathPrefix("/111"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "/111",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "PathPrefix route, URL prefix in request does not match",
|
||||
route: new(Route).PathPrefix("/111"),
|
||||
request: newRequest("GET", "http://localhost/1/2/3"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "/111",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "PathPrefix route with pattern, match",
|
||||
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v1": "222"},
|
||||
host: "",
|
||||
path: "/111/222",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "PathPrefix route with pattern, URL prefix in request does not match",
|
||||
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/aaa/333"),
|
||||
vars: map[string]string{"v1": "222"},
|
||||
host: "",
|
||||
path: "/111/222",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "PathPrefix route with multiple patterns, match",
|
||||
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/222/333"),
|
||||
vars: map[string]string{"v1": "111", "v2": "222"},
|
||||
host: "",
|
||||
path: "/111/222",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "PathPrefix route with multiple patterns, URL prefix in request does not match",
|
||||
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://localhost/111/aaa/333"),
|
||||
vars: map[string]string{"v1": "111", "v2": "222"},
|
||||
host: "",
|
||||
path: "/111/222",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostPath(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Host and Path route, match",
|
||||
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host and Path route, wrong host in request URL",
|
||||
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host and Path route with pattern, match",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "bbb", "v2": "222"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host and Path route with pattern, URL in request does not match",
|
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "bbb", "v2": "222"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
title: "Host and Path route with multiple patterns, match",
|
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Host and Path route with multiple patterns, URL in request does not match",
|
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"),
|
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
|
||||
host: "aaa.bbb.ccc",
|
||||
path: "/111/222/333",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
// newRequestHeaders creates a new request with a method, url, and headers
|
||||
newRequestHeaders := func(method, url string, headers map[string]string) *http.Request {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Headers route, match",
|
||||
route: new(Route).Headers("foo", "bar", "baz", "ding"),
|
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Headers route, bad header values",
|
||||
route: new(Route).Headers("foo", "bar", "baz", "ding"),
|
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Methods route, match GET",
|
||||
route: new(Route).Methods("GET", "POST"),
|
||||
request: newRequest("GET", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Methods route, match POST",
|
||||
route: new(Route).Methods("GET", "POST"),
|
||||
request: newRequest("POST", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Methods route, bad method",
|
||||
route: new(Route).Methods("GET", "POST"),
|
||||
request: newRequest("PUT", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueries(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "Queries route, match",
|
||||
route: new(Route).Queries("foo", "bar", "baz", "ding"),
|
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route, bad query",
|
||||
route: new(Route).Queries("foo", "bar", "baz", "ding"),
|
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=dong"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemes(t *testing.T) {
|
||||
tests := []routeTest{
|
||||
// Schemes
|
||||
{
|
||||
title: "Schemes route, match https",
|
||||
route: new(Route).Schemes("https", "ftp"),
|
||||
request: newRequest("GET", "https://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Schemes route, match ftp",
|
||||
route: new(Route).Schemes("https", "ftp"),
|
||||
request: newRequest("GET", "ftp://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Schemes route, bad scheme",
|
||||
route: new(Route).Schemes("https", "ftp"),
|
||||
request: newRequest("GET", "http://localhost"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatcherFunc(t *testing.T) {
|
||||
m := func(r *http.Request, m *RouteMatch) bool {
|
||||
if r.URL.Host == "aaa.bbb.ccc" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
tests := []routeTest{
|
||||
{
|
||||
title: "MatchFunc route, match",
|
||||
route: new(Route).MatcherFunc(m),
|
||||
request: newRequest("GET", "http://aaa.bbb.ccc"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "MatchFunc route, non-match",
|
||||
route: new(Route).MatcherFunc(m),
|
||||
request: newRequest("GET", "http://aaa.222.ccc"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubRouter(t *testing.T) {
|
||||
subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter()
|
||||
subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter()
|
||||
|
||||
tests := []routeTest{
|
||||
{
|
||||
route: subrouter1.Path("/{v2:[a-z]+}"),
|
||||
request: newRequest("GET", "http://aaa.google.com/bbb"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
|
||||
host: "aaa.google.com",
|
||||
path: "/bbb",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
route: subrouter1.Path("/{v2:[a-z]+}"),
|
||||
request: newRequest("GET", "http://111.google.com/111"),
|
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
|
||||
host: "aaa.google.com",
|
||||
path: "/bbb",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
route: subrouter2.Path("/baz/{v2}"),
|
||||
request: newRequest("GET", "http://localhost/foo/bar/baz/ding"),
|
||||
vars: map[string]string{"v1": "bar", "v2": "ding"},
|
||||
host: "",
|
||||
path: "/foo/bar/baz/ding",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
route: subrouter2.Path("/baz/{v2}"),
|
||||
request: newRequest("GET", "http://localhost/foo/bar"),
|
||||
vars: map[string]string{"v1": "bar", "v2": "ding"},
|
||||
host: "",
|
||||
path: "/foo/bar/baz/ding",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testRoute(t, test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedRoutes(t *testing.T) {
|
||||
r1 := NewRouter()
|
||||
r1.NewRoute().Name("a")
|
||||
r1.NewRoute().Name("b")
|
||||
r1.NewRoute().Name("c")
|
||||
|
||||
r2 := r1.NewRoute().Subrouter()
|
||||
r2.NewRoute().Name("d")
|
||||
r2.NewRoute().Name("e")
|
||||
r2.NewRoute().Name("f")
|
||||
|
||||
r3 := r2.NewRoute().Subrouter()
|
||||
r3.NewRoute().Name("g")
|
||||
r3.NewRoute().Name("h")
|
||||
r3.NewRoute().Name("i")
|
||||
|
||||
if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 {
|
||||
t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes)
|
||||
} else if r1.Get("i") == nil {
|
||||
t.Errorf("Subroute name not registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictSlash(t *testing.T) {
|
||||
var r *Router
|
||||
var req *http.Request
|
||||
var route *Route
|
||||
var match *RouteMatch
|
||||
var matched bool
|
||||
|
||||
// StrictSlash should be ignored for path prefix.
|
||||
// So we register a route ending in slash but it doesn't attempt to add
|
||||
// the slash for a path not ending in slash.
|
||||
r = NewRouter()
|
||||
r.StrictSlash(true)
|
||||
route = r.NewRoute().PathPrefix("/static/")
|
||||
req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil)
|
||||
match = new(RouteMatch)
|
||||
matched = r.Match(req, match)
|
||||
if !matched {
|
||||
t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route))
|
||||
}
|
||||
if match.Handler != nil {
|
||||
t.Errorf("Should not redirect")
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func getRouteTemplate(route *Route) string {
|
||||
host, path := "none", "none"
|
||||
if route.regexp != nil {
|
||||
if route.regexp.host != nil {
|
||||
host = route.regexp.host.template
|
||||
}
|
||||
if route.regexp.path != nil {
|
||||
path = route.regexp.path.template
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Host: %v, Path: %v", host, path)
|
||||
}
|
||||
|
||||
func testRoute(t *testing.T, test routeTest) {
|
||||
request := test.request
|
||||
route := test.route
|
||||
vars := test.vars
|
||||
shouldMatch := test.shouldMatch
|
||||
host := test.host
|
||||
path := test.path
|
||||
url := test.host + test.path
|
||||
|
||||
var match RouteMatch
|
||||
ok := route.Match(request, &match)
|
||||
if ok != shouldMatch {
|
||||
msg := "Should match"
|
||||
if !shouldMatch {
|
||||
msg = "Should not match"
|
||||
}
|
||||
t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars)
|
||||
return
|
||||
}
|
||||
if shouldMatch {
|
||||
if test.vars != nil && !stringMapEqual(test.vars, match.Vars) {
|
||||
t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars)
|
||||
return
|
||||
}
|
||||
if host != "" {
|
||||
u, _ := test.route.URLHost(mapToPairs(match.Vars)...)
|
||||
if host != u.Host {
|
||||
t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route))
|
||||
return
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
u, _ := route.URLPath(mapToPairs(match.Vars)...)
|
||||
if path != u.Path {
|
||||
t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route))
|
||||
return
|
||||
}
|
||||
}
|
||||
if url != "" {
|
||||
u, _ := route.URL(mapToPairs(match.Vars)...)
|
||||
if url != u.Host+u.Path {
|
||||
t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the context is cleared or not cleared properly depending on
|
||||
// the configuration of the router
|
||||
func TestKeepContext(t *testing.T) {
|
||||
func1 := func(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
r := NewRouter()
|
||||
r.HandleFunc("/", func1).Name("func1")
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/", nil)
|
||||
context.Set(req, "t", 1)
|
||||
|
||||
res := new(http.ResponseWriter)
|
||||
r.ServeHTTP(*res, req)
|
||||
|
||||
if _, ok := context.GetOk(req, "t"); ok {
|
||||
t.Error("Context should have been cleared at end of request")
|
||||
}
|
||||
|
||||
r.KeepContext = true
|
||||
|
||||
req, _ = http.NewRequest("GET", "http://localhost/", nil)
|
||||
context.Set(req, "t", 1)
|
||||
|
||||
r.ServeHTTP(*res, req)
|
||||
if _, ok := context.GetOk(req, "t"); !ok {
|
||||
t.Error("Context should NOT have been cleared at end of request")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW
|
||||
func TestSubrouterHeader(t *testing.T) {
|
||||
expected := "func1 response"
|
||||
func1 := func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, expected)
|
||||
}
|
||||
func2 := func(http.ResponseWriter, *http.Request) {}
|
||||
|
||||
r := NewRouter()
|
||||
s := r.Headers("SomeSpecialHeader", "").Subrouter()
|
||||
s.HandleFunc("/", func1).Name("func1")
|
||||
r.HandleFunc("/", func2).Name("func2")
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/", nil)
|
||||
req.Header.Add("SomeSpecialHeader", "foo")
|
||||
match := new(RouteMatch)
|
||||
matched := r.Match(req, match)
|
||||
if !matched {
|
||||
t.Errorf("Should match request")
|
||||
}
|
||||
if match.Route.GetName() != "func1" {
|
||||
t.Errorf("Expecting func1 handler, got %s", match.Route.GetName())
|
||||
}
|
||||
resp := NewRecorder()
|
||||
match.Handler.ServeHTTP(resp, req)
|
||||
if resp.Body.String() != expected {
|
||||
t.Errorf("Expecting %q", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// mapToPairs converts a string map to a slice of string pairs
|
||||
func mapToPairs(m map[string]string) []string {
|
||||
var i int
|
||||
p := make([]string, len(m)*2)
|
||||
for k, v := range m {
|
||||
p[i] = k
|
||||
p[i+1] = v
|
||||
i += 2
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// stringMapEqual checks the equality of two string maps
|
||||
func stringMapEqual(m1, m2 map[string]string) bool {
|
||||
nil1 := m1 == nil
|
||||
nil2 := m2 == nil
|
||||
if nil1 != nil2 || len(m1) != len(m2) {
|
||||
return false
|
||||
}
|
||||
for k, v := range m1 {
|
||||
if v != m2[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// newRequest is a helper function to create a new request with a method and url
|
||||
func newRequest(method, url string) *http.Request {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return req
|
||||
}
|
758
third_party/github.com/gorilla/mux/old_test.go
vendored
Normal file
758
third_party/github.com/gorilla/mux/old_test.go
vendored
Normal file
@ -0,0 +1,758 @@
|
||||
// Old tests ported to Go1. This is a mess. Want to drop it one day.
|
||||
|
||||
// Copyright 2011 Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ResponseRecorder
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// ResponseRecorder is an implementation of http.ResponseWriter that
|
||||
// records its mutations for later inspection in tests.
|
||||
type ResponseRecorder struct {
|
||||
Code int // the HTTP response code from WriteHeader
|
||||
HeaderMap http.Header // the HTTP response headers
|
||||
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
|
||||
Flushed bool
|
||||
}
|
||||
|
||||
// NewRecorder returns an initialized ResponseRecorder.
|
||||
func NewRecorder() *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
HeaderMap: make(http.Header),
|
||||
Body: new(bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
|
||||
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
|
||||
const DefaultRemoteAddr = "1.2.3.4"
|
||||
|
||||
// Header returns the response headers.
|
||||
func (rw *ResponseRecorder) Header() http.Header {
|
||||
return rw.HeaderMap
|
||||
}
|
||||
|
||||
// Write always succeeds and writes to rw.Body, if not nil.
|
||||
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||
if rw.Body != nil {
|
||||
rw.Body.Write(buf)
|
||||
}
|
||||
if rw.Code == 0 {
|
||||
rw.Code = http.StatusOK
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
// WriteHeader sets rw.Code.
|
||||
func (rw *ResponseRecorder) WriteHeader(code int) {
|
||||
rw.Code = code
|
||||
}
|
||||
|
||||
// Flush sets rw.Flushed to true.
|
||||
func (rw *ResponseRecorder) Flush() {
|
||||
rw.Flushed = true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestRouteMatchers(t *testing.T) {
|
||||
var scheme, host, path, query, method string
|
||||
var headers map[string]string
|
||||
var resultVars map[bool]map[string]string
|
||||
|
||||
router := NewRouter()
|
||||
router.NewRoute().Host("{var1}.google.com").
|
||||
Path("/{var2:[a-z]+}/{var3:[0-9]+}").
|
||||
Queries("foo", "bar").
|
||||
Methods("GET").
|
||||
Schemes("https").
|
||||
Headers("x-requested-with", "XMLHttpRequest")
|
||||
router.NewRoute().Host("www.{var4}.com").
|
||||
PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}").
|
||||
Queries("baz", "ding").
|
||||
Methods("POST").
|
||||
Schemes("http").
|
||||
Headers("Content-Type", "application/json")
|
||||
|
||||
reset := func() {
|
||||
// Everything match.
|
||||
scheme = "https"
|
||||
host = "www.google.com"
|
||||
path = "/product/42"
|
||||
query = "?foo=bar"
|
||||
method = "GET"
|
||||
headers = map[string]string{"X-Requested-With": "XMLHttpRequest"}
|
||||
resultVars = map[bool]map[string]string{
|
||||
true: map[string]string{"var1": "www", "var2": "product", "var3": "42"},
|
||||
false: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
reset2 := func() {
|
||||
// Everything match.
|
||||
scheme = "http"
|
||||
host = "www.google.com"
|
||||
path = "/foo/product/42/path/that/is/ignored"
|
||||
query = "?baz=ding"
|
||||
method = "POST"
|
||||
headers = map[string]string{"Content-Type": "application/json"}
|
||||
resultVars = map[bool]map[string]string{
|
||||
true: map[string]string{"var4": "google", "var5": "product", "var6": "42"},
|
||||
false: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
match := func(shouldMatch bool) {
|
||||
url := scheme + "://" + host + path + query
|
||||
request, _ := http.NewRequest(method, url, nil)
|
||||
for key, value := range headers {
|
||||
request.Header.Add(key, value)
|
||||
}
|
||||
|
||||
var routeMatch RouteMatch
|
||||
matched := router.Match(request, &routeMatch)
|
||||
if matched != shouldMatch {
|
||||
// Need better messages. :)
|
||||
if matched {
|
||||
t.Errorf("Should match.")
|
||||
} else {
|
||||
t.Errorf("Should not match.")
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
currentRoute := routeMatch.Route
|
||||
if currentRoute == nil {
|
||||
t.Errorf("Expected a current route.")
|
||||
}
|
||||
vars := routeMatch.Vars
|
||||
expectedVars := resultVars[shouldMatch]
|
||||
if len(vars) != len(expectedVars) {
|
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
|
||||
}
|
||||
for name, value := range vars {
|
||||
if expectedVars[name] != value {
|
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1st route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset()
|
||||
match(true)
|
||||
|
||||
// Scheme doesn't match.
|
||||
reset()
|
||||
scheme = "http"
|
||||
match(false)
|
||||
|
||||
// Host doesn't match.
|
||||
reset()
|
||||
host = "www.mygoogle.com"
|
||||
match(false)
|
||||
|
||||
// Path doesn't match.
|
||||
reset()
|
||||
path = "/product/notdigits"
|
||||
match(false)
|
||||
|
||||
// Query doesn't match.
|
||||
reset()
|
||||
query = "?foo=baz"
|
||||
match(false)
|
||||
|
||||
// Method doesn't match.
|
||||
reset()
|
||||
method = "POST"
|
||||
match(false)
|
||||
|
||||
// Header doesn't match.
|
||||
reset()
|
||||
headers = map[string]string{}
|
||||
match(false)
|
||||
|
||||
// Everything match, again.
|
||||
reset()
|
||||
match(true)
|
||||
|
||||
// 2nd route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset2()
|
||||
match(true)
|
||||
|
||||
// Scheme doesn't match.
|
||||
reset2()
|
||||
scheme = "https"
|
||||
match(false)
|
||||
|
||||
// Host doesn't match.
|
||||
reset2()
|
||||
host = "sub.google.com"
|
||||
match(false)
|
||||
|
||||
// Path doesn't match.
|
||||
reset2()
|
||||
path = "/bar/product/42"
|
||||
match(false)
|
||||
|
||||
// Query doesn't match.
|
||||
reset2()
|
||||
query = "?foo=baz"
|
||||
match(false)
|
||||
|
||||
// Method doesn't match.
|
||||
reset2()
|
||||
method = "GET"
|
||||
match(false)
|
||||
|
||||
// Header doesn't match.
|
||||
reset2()
|
||||
headers = map[string]string{}
|
||||
match(false)
|
||||
|
||||
// Everything match, again.
|
||||
reset2()
|
||||
match(true)
|
||||
}
|
||||
|
||||
type headerMatcherTest struct {
|
||||
matcher headerMatcher
|
||||
headers map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var headerMatcherTests = []headerMatcherTest{
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
|
||||
headers: map[string]string{"X-Requested-With": "XMLHttpRequest"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": ""}),
|
||||
headers: map[string]string{"X-Requested-With": "anything"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}),
|
||||
headers: map[string]string{},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type hostMatcherTest struct {
|
||||
matcher *Route
|
||||
url string
|
||||
vars map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var hostMatcherTests = []hostMatcherTest{
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
|
||||
url: "http://abc.def.ghi/",
|
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"),
|
||||
url: "http://a.b.c/",
|
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type methodMatcherTest struct {
|
||||
matcher methodMatcher
|
||||
method string
|
||||
result bool
|
||||
}
|
||||
|
||||
var methodMatcherTests = []methodMatcherTest{
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "GET",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "POST",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "PUT",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}),
|
||||
method: "DELETE",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type pathMatcherTest struct {
|
||||
matcher *Route
|
||||
url string
|
||||
vars map[string]string
|
||||
result bool
|
||||
}
|
||||
|
||||
var pathMatcherTests = []pathMatcherTest{
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
|
||||
url: "http://localhost:8080/123/456/789",
|
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"),
|
||||
url: "http://localhost:8080/1/2/3",
|
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type queryMatcherTest struct {
|
||||
matcher queryMatcher
|
||||
url string
|
||||
result bool
|
||||
}
|
||||
|
||||
var queryMatcherTests = []queryMatcherTest{
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}),
|
||||
url: "http://localhost:8080/?foo=anything&baz=anything",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type schemeMatcherTest struct {
|
||||
matcher schemeMatcher
|
||||
url string
|
||||
result bool
|
||||
}
|
||||
|
||||
var schemeMatcherTests = []schemeMatcherTest{
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http", "https"}),
|
||||
url: "http://localhost:8080/",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http", "https"}),
|
||||
url: "https://localhost:8080/",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"https"}),
|
||||
url: "http://localhost:8080/",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
matcher: schemeMatcher([]string{"http"}),
|
||||
url: "https://localhost:8080/",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type urlBuildingTest struct {
|
||||
route *Route
|
||||
vars []string
|
||||
url string
|
||||
}
|
||||
|
||||
var urlBuildingTests = []urlBuildingTest{
|
||||
{
|
||||
route: new(Route).Host("foo.domain.com"),
|
||||
vars: []string{},
|
||||
url: "http://foo.domain.com",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("{subdomain}.domain.com"),
|
||||
vars: []string{"subdomain", "bar"},
|
||||
url: "http://bar.domain.com",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("foo.domain.com").Path("/articles"),
|
||||
vars: []string{},
|
||||
url: "http://foo.domain.com/articles",
|
||||
},
|
||||
{
|
||||
route: new(Route).Path("/articles"),
|
||||
vars: []string{},
|
||||
url: "/articles",
|
||||
},
|
||||
{
|
||||
route: new(Route).Path("/articles/{category}/{id:[0-9]+}"),
|
||||
vars: []string{"category", "technology", "id", "42"},
|
||||
url: "/articles/technology/42",
|
||||
},
|
||||
{
|
||||
route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"),
|
||||
vars: []string{"subdomain", "foo", "category", "technology", "id", "42"},
|
||||
url: "http://foo.domain.com/articles/technology/42",
|
||||
},
|
||||
}
|
||||
|
||||
func TestHeaderMatcher(t *testing.T) {
|
||||
for _, v := range headerMatcherTests {
|
||||
request, _ := http.NewRequest("GET", "http://localhost:8080/", nil)
|
||||
for key, value := range v.headers {
|
||||
request.Header.Add(key, value)
|
||||
}
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, request.Header)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, request.Header)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMatcher(t *testing.T) {
|
||||
for _, v := range hostMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
if result {
|
||||
if len(vars) != len(v.vars) {
|
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
|
||||
}
|
||||
for name, value := range vars {
|
||||
if v.vars[name] != value {
|
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(vars) != 0 {
|
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodMatcher(t *testing.T) {
|
||||
for _, v := range methodMatcherTests {
|
||||
request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.method)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.method)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathMatcher(t *testing.T) {
|
||||
for _, v := range pathMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
if result {
|
||||
if len(vars) != len(v.vars) {
|
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars))
|
||||
}
|
||||
for name, value := range vars {
|
||||
if v.vars[name] != value {
|
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(vars) != 0 {
|
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryMatcher(t *testing.T) {
|
||||
for _, v := range queryMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemeMatcher(t *testing.T) {
|
||||
for _, v := range queryMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlBuilding(t *testing.T) {
|
||||
|
||||
for _, v := range urlBuildingTests {
|
||||
u, _ := v.route.URL(v.vars...)
|
||||
url := u.String()
|
||||
if url != v.url {
|
||||
t.Errorf("expected %v, got %v", v.url, url)
|
||||
/*
|
||||
reversePath := ""
|
||||
reverseHost := ""
|
||||
if v.route.pathTemplate != nil {
|
||||
reversePath = v.route.pathTemplate.Reverse
|
||||
}
|
||||
if v.route.hostTemplate != nil {
|
||||
reverseHost = v.route.hostTemplate.Reverse
|
||||
}
|
||||
|
||||
t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
ArticleHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
router := NewRouter()
|
||||
router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article")
|
||||
|
||||
url, _ := router.Get("article").URL("category", "technology", "id", "42")
|
||||
expected := "/articles/technology/42"
|
||||
if url.String() != expected {
|
||||
t.Errorf("Expected %v, got %v", expected, url.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchedRouteName(t *testing.T) {
|
||||
routeName := "stock"
|
||||
router := NewRouter()
|
||||
route := router.NewRoute().Path("/products/").Name(routeName)
|
||||
|
||||
url := "http://www.domain.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
|
||||
if !ok || rv.Route != route {
|
||||
t.Errorf("Expected same route, got %+v.", rv.Route)
|
||||
}
|
||||
|
||||
retName := rv.Route.GetName()
|
||||
if retName != routeName {
|
||||
t.Errorf("Expected %q, got %q.", routeName, retName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubRouting(t *testing.T) {
|
||||
// Example from docs.
|
||||
router := NewRouter()
|
||||
subrouter := router.NewRoute().Host("www.domain.com").Subrouter()
|
||||
route := subrouter.NewRoute().Path("/products/").Name("products")
|
||||
|
||||
url := "http://www.domain.com/products/"
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
var rv RouteMatch
|
||||
ok := router.Match(request, &rv)
|
||||
|
||||
if !ok || rv.Route != route {
|
||||
t.Errorf("Expected same route, got %+v.", rv.Route)
|
||||
}
|
||||
|
||||
u, _ := router.Get("products").URL()
|
||||
builtUrl := u.String()
|
||||
// Yay, subroute aware of the domain when building!
|
||||
if builtUrl != url {
|
||||
t.Errorf("Expected %q, got %q.", url, builtUrl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariableNames(t *testing.T) {
|
||||
route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}")
|
||||
if route.err == nil {
|
||||
t.Errorf("Expected error for duplicated variable names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectSlash(t *testing.T) {
|
||||
var route *Route
|
||||
var routeMatch RouteMatch
|
||||
r := NewRouter()
|
||||
|
||||
r.StrictSlash(false)
|
||||
route = r.NewRoute()
|
||||
if route.strictSlash != false {
|
||||
t.Errorf("Expected false redirectSlash.")
|
||||
}
|
||||
|
||||
r.StrictSlash(true)
|
||||
route = r.NewRoute()
|
||||
if route.strictSlash != true {
|
||||
t.Errorf("Expected true redirectSlash.")
|
||||
}
|
||||
|
||||
route = new(Route)
|
||||
route.strictSlash = true
|
||||
route.Path("/{arg1}/{arg2:[0-9]+}/")
|
||||
request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil)
|
||||
routeMatch = RouteMatch{}
|
||||
_ = route.Match(request, &routeMatch)
|
||||
vars := routeMatch.Vars
|
||||
if vars["arg1"] != "foo" {
|
||||
t.Errorf("Expected foo.")
|
||||
}
|
||||
if vars["arg2"] != "123" {
|
||||
t.Errorf("Expected 123.")
|
||||
}
|
||||
rsp := NewRecorder()
|
||||
routeMatch.Handler.ServeHTTP(rsp, request)
|
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" {
|
||||
t.Errorf("Expected redirect header.")
|
||||
}
|
||||
|
||||
route = new(Route)
|
||||
route.strictSlash = true
|
||||
route.Path("/{arg1}/{arg2:[0-9]+}")
|
||||
request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil)
|
||||
routeMatch = RouteMatch{}
|
||||
_ = route.Match(request, &routeMatch)
|
||||
vars = routeMatch.Vars
|
||||
if vars["arg1"] != "foo" {
|
||||
t.Errorf("Expected foo.")
|
||||
}
|
||||
if vars["arg2"] != "123" {
|
||||
t.Errorf("Expected 123.")
|
||||
}
|
||||
rsp = NewRecorder()
|
||||
routeMatch.Handler.ServeHTTP(rsp, request)
|
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" {
|
||||
t.Errorf("Expected redirect header.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for the new regexp library, still not available in stable Go.
|
||||
func TestNewRegexp(t *testing.T) {
|
||||
var p *routeRegexp
|
||||
var matches []string
|
||||
|
||||
tests := map[string]map[string][]string{
|
||||
"/{foo:a{2}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": nil,
|
||||
"/aaaa": nil,
|
||||
},
|
||||
"/{foo:a{2,}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": {"aaa"},
|
||||
"/aaaa": {"aaaa"},
|
||||
},
|
||||
"/{foo:a{2,3}}": {
|
||||
"/a": nil,
|
||||
"/aa": {"aa"},
|
||||
"/aaa": {"aaa"},
|
||||
"/aaaa": nil,
|
||||
},
|
||||
"/{foo:[a-z]{3}}/{bar:[a-z]{2}}": {
|
||||
"/a": nil,
|
||||
"/ab": nil,
|
||||
"/abc": nil,
|
||||
"/abcd": nil,
|
||||
"/abc/ab": {"abc", "ab"},
|
||||
"/abc/abc": nil,
|
||||
"/abcd/ab": nil,
|
||||
},
|
||||
`/{foo:\w{3,}}/{bar:\d{2,}}`: {
|
||||
"/a": nil,
|
||||
"/ab": nil,
|
||||
"/abc": nil,
|
||||
"/abc/1": nil,
|
||||
"/abc/12": {"abc", "12"},
|
||||
"/abcd/12": {"abcd", "12"},
|
||||
"/abcd/123": {"abcd", "123"},
|
||||
},
|
||||
}
|
||||
|
||||
for pattern, paths := range tests {
|
||||
p, _ = newRouteRegexp(pattern, false, false, false)
|
||||
for path, result := range paths {
|
||||
matches = p.regexp.FindStringSubmatch(path)
|
||||
if result == nil {
|
||||
if matches != nil {
|
||||
t.Errorf("%v should not match %v.", pattern, path)
|
||||
}
|
||||
} else {
|
||||
if len(matches) != len(result)+1 {
|
||||
t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches))
|
||||
} else {
|
||||
for k, v := range result {
|
||||
if matches[k+1] != v {
|
||||
t.Errorf("Expected %v, got %v.", v, matches[k+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
247
third_party/github.com/gorilla/mux/regexp.go
vendored
Normal file
247
third_party/github.com/gorilla/mux/regexp.go
vendored
Normal file
@ -0,0 +1,247 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||
// used to match a host or path.
|
||||
//
|
||||
// It will extract named variables, assemble a regexp to be matched, create
|
||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||
// values used in URL building.
|
||||
//
|
||||
// Previously we accepted only Python-like identifiers for variable
|
||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||
// name and pattern can't be empty, and names can't contain a colon.
|
||||
func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) {
|
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(tpl)
|
||||
if errBraces != nil {
|
||||
return nil, errBraces
|
||||
}
|
||||
// Backup the original.
|
||||
template := tpl
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if matchHost {
|
||||
defaultPattern = "[^.]+"
|
||||
matchPrefix, strictSlash = false, false
|
||||
}
|
||||
if matchPrefix {
|
||||
strictSlash = false
|
||||
}
|
||||
// Set a flag for strictSlash.
|
||||
endSlash := false
|
||||
if strictSlash && strings.HasSuffix(tpl, "/") {
|
||||
tpl = tpl[:len(tpl)-1]
|
||||
endSlash = true
|
||||
}
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("^")
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
for i := 0; i < len(idxs); i += 2 {
|
||||
// Set all values we are interested in.
|
||||
raw := tpl[end:idxs[i]]
|
||||
end = idxs[i+1]
|
||||
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
|
||||
name := parts[0]
|
||||
patt := defaultPattern
|
||||
if len(parts) == 2 {
|
||||
patt = parts[1]
|
||||
}
|
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" {
|
||||
return nil, fmt.Errorf("mux: missing name or pattern in %q",
|
||||
tpl[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
|
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
||||
// Append variable name and compiled pattern.
|
||||
varsN[i/2] = name
|
||||
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Add the remaining.
|
||||
raw := tpl[end:]
|
||||
pattern.WriteString(regexp.QuoteMeta(raw))
|
||||
if strictSlash {
|
||||
pattern.WriteString("[/]?")
|
||||
}
|
||||
if !matchPrefix {
|
||||
pattern.WriteByte('$')
|
||||
}
|
||||
reverse.WriteString(raw)
|
||||
if endSlash {
|
||||
reverse.WriteByte('/')
|
||||
}
|
||||
// Compile full regexp.
|
||||
reg, errCompile := regexp.Compile(pattern.String())
|
||||
if errCompile != nil {
|
||||
return nil, errCompile
|
||||
}
|
||||
// Done!
|
||||
return &routeRegexp{
|
||||
template: template,
|
||||
matchHost: matchHost,
|
||||
regexp: reg,
|
||||
reverse: reverse.String(),
|
||||
varsN: varsN,
|
||||
varsR: varsR,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// routeRegexp stores a regexp to match a host or path and information to
|
||||
// collect and validate route variables.
|
||||
type routeRegexp struct {
|
||||
// The unmodified template.
|
||||
template string
|
||||
// True for host match, false for path match.
|
||||
matchHost bool
|
||||
// Expanded regexp.
|
||||
regexp *regexp.Regexp
|
||||
// Reverse template.
|
||||
reverse string
|
||||
// Variable names.
|
||||
varsN []string
|
||||
// Variable regexps (validators).
|
||||
varsR []*regexp.Regexp
|
||||
}
|
||||
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if !r.matchHost {
|
||||
return r.regexp.MatchString(req.URL.Path)
|
||||
}
|
||||
return r.regexp.MatchString(getHost(req))
|
||||
}
|
||||
|
||||
// url builds a URL part using the given values.
|
||||
func (r *routeRegexp) url(pairs ...string) (string, error) {
|
||||
values, err := mapFromPairs(pairs...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
urlValues := make([]interface{}, len(r.varsN))
|
||||
for k, v := range r.varsN {
|
||||
value, ok := values[v]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("mux: missing route variable %q", v)
|
||||
}
|
||||
urlValues[k] = value
|
||||
}
|
||||
rv := fmt.Sprintf(r.reverse, urlValues...)
|
||||
if !r.regexp.MatchString(rv) {
|
||||
// The URL is checked against the full regexp, instead of checking
|
||||
// individual variables. This is faster but to provide a good error
|
||||
// message, we check individual regexps if the URL doesn't match.
|
||||
for k, v := range r.varsN {
|
||||
if !r.varsR[k].MatchString(values[v]) {
|
||||
return "", fmt.Errorf(
|
||||
"mux: variable %q doesn't match, expected %q", values[v],
|
||||
r.varsR[k].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
var level, idx int
|
||||
idxs := make([]int, 0)
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
if level++; level == 1 {
|
||||
idx = i
|
||||
}
|
||||
case '}':
|
||||
if level--; level == 0 {
|
||||
idxs = append(idxs, idx, i+1)
|
||||
} else if level < 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if level != 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct {
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
}
|
||||
|
||||
// setMatch extracts the variables from the URL once a route matches.
|
||||
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
|
||||
// Store host variables.
|
||||
if v.host != nil {
|
||||
hostVars := v.host.regexp.FindStringSubmatch(getHost(req))
|
||||
if hostVars != nil {
|
||||
for k, v := range v.host.varsN {
|
||||
m.Vars[v] = hostVars[k+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store path variables.
|
||||
if v.path != nil {
|
||||
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path)
|
||||
if pathVars != nil {
|
||||
for k, v := range v.path.varsN {
|
||||
m.Vars[v] = pathVars[k+1]
|
||||
}
|
||||
// Check if we should redirect.
|
||||
if r.strictSlash {
|
||||
p1 := strings.HasSuffix(req.URL.Path, "/")
|
||||
p2 := strings.HasSuffix(v.path.template, "/")
|
||||
if p1 != p2 {
|
||||
u, _ := url.Parse(req.URL.String())
|
||||
if p1 {
|
||||
u.Path = u.Path[:len(u.Path)-1]
|
||||
} else {
|
||||
u.Path += "/"
|
||||
}
|
||||
m.Handler = http.RedirectHandler(u.String(), 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getHost tries its best to return the request host.
|
||||
func getHost(r *http.Request) string {
|
||||
if !r.URL.IsAbs() {
|
||||
host := r.Host
|
||||
// Slice off any port information.
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
return host
|
||||
}
|
||||
return r.URL.Host
|
||||
}
|
499
third_party/github.com/gorilla/mux/route.go
vendored
Normal file
499
third_party/github.com/gorilla/mux/route.go
vendored
Normal file
@ -0,0 +1,499 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Route stores information to match a request and build URLs.
|
||||
type Route struct {
|
||||
// Parent where the route was registered (a Router).
|
||||
parent parentRoute
|
||||
// Request handler for the route.
|
||||
handler http.Handler
|
||||
// List of matchers.
|
||||
matchers []matcher
|
||||
// Manager for the variables from host and path.
|
||||
regexp *routeRegexpGroup
|
||||
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||
// redirect to the former and vice versa.
|
||||
strictSlash bool
|
||||
// If true, this route never matches: it is only used to build URLs.
|
||||
buildOnly bool
|
||||
// The name used to build URLs.
|
||||
name string
|
||||
// Error resulted from building a route.
|
||||
err error
|
||||
}
|
||||
|
||||
// Match matches the route against the request.
|
||||
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.buildOnly || r.err != nil {
|
||||
return false
|
||||
}
|
||||
// Match everything.
|
||||
for _, m := range r.matchers {
|
||||
if matched := m.Match(req, match); !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Yay, we have a match. Let's collect some info about it.
|
||||
if match.Route == nil {
|
||||
match.Route = r
|
||||
}
|
||||
if match.Handler == nil {
|
||||
match.Handler = r.handler
|
||||
}
|
||||
if match.Vars == nil {
|
||||
match.Vars = make(map[string]string)
|
||||
}
|
||||
// Set variables.
|
||||
if r.regexp != nil {
|
||||
r.regexp.setMatch(req, match, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route attributes
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetError returns an error resulted from building the route, if any.
|
||||
func (r *Route) GetError() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||
func (r *Route) BuildOnly() *Route {
|
||||
r.buildOnly = true
|
||||
return r
|
||||
}
|
||||
|
||||
// Handler --------------------------------------------------------------------
|
||||
|
||||
// Handler sets a handler for the route.
|
||||
func (r *Route) Handler(handler http.Handler) *Route {
|
||||
if r.err == nil {
|
||||
r.handler = handler
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// HandlerFunc sets a handler function for the route.
|
||||
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
|
||||
return r.Handler(http.HandlerFunc(f))
|
||||
}
|
||||
|
||||
// GetHandler returns the handler for the route, if any.
|
||||
func (r *Route) GetHandler() http.Handler {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
// Name -----------------------------------------------------------------------
|
||||
|
||||
// Name sets the name for the route, used to build URLs.
|
||||
// If the name was registered already it will be overwritten.
|
||||
func (r *Route) Name(name string) *Route {
|
||||
if r.name != "" {
|
||||
r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
|
||||
r.name, name)
|
||||
}
|
||||
if r.err == nil {
|
||||
r.name = name
|
||||
r.getNamedRoutes()[name] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetName returns the name for the route, if any.
|
||||
func (r *Route) GetName() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matchers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// matcher types try to match a request.
|
||||
type matcher interface {
|
||||
Match(*http.Request, *RouteMatch) bool
|
||||
}
|
||||
|
||||
// addMatcher adds a matcher to the route.
|
||||
func (r *Route) addMatcher(m matcher) *Route {
|
||||
if r.err == nil {
|
||||
r.matchers = append(r.matchers, m)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
r.regexp = r.getRegexpGroup()
|
||||
if !matchHost {
|
||||
if len(tpl) == 0 || tpl[0] != '/' {
|
||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
||||
}
|
||||
}
|
||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matchHost {
|
||||
if r.regexp.path != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.host = rr
|
||||
} else {
|
||||
if r.regexp.host != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.path = rr
|
||||
}
|
||||
r.addMatcher(rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers --------------------------------------------------------------------
|
||||
|
||||
// headerMatcher matches the request against header values.
|
||||
type headerMatcher map[string]string
|
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMap(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Headers("Content-Type", "application/json",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both request header values match.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]string
|
||||
headers, r.err = mapFromPairs(pairs...)
|
||||
return r.addMatcher(headerMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to me matched:
|
||||
//
|
||||
// - {name} matches anything until the next dot.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("www.domain.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, true, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// MatcherFunc ----------------------------------------------------------------
|
||||
|
||||
// MatcherFunc is the function signature used by custom matchers.
|
||||
type MatcherFunc func(*http.Request, *RouteMatch) bool
|
||||
|
||||
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return m(r, match)
|
||||
}
|
||||
|
||||
// MatcherFunc adds a custom function to be used as request matcher.
|
||||
func (r *Route) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.addMatcher(f)
|
||||
}
|
||||
|
||||
// Methods --------------------------------------------------------------------
|
||||
|
||||
// methodMatcher matches the request against HTTP methods.
|
||||
type methodMatcher []string
|
||||
|
||||
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.Method)
|
||||
}
|
||||
|
||||
// Methods adds a matcher for HTTP methods.
|
||||
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||
// "GET", "POST", "PUT".
|
||||
func (r *Route) Methods(methods ...string) *Route {
|
||||
for k, v := range methods {
|
||||
methods[k] = strings.ToUpper(v)
|
||||
}
|
||||
return r.addMatcher(methodMatcher(methods))
|
||||
}
|
||||
|
||||
// Path -----------------------------------------------------------------------
|
||||
|
||||
// Path adds a matcher for the URL path.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to me matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Path("/products/").Handler(ProductsHandler)
|
||||
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||
// Handler(ArticleHandler)
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, false, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// PathPrefix -----------------------------------------------------------------
|
||||
|
||||
// PathPrefix adds a matcher for the URL path prefix.
|
||||
func (r *Route) PathPrefix(tpl string) *Route {
|
||||
r.strictSlash = false
|
||||
r.err = r.addRegexpMatcher(tpl, false, true)
|
||||
return r
|
||||
}
|
||||
|
||||
// Query ----------------------------------------------------------------------
|
||||
|
||||
// queryMatcher matches the request against URL queries.
|
||||
type queryMatcher map[string]string
|
||||
|
||||
func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMap(m, r.URL.Query(), false)
|
||||
}
|
||||
|
||||
// Queries adds a matcher for URL query values.
|
||||
// It accepts a sequence of key/value pairs. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Queries("foo", "bar", "baz", "ding")
|
||||
//
|
||||
// The above route will only match if the URL contains the defined queries
|
||||
// values, e.g.: ?foo=bar&baz=ding.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Queries(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var queries map[string]string
|
||||
queries, r.err = mapFromPairs(pairs...)
|
||||
return r.addMatcher(queryMatcher(queries))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Schemes --------------------------------------------------------------------
|
||||
|
||||
// schemeMatcher matches the request against URL schemes.
|
||||
type schemeMatcher []string
|
||||
|
||||
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.URL.Scheme)
|
||||
}
|
||||
|
||||
// Schemes adds a matcher for URL schemes.
|
||||
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||
func (r *Route) Schemes(schemes ...string) *Route {
|
||||
for k, v := range schemes {
|
||||
schemes[k] = strings.ToLower(v)
|
||||
}
|
||||
return r.addMatcher(schemeMatcher(schemes))
|
||||
}
|
||||
|
||||
// Subrouter ------------------------------------------------------------------
|
||||
|
||||
// Subrouter creates a subrouter for the route.
|
||||
//
|
||||
// It will test the inner routes only if the parent route matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// s := r.Host("www.domain.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
//
|
||||
// Here, the routes registered in the subrouter won't be tested if the host
|
||||
// doesn't match.
|
||||
func (r *Route) Subrouter() *Router {
|
||||
router := &Router{parent: r, strictSlash: r.strictSlash}
|
||||
r.addMatcher(router)
|
||||
return router
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// URL building
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// URL builds a URL for the route.
|
||||
//
|
||||
// It accepts a sequence of key/value pairs for the route variables. For
|
||||
// example, given this route:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// ...a URL for it can be built using:
|
||||
//
|
||||
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
//
|
||||
// ...which will return an url.URL with the following path:
|
||||
//
|
||||
// "/articles/technology/42"
|
||||
//
|
||||
// This also works for host variables:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("{subdomain}.domain.com").
|
||||
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
// url, err := r.Get("article").URL("subdomain", "news",
|
||||
// "category", "technology",
|
||||
// "id", "42")
|
||||
//
|
||||
// All variables defined in the route are required, and their values must
|
||||
// conform to the corresponding patterns.
|
||||
func (r *Route) URL(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host or path")
|
||||
}
|
||||
var scheme, host, path string
|
||||
var err error
|
||||
if r.regexp.host != nil {
|
||||
// Set a default scheme.
|
||||
scheme = "http"
|
||||
if host, err = r.regexp.host.url(pairs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
if path, err = r.regexp.path.url(pairs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a host defined.
|
||||
func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.host == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
host, err := r.regexp.host.url(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a path defined.
|
||||
func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp == nil || r.regexp.path == nil {
|
||||
return nil, errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
path, err := r.regexp.path.url(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// parentRoute allows routes to know about parent host and path definitions.
|
||||
type parentRoute interface {
|
||||
getNamedRoutes() map[string]*Route
|
||||
getRegexpGroup() *routeRegexpGroup
|
||||
}
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Route) getNamedRoutes() map[string]*Route {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
return r.parent.getNamedRoutes()
|
||||
}
|
||||
|
||||
// getRegexpGroup returns regexp definitions from this route.
|
||||
func (r *Route) getRegexpGroup() *routeRegexpGroup {
|
||||
if r.regexp == nil {
|
||||
if r.parent == nil {
|
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter()
|
||||
}
|
||||
regexp := r.parent.getRegexpGroup()
|
||||
if regexp == nil {
|
||||
r.regexp = new(routeRegexpGroup)
|
||||
} else {
|
||||
// Copy.
|
||||
r.regexp = &routeRegexpGroup{
|
||||
host: regexp.host,
|
||||
path: regexp.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.regexp
|
||||
}
|
187
util.go
187
util.go
@ -1,121 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-log/log"
|
||||
"github.com/coreos/go-raft"
|
||||
"github.com/coreos/etcd/log"
|
||||
)
|
||||
|
||||
//--------------------------------------
|
||||
// etcd http Helper
|
||||
//--------------------------------------
|
||||
|
||||
// Convert string duration to time format
|
||||
func durationToExpireTime(strDuration string) (time.Time, error) {
|
||||
if strDuration != "" {
|
||||
duration, err := strconv.Atoi(strDuration)
|
||||
|
||||
if err != nil {
|
||||
return store.Permanent, err
|
||||
}
|
||||
return time.Now().Add(time.Second * (time.Duration)(duration)), nil
|
||||
|
||||
} else {
|
||||
return store.Permanent, nil
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// HTTP Utilities
|
||||
//--------------------------------------
|
||||
|
||||
func (r *raftServer) dispatch(c Command, w http.ResponseWriter, req *http.Request, toURL func(name string) (string, bool)) error {
|
||||
if r.State() == raft.Leader {
|
||||
if response, err := r.Do(c); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if response == nil {
|
||||
return etcdErr.NewError(300, "Empty response from raft", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
|
||||
event, ok := response.(*store.Event)
|
||||
if ok {
|
||||
bytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(event.Index))
|
||||
w.Header().Add("X-Etcd-Term", fmt.Sprint(event.Term))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bytes, _ := response.([]byte)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
leader := r.Leader()
|
||||
// current no leader
|
||||
if leader == "" {
|
||||
return etcdErr.NewError(300, "", store.UndefIndex, store.UndefTerm)
|
||||
}
|
||||
url, _ := toURL(leader)
|
||||
|
||||
redirect(url, w, req)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func redirect(hostname string, w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
|
||||
url := hostname + path
|
||||
|
||||
debugf("Redirect to %s", url)
|
||||
|
||||
http.Redirect(w, req, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func decodeJsonRequest(req *http.Request, data interface{}) error {
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
if err := decoder.Decode(&data); err != nil && err != io.EOF {
|
||||
warnf("Malformed json request: %v", err)
|
||||
return fmt.Errorf("Malformed json request: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeJsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if data != nil {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeURL will cleanup a host string in the format hostname:port and
|
||||
// attach a schema.
|
||||
func sanitizeURL(host string, defaultScheme string) string {
|
||||
@ -126,13 +24,13 @@ func sanitizeURL(host string, defaultScheme string) string {
|
||||
|
||||
p, err := url.Parse(host)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Make sure the host is in Host:Port format
|
||||
_, _, err = net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
p = &url.URL{Host: host, Scheme: defaultScheme}
|
||||
@ -145,12 +43,12 @@ func sanitizeURL(host string, defaultScheme string) string {
|
||||
func sanitizeListenHost(listen string, advertised string) string {
|
||||
aurl, err := url.Parse(advertised)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ahost, aport, err := net.SplitHostPort(aurl.Host)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// If the listen host isn't set use the advertised host
|
||||
@ -163,54 +61,10 @@ func sanitizeListenHost(listen string, advertised string) string {
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getNodePath(urlPath string) string {
|
||||
pathPrefixLen := len("/" + version + "/keys")
|
||||
return urlPath[pathPrefixLen:]
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// Log
|
||||
//--------------------------------------
|
||||
|
||||
var logger *log.Logger = log.New("etcd", false,
|
||||
log.CombinedSink(os.Stdout, "[%s] %s %-9s | %s\n", []string{"prefix", "time", "priority", "message"}))
|
||||
|
||||
func infof(format string, v ...interface{}) {
|
||||
logger.Infof(format, v...)
|
||||
}
|
||||
|
||||
func debugf(format string, v ...interface{}) {
|
||||
if verbose {
|
||||
logger.Debugf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func debug(v ...interface{}) {
|
||||
if verbose {
|
||||
logger.Debug(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func warnf(format string, v ...interface{}) {
|
||||
logger.Warningf(format, v...)
|
||||
}
|
||||
|
||||
func warn(v ...interface{}) {
|
||||
logger.Warning(v...)
|
||||
}
|
||||
|
||||
func fatalf(format string, v ...interface{}) {
|
||||
logger.Fatalf(format, v...)
|
||||
}
|
||||
|
||||
func fatal(v ...interface{}) {
|
||||
logger.Fatalln(v...)
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// CPU profile
|
||||
//--------------------------------------
|
||||
@ -218,7 +72,7 @@ func runCPUProfile() {
|
||||
|
||||
f, err := os.Create(cpuprofile)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
|
||||
@ -226,34 +80,9 @@ func runCPUProfile() {
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
infof("captured %v, stopping profiler and exiting..", sig)
|
||||
log.Infof("captured %v, stopping profiler and exiting..", sig)
|
||||
pprof.StopCPUProfile()
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
// Testing
|
||||
//--------------------------------------
|
||||
func directSet() {
|
||||
c := make(chan bool, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
go send(c)
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
<-c
|
||||
}
|
||||
}
|
||||
|
||||
func send(c chan bool) {
|
||||
for i := 0; i < 10; i++ {
|
||||
command := &UpdateCommand{}
|
||||
command.Key = "foo"
|
||||
command.Value = "bar"
|
||||
command.ExpireTime = time.Unix(0, 0)
|
||||
//r.Do(command)
|
||||
}
|
||||
c <- true
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user