*: remove unused pkgs
This commit is contained in:
@ -1,37 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
// ClusterConfig represents cluster-wide configuration settings.
|
|
||||||
type ClusterConfig struct {
|
|
||||||
// ActiveSize is the maximum number of node that can join as Raft followers.
|
|
||||||
// Nodes that join the cluster after the limit is reached are standbys.
|
|
||||||
ActiveSize int `json:"activeSize"`
|
|
||||||
|
|
||||||
// RemoveDelay is the amount of time, in seconds, after a node is
|
|
||||||
// unreachable that it will be swapped out as a standby node.
|
|
||||||
RemoveDelay float64 `json:"removeDelay"`
|
|
||||||
|
|
||||||
// SyncInterval is the amount of time, in seconds, between
|
|
||||||
// cluster sync when it runs in standby mode.
|
|
||||||
SyncInterval float64 `json:"syncInterval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClusterConfig returns a cluster configuration with default settings.
|
|
||||||
func NewClusterConfig() *ClusterConfig {
|
|
||||||
return &ClusterConfig{
|
|
||||||
ActiveSize: DefaultActiveSize,
|
|
||||||
RemoveDelay: DefaultRemoveDelay,
|
|
||||||
SyncInterval: DefaultSyncInterval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClusterConfig) Sanitize() {
|
|
||||||
if c.ActiveSize < MinActiveSize {
|
|
||||||
c.ActiveSize = MinActiveSize
|
|
||||||
}
|
|
||||||
if c.RemoveDelay < MinRemoveDelay {
|
|
||||||
c.RemoveDelay = MinRemoveDelay
|
|
||||||
}
|
|
||||||
if c.SyncInterval < MinSyncInterval {
|
|
||||||
c.SyncInterval = MinSyncInterval
|
|
||||||
}
|
|
||||||
}
|
|
426
conf/config.go
426
conf/config.go
@ -1,426 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The default location for the etcd configuration file.
|
|
||||||
const DefaultSystemConfigPath = "/etc/etcd/etcd.conf"
|
|
||||||
|
|
||||||
// Config represents the server configuration.
|
|
||||||
type Config struct {
|
|
||||||
SystemPath string
|
|
||||||
|
|
||||||
Addr string `env:"ETCD_ADDR"`
|
|
||||||
BindAddr string `env:"ETCD_BIND_ADDR"`
|
|
||||||
CAFile string `env:"ETCD_CA_FILE"`
|
|
||||||
CertFile string `env:"ETCD_CERT_FILE"`
|
|
||||||
CPUProfileFile string
|
|
||||||
CorsOrigins []string `env:"ETCD_CORS"`
|
|
||||||
DataDir string `env:"ETCD_DATA_DIR"`
|
|
||||||
Discovery string `env:"ETCD_DISCOVERY"`
|
|
||||||
Force bool
|
|
||||||
KeyFile string `env:"ETCD_KEY_FILE"`
|
|
||||||
HTTPReadTimeout float64 `env:"ETCD_HTTP_READ_TIMEOUT"`
|
|
||||||
HTTPWriteTimeout float64 `env:"ETCD_HTTP_WRITE_TIMEOUT"`
|
|
||||||
Peers []string `env:"ETCD_PEERS"`
|
|
||||||
PeersFile string `env:"ETCD_PEERS_FILE"`
|
|
||||||
MaxResultBuffer int `env:"ETCD_MAX_RESULT_BUFFER"`
|
|
||||||
MaxRetryAttempts int `env:"ETCD_MAX_RETRY_ATTEMPTS"`
|
|
||||||
RetryInterval float64 `env:"ETCD_RETRY_INTERVAL"`
|
|
||||||
Name string `env:"ETCD_NAME"`
|
|
||||||
Snapshot bool `env:"ETCD_SNAPSHOT"`
|
|
||||||
SnapshotCount int `env:"ETCD_SNAPSHOTCOUNT"`
|
|
||||||
ShowHelp bool
|
|
||||||
ShowVersion bool
|
|
||||||
Verbose bool `env:"ETCD_VERBOSE"`
|
|
||||||
VeryVerbose bool `env:"ETCD_VERY_VERBOSE"`
|
|
||||||
VeryVeryVerbose bool `env:"ETCD_VERY_VERY_VERBOSE"`
|
|
||||||
Peer struct {
|
|
||||||
Addr string `env:"ETCD_PEER_ADDR"`
|
|
||||||
BindAddr string `env:"ETCD_PEER_BIND_ADDR"`
|
|
||||||
CAFile string `env:"ETCD_PEER_CA_FILE"`
|
|
||||||
CertFile string `env:"ETCD_PEER_CERT_FILE"`
|
|
||||||
KeyFile string `env:"ETCD_PEER_KEY_FILE"`
|
|
||||||
HeartbeatInterval int `env:"ETCD_PEER_HEARTBEAT_INTERVAL"`
|
|
||||||
ElectionTimeout int `env:"ETCD_PEER_ELECTION_TIMEOUT"`
|
|
||||||
}
|
|
||||||
strTrace string `env:"ETCD_TRACE"`
|
|
||||||
GraphiteHost string `env:"ETCD_GRAPHITE_HOST"`
|
|
||||||
Cluster struct {
|
|
||||||
ActiveSize int `env:"ETCD_CLUSTER_ACTIVE_SIZE"`
|
|
||||||
RemoveDelay float64 `env:"ETCD_CLUSTER_REMOVE_DELAY"`
|
|
||||||
SyncInterval float64 `env:"ETCD_CLUSTER_SYNC_INTERVAL"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a Config initialized with default values.
|
|
||||||
func New() *Config {
|
|
||||||
c := new(Config)
|
|
||||||
c.SystemPath = DefaultSystemConfigPath
|
|
||||||
c.Addr = "127.0.0.1:4001"
|
|
||||||
c.HTTPReadTimeout = DefaultReadTimeout
|
|
||||||
c.HTTPWriteTimeout = DefaultWriteTimeout
|
|
||||||
c.MaxResultBuffer = 1024
|
|
||||||
c.MaxRetryAttempts = 3
|
|
||||||
c.RetryInterval = 10.0
|
|
||||||
c.Snapshot = true
|
|
||||||
c.SnapshotCount = 10000
|
|
||||||
c.Peer.Addr = "127.0.0.1:7001"
|
|
||||||
c.Peer.HeartbeatInterval = DefaultHeartbeatInterval
|
|
||||||
c.Peer.ElectionTimeout = DefaultElectionTimeout
|
|
||||||
rand.Seed(time.Now().UTC().UnixNano())
|
|
||||||
// Make maximum twice as minimum.
|
|
||||||
c.RetryInterval = float64(50+rand.Int()%50) * DefaultHeartbeatInterval / 1000
|
|
||||||
c.Cluster.ActiveSize = DefaultActiveSize
|
|
||||||
c.Cluster.RemoveDelay = DefaultRemoveDelay
|
|
||||||
c.Cluster.SyncInterval = DefaultSyncInterval
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads the configuration from the system config, command line config,
|
|
||||||
// environment variables, and finally command line arguments.
|
|
||||||
func (c *Config) Load(arguments []string) error {
|
|
||||||
var path string
|
|
||||||
f := flag.NewFlagSet("etcd", -1)
|
|
||||||
f.SetOutput(ioutil.Discard)
|
|
||||||
f.StringVar(&path, "config", "", "path to config file")
|
|
||||||
f.Parse(arguments)
|
|
||||||
|
|
||||||
// Load from the environment variables next.
|
|
||||||
if err := c.LoadEnv(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from command line flags.
|
|
||||||
if err := c.LoadFlags(arguments); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads peers if a peer file was specified.
|
|
||||||
if err := c.LoadPeersFile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadEnv loads the configuration via environment variables.
|
|
||||||
func (c *Config) LoadEnv() error {
|
|
||||||
if err := c.loadEnv(c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.loadEnv(&c.Peer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.loadEnv(&c.Cluster); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) loadEnv(target interface{}) error {
|
|
||||||
value := reflect.Indirect(reflect.ValueOf(target))
|
|
||||||
typ := value.Type()
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
|
||||||
field := typ.Field(i)
|
|
||||||
|
|
||||||
// Retrieve environment variable.
|
|
||||||
v := strings.TrimSpace(os.Getenv(field.Tag.Get("env")))
|
|
||||||
if v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the appropriate type.
|
|
||||||
switch field.Type.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
value.Field(i).SetBool(v != "0" && v != "false")
|
|
||||||
case reflect.Int:
|
|
||||||
newValue, err := strconv.ParseInt(v, 10, 0)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Parse error: %s: %s", field.Tag.Get("env"), err)
|
|
||||||
}
|
|
||||||
value.Field(i).SetInt(newValue)
|
|
||||||
case reflect.String:
|
|
||||||
value.Field(i).SetString(v)
|
|
||||||
case reflect.Slice:
|
|
||||||
value.Field(i).Set(reflect.ValueOf(trimSplit(v, ",")))
|
|
||||||
case reflect.Float64:
|
|
||||||
newValue, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Parse error: %s: %s", field.Tag.Get("env"), err)
|
|
||||||
}
|
|
||||||
value.Field(i).SetFloat(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads configuration from command line flags.
|
|
||||||
func (c *Config) LoadFlags(arguments []string) error {
|
|
||||||
var peers, cors, path string
|
|
||||||
|
|
||||||
f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
||||||
f.SetOutput(ioutil.Discard)
|
|
||||||
|
|
||||||
f.BoolVar(&c.ShowHelp, "h", false, "")
|
|
||||||
f.BoolVar(&c.ShowHelp, "help", false, "")
|
|
||||||
f.BoolVar(&c.ShowVersion, "version", false, "")
|
|
||||||
|
|
||||||
f.BoolVar(&c.Force, "f", false, "")
|
|
||||||
f.BoolVar(&c.Force, "force", false, "")
|
|
||||||
|
|
||||||
f.BoolVar(&c.Verbose, "v", c.Verbose, "")
|
|
||||||
f.BoolVar(&c.VeryVerbose, "vv", c.VeryVerbose, "")
|
|
||||||
f.BoolVar(&c.VeryVeryVerbose, "vvv", c.VeryVeryVerbose, "")
|
|
||||||
|
|
||||||
f.StringVar(&peers, "peers", "", "")
|
|
||||||
f.StringVar(&c.PeersFile, "peers-file", c.PeersFile, "")
|
|
||||||
|
|
||||||
f.StringVar(&c.Name, "name", c.Name, "")
|
|
||||||
f.StringVar(&c.Addr, "addr", c.Addr, "")
|
|
||||||
f.StringVar(&c.Discovery, "discovery", c.Discovery, "")
|
|
||||||
f.StringVar(&c.BindAddr, "bind-addr", c.BindAddr, "")
|
|
||||||
f.StringVar(&c.Peer.Addr, "peer-addr", c.Peer.Addr, "")
|
|
||||||
f.StringVar(&c.Peer.BindAddr, "peer-bind-addr", c.Peer.BindAddr, "")
|
|
||||||
|
|
||||||
f.StringVar(&c.CAFile, "ca-file", c.CAFile, "")
|
|
||||||
f.StringVar(&c.CertFile, "cert-file", c.CertFile, "")
|
|
||||||
f.StringVar(&c.KeyFile, "key-file", c.KeyFile, "")
|
|
||||||
|
|
||||||
f.StringVar(&c.Peer.CAFile, "peer-ca-file", c.Peer.CAFile, "")
|
|
||||||
f.StringVar(&c.Peer.CertFile, "peer-cert-file", c.Peer.CertFile, "")
|
|
||||||
f.StringVar(&c.Peer.KeyFile, "peer-key-file", c.Peer.KeyFile, "")
|
|
||||||
|
|
||||||
f.Float64Var(&c.HTTPReadTimeout, "http-read-timeout", c.HTTPReadTimeout, "")
|
|
||||||
f.Float64Var(&c.HTTPWriteTimeout, "http-write-timeout", c.HTTPReadTimeout, "")
|
|
||||||
|
|
||||||
f.StringVar(&c.DataDir, "data-dir", c.DataDir, "")
|
|
||||||
f.IntVar(&c.MaxResultBuffer, "max-result-buffer", c.MaxResultBuffer, "")
|
|
||||||
f.IntVar(&c.MaxRetryAttempts, "max-retry-attempts", c.MaxRetryAttempts, "")
|
|
||||||
f.Float64Var(&c.RetryInterval, "retry-interval", c.RetryInterval, "")
|
|
||||||
f.IntVar(&c.Peer.HeartbeatInterval, "peer-heartbeat-interval", c.Peer.HeartbeatInterval, "")
|
|
||||||
f.IntVar(&c.Peer.ElectionTimeout, "peer-election-timeout", c.Peer.ElectionTimeout, "")
|
|
||||||
|
|
||||||
f.StringVar(&cors, "cors", "", "")
|
|
||||||
|
|
||||||
f.BoolVar(&c.Snapshot, "snapshot", c.Snapshot, "")
|
|
||||||
f.IntVar(&c.SnapshotCount, "snapshot-count", c.SnapshotCount, "")
|
|
||||||
f.StringVar(&c.CPUProfileFile, "cpuprofile", "", "")
|
|
||||||
|
|
||||||
f.StringVar(&c.strTrace, "trace", "", "")
|
|
||||||
f.StringVar(&c.GraphiteHost, "graphite-host", "", "")
|
|
||||||
|
|
||||||
f.IntVar(&c.Cluster.ActiveSize, "cluster-active-size", c.Cluster.ActiveSize, "")
|
|
||||||
f.Float64Var(&c.Cluster.RemoveDelay, "cluster-remove-delay", c.Cluster.RemoveDelay, "")
|
|
||||||
f.Float64Var(&c.Cluster.SyncInterval, "cluster-sync-interval", c.Cluster.SyncInterval, "")
|
|
||||||
|
|
||||||
// BEGIN IGNORED FLAGS
|
|
||||||
f.StringVar(&path, "config", "", "")
|
|
||||||
// BEGIN IGNORED FLAGS
|
|
||||||
|
|
||||||
if err := f.Parse(arguments); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert some parameters to lists.
|
|
||||||
if peers != "" {
|
|
||||||
c.Peers = trimSplit(peers, ",")
|
|
||||||
}
|
|
||||||
if cors != "" {
|
|
||||||
c.CorsOrigins = trimSplit(cors, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPeersFile loads the peers listed in the peers file.
|
|
||||||
func (c *Config) LoadPeersFile() error {
|
|
||||||
if c.PeersFile == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(c.PeersFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Peers file error: %s", err)
|
|
||||||
}
|
|
||||||
c.Peers = trimSplit(string(b), ",")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataDirFromName sets the data dir from a machine name and issue a warning
|
|
||||||
// that etcd is guessing.
|
|
||||||
func (c *Config) DataDirFromName() {
|
|
||||||
c.DataDir = c.Name + ".etcd"
|
|
||||||
log.Printf("Using the directory %s as the etcd curation directory because a directory was not specified. ", c.DataDir)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NameFromHostname sets the machine name from the hostname. This is to help
|
|
||||||
// people get started without thinking up a name.
|
|
||||||
func (c *Config) NameFromHostname() {
|
|
||||||
host, err := os.Hostname()
|
|
||||||
if err != nil && host == "" {
|
|
||||||
log.Fatal("Node name required and hostname not set. e.g. '-name=name'")
|
|
||||||
}
|
|
||||||
c.Name = host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset removes all server configuration files.
|
|
||||||
func (c *Config) Reset() error {
|
|
||||||
if err := os.RemoveAll(filepath.Join(c.DataDir, "log")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(filepath.Join(c.DataDir, "conf")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(filepath.Join(c.DataDir, "snapshot")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(filepath.Join(c.DataDir, "standby_info")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize cleans the input fields.
|
|
||||||
func (c *Config) Sanitize() error {
|
|
||||||
var err error
|
|
||||||
var url *url.URL
|
|
||||||
|
|
||||||
// Sanitize the URLs first.
|
|
||||||
if c.Addr, url, err = sanitizeURL(c.Addr, c.EtcdTLSInfo().Scheme()); err != nil {
|
|
||||||
return fmt.Errorf("Advertised URL: %s", err)
|
|
||||||
}
|
|
||||||
if c.BindAddr, err = sanitizeBindAddr(c.BindAddr, url); err != nil {
|
|
||||||
return fmt.Errorf("Listen Host: %s", err)
|
|
||||||
}
|
|
||||||
if c.Peer.Addr, url, err = sanitizeURL(c.Peer.Addr, c.PeerTLSInfo().Scheme()); err != nil {
|
|
||||||
return fmt.Errorf("Peer Advertised URL: %s", err)
|
|
||||||
}
|
|
||||||
if c.Peer.BindAddr, err = sanitizeBindAddr(c.Peer.BindAddr, url); err != nil {
|
|
||||||
return fmt.Errorf("Peer Listen Host: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only guess the machine name if there is no data dir specified
|
|
||||||
// because the info file should have our name
|
|
||||||
if c.Name == "" && c.DataDir == "" {
|
|
||||||
c.NameFromHostname()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.DataDir == "" && c.Name != "" && !c.ShowVersion && !c.ShowHelp {
|
|
||||||
c.DataDirFromName()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EtcdTLSInfo retrieves a TLSInfo object for the etcd server
|
|
||||||
func (c *Config) EtcdTLSInfo() *TLSInfo {
|
|
||||||
return &TLSInfo{
|
|
||||||
CAFile: c.CAFile,
|
|
||||||
CertFile: c.CertFile,
|
|
||||||
KeyFile: c.KeyFile,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeerRaftInfo retrieves a TLSInfo object for the peer server.
|
|
||||||
func (c *Config) PeerTLSInfo() *TLSInfo {
|
|
||||||
return &TLSInfo{
|
|
||||||
CAFile: c.Peer.CAFile,
|
|
||||||
CertFile: c.Peer.CertFile,
|
|
||||||
KeyFile: c.Peer.KeyFile,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetricsBucketName generates the name that should be used for a
|
|
||||||
// corresponding MetricsBucket object
|
|
||||||
func (c *Config) MetricsBucketName() string {
|
|
||||||
return fmt.Sprintf("etcd.%s", c.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trace determines if any trace-level information should be emitted
|
|
||||||
func (c *Config) Trace() bool {
|
|
||||||
return c.strTrace == "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ClusterConfig() *ClusterConfig {
|
|
||||||
return &ClusterConfig{
|
|
||||||
ActiveSize: c.Cluster.ActiveSize,
|
|
||||||
RemoveDelay: c.Cluster.RemoveDelay,
|
|
||||||
SyncInterval: c.Cluster.SyncInterval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeURL will cleanup a host string in the format hostname[:port] and
|
|
||||||
// attach a schema.
|
|
||||||
func sanitizeURL(host string, defaultScheme string) (string, *url.URL, error) {
|
|
||||||
// Blank URLs are fine input, just return it
|
|
||||||
if len(host) == 0 {
|
|
||||||
return host, &url.URL{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := url.Parse(host)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the host is in Host:Port format
|
|
||||||
_, _, err = net.SplitHostPort(host)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p = &url.URL{Host: host, Scheme: defaultScheme}
|
|
||||||
return p.String(), p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeBindAddr cleans up the BindAddr parameter and appends a port
|
|
||||||
// if necessary based on the advertised port.
|
|
||||||
func sanitizeBindAddr(bindAddr string, aurl *url.URL) (string, error) {
|
|
||||||
// If it is a valid host:port simply return with no further checks.
|
|
||||||
bhost, bport, err := net.SplitHostPort(bindAddr)
|
|
||||||
if err == nil && bhost != "" {
|
|
||||||
return bindAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitHostPort makes the host optional, but we don't want that.
|
|
||||||
if bhost == "" && bport != "" {
|
|
||||||
return "", fmt.Errorf("IP required can't use a port only")
|
|
||||||
}
|
|
||||||
|
|
||||||
// bindAddr doesn't have a port if we reach here so take the port from the
|
|
||||||
// advertised URL.
|
|
||||||
_, aport, err := net.SplitHostPort(aurl.Host)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return net.JoinHostPort(bindAddr, aport), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// trimSplit slices s into all substrings separated by sep and returns a
|
|
||||||
// slice of the substrings between the separator with all leading and trailing
|
|
||||||
// white space removed, as defined by Unicode.
|
|
||||||
func trimSplit(s, sep string) []string {
|
|
||||||
trimmed := strings.Split(s, sep)
|
|
||||||
for i := range trimmed {
|
|
||||||
trimmed[i] = strings.TrimSpace(trimmed[i])
|
|
||||||
}
|
|
||||||
return trimmed
|
|
||||||
}
|
|
@ -1,572 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensures that a configuration can be retrieved from environment variables.
|
|
||||||
func TestConfigEnv(t *testing.T) {
|
|
||||||
os.Setenv("ETCD_CA_FILE", "/tmp/file.ca")
|
|
||||||
os.Setenv("ETCD_CERT_FILE", "/tmp/file.cert")
|
|
||||||
os.Setenv("ETCD_CPU_PROFILE_FILE", "XXX")
|
|
||||||
os.Setenv("ETCD_CORS", "localhost:4001,localhost:4002")
|
|
||||||
os.Setenv("ETCD_DATA_DIR", "/tmp/data")
|
|
||||||
os.Setenv("ETCD_DISCOVERY", "http://example.com/foobar")
|
|
||||||
os.Setenv("ETCD_HTTP_READ_TIMEOUT", "2.34")
|
|
||||||
os.Setenv("ETCD_HTTP_WRITE_TIMEOUT", "1.23")
|
|
||||||
os.Setenv("ETCD_KEY_FILE", "/tmp/file.key")
|
|
||||||
os.Setenv("ETCD_BIND_ADDR", "127.0.0.1:4003")
|
|
||||||
os.Setenv("ETCD_PEERS", "coreos.com:4001,coreos.com:4002")
|
|
||||||
os.Setenv("ETCD_PEERS_FILE", "/tmp/peers")
|
|
||||||
os.Setenv("ETCD_MAX_CLUSTER_SIZE", "10")
|
|
||||||
os.Setenv("ETCD_MAX_RESULT_BUFFER", "512")
|
|
||||||
os.Setenv("ETCD_MAX_RETRY_ATTEMPTS", "5")
|
|
||||||
os.Setenv("ETCD_NAME", "test-name")
|
|
||||||
os.Setenv("ETCD_SNAPSHOT", "true")
|
|
||||||
os.Setenv("ETCD_VERBOSE", "1")
|
|
||||||
os.Setenv("ETCD_VERY_VERBOSE", "yes")
|
|
||||||
os.Setenv("ETCD_PEER_ADDR", "127.0.0.1:7002")
|
|
||||||
os.Setenv("ETCD_PEER_CA_FILE", "/tmp/peer/file.ca")
|
|
||||||
os.Setenv("ETCD_PEER_CERT_FILE", "/tmp/peer/file.cert")
|
|
||||||
os.Setenv("ETCD_PEER_KEY_FILE", "/tmp/peer/file.key")
|
|
||||||
os.Setenv("ETCD_PEER_BIND_ADDR", "127.0.0.1:7003")
|
|
||||||
os.Setenv("ETCD_CLUSTER_ACTIVE_SIZE", "5")
|
|
||||||
os.Setenv("ETCD_CLUSTER_REMOVE_DELAY", "100")
|
|
||||||
os.Setenv("ETCD_CLUSTER_SYNC_INTERVAL", "10")
|
|
||||||
|
|
||||||
c := New()
|
|
||||||
c.LoadEnv()
|
|
||||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
|
||||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
|
||||||
assert.Equal(t, c.CorsOrigins, []string{"localhost:4001", "localhost:4002"}, "")
|
|
||||||
assert.Equal(t, c.DataDir, "/tmp/data", "")
|
|
||||||
assert.Equal(t, c.Discovery, "http://example.com/foobar", "")
|
|
||||||
assert.Equal(t, c.HTTPReadTimeout, 2.34, "")
|
|
||||||
assert.Equal(t, c.HTTPWriteTimeout, 1.23, "")
|
|
||||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
|
||||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
|
||||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
|
||||||
assert.Equal(t, c.PeersFile, "/tmp/peers", "")
|
|
||||||
assert.Equal(t, c.MaxResultBuffer, 512, "")
|
|
||||||
assert.Equal(t, c.MaxRetryAttempts, 5, "")
|
|
||||||
assert.Equal(t, c.Name, "test-name", "")
|
|
||||||
assert.Equal(t, c.Snapshot, true, "")
|
|
||||||
assert.Equal(t, c.Verbose, true, "")
|
|
||||||
assert.Equal(t, c.VeryVerbose, true, "")
|
|
||||||
assert.Equal(t, c.Peer.Addr, "127.0.0.1:7002", "")
|
|
||||||
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
|
|
||||||
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
|
|
||||||
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
|
|
||||||
assert.Equal(t, c.Peer.BindAddr, "127.0.0.1:7003", "")
|
|
||||||
assert.Equal(t, c.Cluster.ActiveSize, 5, "")
|
|
||||||
assert.Equal(t, c.Cluster.RemoveDelay, 100.0, "")
|
|
||||||
assert.Equal(t, c.Cluster.SyncInterval, 10.0, "")
|
|
||||||
|
|
||||||
// Clear this as it will mess up other tests
|
|
||||||
os.Setenv("ETCD_DISCOVERY", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the "help" flag can be parsed.
|
|
||||||
func TestConfigHelpFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-help"}), "")
|
|
||||||
assert.True(t, c.ShowHelp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the abbreviated "help" flag can be parsed.
|
|
||||||
func TestConfigAbbreviatedHelpFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-h"}), "")
|
|
||||||
assert.True(t, c.ShowHelp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the "version" flag can be parsed.
|
|
||||||
func TestConfigVersionFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-version"}), "")
|
|
||||||
assert.True(t, c.ShowVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the "force config" flag can be parsed.
|
|
||||||
func TestConfigForceFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-force"}), "")
|
|
||||||
assert.True(t, c.Force)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the abbreviated "force config" flag can be parsed.
|
|
||||||
func TestConfigAbbreviatedForceFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-f"}), "")
|
|
||||||
assert.True(t, c.Force)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the advertised url can be parsed from the environment.
|
|
||||||
func TestConfigAddrEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_ADDR", "127.0.0.1:4002", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Addr, "127.0.0.1:4002", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the advertised flag can be parsed.
|
|
||||||
func TestConfigAddrFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4002"}), "")
|
|
||||||
assert.Equal(t, c.Addr, "127.0.0.1:4002", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the CA file can be parsed from the environment.
|
|
||||||
func TestConfigCAFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_CA_FILE", "/tmp/file.ca", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the CA file flag can be parsed.
|
|
||||||
func TestConfigCAFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-ca-file", "/tmp/file.ca"}), "")
|
|
||||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the CA file can be parsed from the environment.
|
|
||||||
func TestConfigCertFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_CERT_FILE", "/tmp/file.cert", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Cert file flag can be parsed.
|
|
||||||
func TestConfigCertFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-cert-file", "/tmp/file.cert"}), "")
|
|
||||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Key file can be parsed from the environment.
|
|
||||||
func TestConfigKeyFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_KEY_FILE", "/tmp/file.key", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Key file flag can be parsed.
|
|
||||||
func TestConfigKeyFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-key-file", "/tmp/file.key"}), "")
|
|
||||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host can be parsed from the environment.
|
|
||||||
func TestConfigBindAddrEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_BIND_ADDR", "127.0.0.1:4003", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host file flag can be parsed.
|
|
||||||
func TestConfigBindAddrFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-bind-addr", "127.0.0.1:4003"}), "")
|
|
||||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host port overrides the advertised port
|
|
||||||
func TestConfigBindAddrOverride(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", "127.0.0.1:4010"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4010", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host port overrides the advertised port
|
|
||||||
func TestConfigBindIPv6AddrOverride(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "[::1]:4009", "-bind-addr", "[::1]:4010"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "[::1]:4010", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host port overrides the advertised port
|
|
||||||
func TestConfigBindIPv6WithZoneAddrOverride(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "[::1%25lo]:4009", "-bind-addr", "[::1%25lo]:4010"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "[::1%25lo]:4010", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host inherits its port from the advertised addr
|
|
||||||
func TestConfigBindAddrInheritPort(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", "127.0.0.1"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4009", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host inherits its port from the advertised addr
|
|
||||||
func TestConfigBindIPv6AddrInheritPort(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "[::1]:4009", "-bind-addr", "::1"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "[::1]:4009", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Listen Host inherits its port from the advertised addr
|
|
||||||
func TestConfigBindIPv6WithZoneAddrInheritPort(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "[::1%25lo]:4009", "-bind-addr", "::1%25lo"}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
assert.Equal(t, c.BindAddr, "[::1%25lo]:4009", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a port only argument errors out
|
|
||||||
func TestConfigBindAddrErrorOnNoHost(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", ":4010"}), "")
|
|
||||||
assert.Error(t, c.Sanitize())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a bad IPv6 address will raise an error
|
|
||||||
func TestConfigBindAddrErrorOnBadIPv6Addr(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "[::1%lo]:4009"}), "")
|
|
||||||
assert.Error(t, c.Sanitize())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the peers can be parsed from the environment.
|
|
||||||
func TestConfigPeersEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEERS", "coreos.com:4001,coreos.com:4002", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peers flag can be parsed.
|
|
||||||
func TestConfigPeersFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peers", "coreos.com:4001,coreos.com:4002"}), "")
|
|
||||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peers File can be parsed from the environment.
|
|
||||||
func TestConfigPeersFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEERS_FILE", "/tmp/peers", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.PeersFile, "/tmp/peers", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peers File flag can be parsed.
|
|
||||||
func TestConfigPeersFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peers-file", "/tmp/peers"}), "")
|
|
||||||
assert.Equal(t, c.PeersFile, "/tmp/peers", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Max Result Buffer can be parsed from the environment.
|
|
||||||
func TestConfigMaxResultBufferEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_MAX_RESULT_BUFFER", "512", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.MaxResultBuffer, 512, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Max Result Buffer flag can be parsed.
|
|
||||||
func TestConfigMaxResultBufferFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-max-result-buffer", "512"}), "")
|
|
||||||
assert.Equal(t, c.MaxResultBuffer, 512, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Max Retry Attempts can be parsed from the environment.
|
|
||||||
func TestConfigMaxRetryAttemptsEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_MAX_RETRY_ATTEMPTS", "10", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.MaxRetryAttempts, 10, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Max Retry Attempts flag can be parsed.
|
|
||||||
func TestConfigMaxRetryAttemptsFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-max-retry-attempts", "10"}), "")
|
|
||||||
assert.Equal(t, c.MaxRetryAttempts, 10, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Name can be parsed from the environment.
|
|
||||||
func TestConfigNameEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_NAME", "test-name", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Name, "test-name", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Name flag can be parsed.
|
|
||||||
func TestConfigNameFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-name", "test-name"}), "")
|
|
||||||
assert.Equal(t, c.Name, "test-name", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a Name gets guessed if not specified
|
|
||||||
func TestConfigNameGuess(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
name, _ := os.Hostname()
|
|
||||||
assert.Equal(t, c.Name, name, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a DataDir gets guessed if not specified
|
|
||||||
func TestConfigDataDirGuess(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{}), "")
|
|
||||||
assert.Nil(t, c.Sanitize())
|
|
||||||
name, _ := os.Hostname()
|
|
||||||
assert.Equal(t, c.DataDir, name+".etcd", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that Snapshot can be parsed from the environment.
|
|
||||||
func TestConfigSnapshotEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_SNAPSHOT", "1", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Snapshot, true, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Snapshot flag can be parsed.
|
|
||||||
func TestConfigSnapshotFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-snapshot"}), "")
|
|
||||||
assert.Equal(t, c.Snapshot, true, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that Verbose can be parsed from the environment.
|
|
||||||
func TestConfigVerboseEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_VERBOSE", "true", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Verbose, true, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Verbose flag can be parsed.
|
|
||||||
func TestConfigVerboseFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-v"}), "")
|
|
||||||
assert.Equal(t, c.Verbose, true, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that Very Verbose can be parsed from the environment.
|
|
||||||
func TestConfigVeryVerboseEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_VERY_VERBOSE", "true", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.VeryVerbose, true, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Very Verbose flag can be parsed.
|
|
||||||
func TestConfigVeryVerboseFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-vv"}), "")
|
|
||||||
assert.Equal(t, c.VeryVerbose, true, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Advertised URL can be parsed from the environment.
|
|
||||||
func TestConfigPeerAddrEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEER_ADDR", "localhost:7002", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peer.Addr, "localhost:7002", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Advertised URL flag can be parsed.
|
|
||||||
func TestConfigPeerAddrFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peer-addr", "localhost:7002"}), "")
|
|
||||||
assert.Equal(t, c.Peer.Addr, "localhost:7002", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer CA File can be parsed from the environment.
|
|
||||||
func TestConfigPeerCAFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEER_CA_FILE", "/tmp/peer/file.ca", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer CA file flag can be parsed.
|
|
||||||
func TestConfigPeerCAFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peer-ca-file", "/tmp/peer/file.ca"}), "")
|
|
||||||
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Cert File can be parsed from the environment.
|
|
||||||
func TestConfigPeerCertFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEER_CERT_FILE", "/tmp/peer/file.cert", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Cert file flag can be parsed.
|
|
||||||
func TestConfigPeerCertFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peer-cert-file", "/tmp/peer/file.cert"}), "")
|
|
||||||
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Key File can be parsed from the environment.
|
|
||||||
func TestConfigPeerKeyFileEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEER_KEY_FILE", "/tmp/peer/file.key", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Key file flag can be parsed.
|
|
||||||
func TestConfigPeerKeyFileFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peer-key-file", "/tmp/peer/file.key"}), "")
|
|
||||||
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Listen Host can be parsed from the environment.
|
|
||||||
func TestConfigPeerBindAddrEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_PEER_BIND_ADDR", "localhost:7004", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Peer.BindAddr, "localhost:7004", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a bad flag returns an error.
|
|
||||||
func TestConfigBadFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
err := c.LoadFlags([]string{"-no-such-flag"})
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, err.Error(), `flag provided but not defined: -no-such-flag`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the Peer Listen Host file flag can be parsed.
|
|
||||||
func TestConfigPeerBindAddrFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-peer-bind-addr", "127.0.0.1:4003"}), "")
|
|
||||||
assert.Equal(t, c.Peer.BindAddr, "127.0.0.1:4003", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the cluster active size can be parsed from the environment.
|
|
||||||
func TestConfigClusterActiveSizeEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_CLUSTER_ACTIVE_SIZE", "5", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Cluster.ActiveSize, 5, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the cluster active size flag can be parsed.
|
|
||||||
func TestConfigClusterActiveSizeFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-cluster-active-size", "5"}), "")
|
|
||||||
assert.Equal(t, c.Cluster.ActiveSize, 5, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the cluster remove delay can be parsed from the environment.
|
|
||||||
func TestConfigClusterRemoveDelayEnv(t *testing.T) {
|
|
||||||
withEnv("ETCD_CLUSTER_REMOVE_DELAY", "100", func(c *Config) {
|
|
||||||
assert.Nil(t, c.LoadEnv(), "")
|
|
||||||
assert.Equal(t, c.Cluster.RemoveDelay, 100.0, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that the cluster remove delay flag can be parsed.
|
|
||||||
func TestConfigClusterRemoveDelayFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-cluster-remove-delay", "100"}), "")
|
|
||||||
assert.Equal(t, c.Cluster.RemoveDelay, 100.0, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigClusterSyncIntervalFlag(t *testing.T) {
|
|
||||||
c := New()
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-http-read-timeout", "2.34"}), "")
|
|
||||||
assert.Equal(t, c.HTTPReadTimeout, 2.34, "")
|
|
||||||
assert.Nil(t, c.LoadFlags([]string{"-http-write-timeout", "1.23"}), "")
|
|
||||||
assert.Equal(t, c.HTTPWriteTimeout, 1.23, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that a custom config field is overridden by an environment variable.
|
|
||||||
func TestConfigEnvVarOverrideCustomConfig(t *testing.T) {
|
|
||||||
os.Setenv("ETCD_PEER_ADDR", "127.0.0.1:8000")
|
|
||||||
defer os.Setenv("ETCD_PEER_ADDR", "")
|
|
||||||
|
|
||||||
custom := `[peer]` + "\n" + `advertised_url = "127.0.0.1:9000"`
|
|
||||||
withTempFile(custom, func(path string) {
|
|
||||||
c := New()
|
|
||||||
c.SystemPath = ""
|
|
||||||
assert.Nil(t, c.Load([]string{"-config", path}), "")
|
|
||||||
assert.Equal(t, c.Peer.Addr, "127.0.0.1:8000", "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that an environment variable field is overridden by a command line argument.
|
|
||||||
func TestConfigCLIArgsOverrideEnvVar(t *testing.T) {
|
|
||||||
os.Setenv("ETCD_ADDR", "127.0.0.1:1000")
|
|
||||||
defer os.Setenv("ETCD_ADDR", "")
|
|
||||||
|
|
||||||
c := New()
|
|
||||||
c.SystemPath = ""
|
|
||||||
assert.Nil(t, c.Load([]string{"-addr", "127.0.0.1:2000"}), "")
|
|
||||||
assert.Equal(t, c.Addr, "127.0.0.1:2000", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------
|
|
||||||
// Helpers
|
|
||||||
//--------------------------------------
|
|
||||||
|
|
||||||
// Sets up the environment with a given environment variable set.
|
|
||||||
func withEnv(key, value string, f func(c *Config)) {
|
|
||||||
os.Setenv(key, value)
|
|
||||||
defer os.Setenv(key, "")
|
|
||||||
c := New()
|
|
||||||
f(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a temp file and calls a function with the context.
|
|
||||||
func withTempFile(content string, fn func(string)) {
|
|
||||||
f, _ := ioutil.TempFile("", "")
|
|
||||||
f.WriteString(content)
|
|
||||||
f.Close()
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
fn(f.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Captures STDOUT & STDERR and returns the output as strings.
|
|
||||||
func capture(fn func()) (string, string) {
|
|
||||||
// Create temp files.
|
|
||||||
tmpout, _ := ioutil.TempFile("", "")
|
|
||||||
defer os.Remove(tmpout.Name())
|
|
||||||
tmperr, _ := ioutil.TempFile("", "")
|
|
||||||
defer os.Remove(tmperr.Name())
|
|
||||||
|
|
||||||
stdout, stderr := os.Stdout, os.Stderr
|
|
||||||
os.Stdout, os.Stderr = tmpout, tmperr
|
|
||||||
|
|
||||||
// Execute function argument and then reassign stdout/stderr.
|
|
||||||
fn()
|
|
||||||
os.Stdout, os.Stderr = stdout, stderr
|
|
||||||
|
|
||||||
// Close temp files and read them.
|
|
||||||
tmpout.Close()
|
|
||||||
bout, _ := ioutil.ReadFile(tmpout.Name())
|
|
||||||
tmperr.Close()
|
|
||||||
berr, _ := ioutil.ReadFile(tmperr.Name())
|
|
||||||
|
|
||||||
return string(bout), string(berr)
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// The amount of time (in ms) to elapse without a heartbeat before becoming a candidate
|
|
||||||
DefaultElectionTimeout = 200
|
|
||||||
|
|
||||||
// The frequency (in ms) by which heartbeats are sent to followers.
|
|
||||||
DefaultHeartbeatInterval = 50
|
|
||||||
|
|
||||||
// DefaultActiveSize is the default number of active followers allowed.
|
|
||||||
DefaultActiveSize = 9
|
|
||||||
|
|
||||||
// MinActiveSize is the minimum active size allowed.
|
|
||||||
MinActiveSize = 3
|
|
||||||
|
|
||||||
// DefaultRemoveDelay is the default elapsed time before removal.
|
|
||||||
DefaultRemoveDelay = float64((30 * time.Minute) / time.Second)
|
|
||||||
|
|
||||||
// MinRemoveDelay is the minimum remove delay allowed.
|
|
||||||
MinRemoveDelay = float64((2 * time.Second) / time.Second)
|
|
||||||
|
|
||||||
// DefaultSyncInterval is the default interval for cluster sync.
|
|
||||||
DefaultSyncInterval = float64((5 * time.Second) / time.Second)
|
|
||||||
|
|
||||||
// MinSyncInterval is the minimum sync interval allowed.
|
|
||||||
MinSyncInterval = float64((1 * time.Second) / time.Second)
|
|
||||||
|
|
||||||
DefaultReadTimeout = float64((5 * time.Minute) / time.Second)
|
|
||||||
DefaultWriteTimeout = float64((5 * time.Minute) / time.Second)
|
|
||||||
)
|
|
@ -1,105 +0,0 @@
|
|||||||
package conf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLSInfo holds the SSL certificates paths.
|
|
||||||
type TLSInfo struct {
|
|
||||||
CertFile string `json:"CertFile"`
|
|
||||||
KeyFile string `json:"KeyFile"`
|
|
||||||
CAFile string `json:"CAFile"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info TLSInfo) Scheme() string {
|
|
||||||
if info.KeyFile != "" && info.CertFile != "" {
|
|
||||||
return "https"
|
|
||||||
} else {
|
|
||||||
return "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a tls.Config object for a server from the given files.
|
|
||||||
func (info TLSInfo) ServerConfig() (*tls.Config, error) {
|
|
||||||
// Both the key and cert must be present.
|
|
||||||
if info.KeyFile == "" || info.CertFile == "" {
|
|
||||||
return nil, fmt.Errorf("KeyFile and CertFile must both be present[key: %v, cert: %v]", info.KeyFile, info.CertFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg tls.Config
|
|
||||||
|
|
||||||
tlsCert, err := tls.LoadX509KeyPair(info.CertFile, info.KeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Certificates = []tls.Certificate{tlsCert}
|
|
||||||
|
|
||||||
if info.CAFile != "" {
|
|
||||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
|
||||||
cp, err := newCertPool(info.CAFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.RootCAs = cp
|
|
||||||
cfg.ClientCAs = cp
|
|
||||||
} else {
|
|
||||||
cfg.ClientAuth = tls.NoClientCert
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a tls.Config object for a client from the given files.
|
|
||||||
func (info TLSInfo) ClientConfig() (*tls.Config, error) {
|
|
||||||
var cfg tls.Config
|
|
||||||
|
|
||||||
if info.KeyFile == "" || info.CertFile == "" {
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsCert, err := tls.LoadX509KeyPair(info.CertFile, info.KeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Certificates = []tls.Certificate{tlsCert}
|
|
||||||
|
|
||||||
if info.CAFile != "" {
|
|
||||||
cp, err := newCertPool(info.CAFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.RootCAs = cp
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newCertPool creates x509 certPool with provided CA file
|
|
||||||
func newCertPool(CAFile string) (*x509.CertPool, error) {
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
pemByte, err := ioutil.ReadFile(CAFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
var block *pem.Block
|
|
||||||
block, pemByte = pem.Decode(pemByte)
|
|
||||||
if block == nil {
|
|
||||||
return certPool, nil
|
|
||||||
}
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
certPool.AddCert(cert)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stateKey = "_state"
|
|
||||||
startedState = "started"
|
|
||||||
defaultTTL = 604800 // One week TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
type discoverer struct {
|
|
||||||
client *etcd.Client
|
|
||||||
name string
|
|
||||||
addr string
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDiscoverer(u *url.URL, name, raftPubAddr string) *discoverer {
|
|
||||||
d := &discoverer{name: name, addr: raftPubAddr}
|
|
||||||
|
|
||||||
// prefix is prepended to all keys for this discovery
|
|
||||||
d.prefix = strings.TrimPrefix(u.Path, "/v2/keys/")
|
|
||||||
|
|
||||||
// keep the old path in case we need to set the KeyPrefix below
|
|
||||||
oldPath := u.Path
|
|
||||||
u.Path = ""
|
|
||||||
|
|
||||||
// Connect to a scheme://host not a full URL with path
|
|
||||||
d.client = etcd.NewClient([]string{u.String()})
|
|
||||||
|
|
||||||
if !strings.HasPrefix(oldPath, "/v2/keys") {
|
|
||||||
d.client.SetKeyPrefix("")
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *discoverer) discover() ([]string, error) {
|
|
||||||
log.Printf("discoverer name=%s target=\"%q\" prefix=%s\n", d.name, d.client.GetCluster(), d.prefix)
|
|
||||||
|
|
||||||
if _, err := d.client.Set(path.Join(d.prefix, d.name), d.addr, defaultTTL); err != nil {
|
|
||||||
return nil, fmt.Errorf("discovery service error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to take the leadership role, if there is no error we are it!
|
|
||||||
resp, err := d.client.Create(path.Join(d.prefix, stateKey), startedState, 0)
|
|
||||||
// Bail out on unexpected errors
|
|
||||||
if err != nil {
|
|
||||||
if clientErr, ok := err.(*etcd.EtcdError); !ok || clientErr.ErrorCode != etcdErr.EcodeNodeExist {
|
|
||||||
return nil, fmt.Errorf("discovery service error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got a response then the CAS was successful, we are leader
|
|
||||||
if resp != nil && resp.Node.Value == startedState {
|
|
||||||
// We are the leader, we have no peers
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall through to finding the other discovery peers
|
|
||||||
return d.findPeers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *discoverer) findPeers() (peers []string, err error) {
|
|
||||||
resp, err := d.client.Get(path.Join(d.prefix), false, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("discovery service error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
node := resp.Node
|
|
||||||
|
|
||||||
if node == nil {
|
|
||||||
return nil, fmt.Errorf("%s key doesn't exist.", d.prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range node.Nodes {
|
|
||||||
// Skip our own entry in the list, there is no point
|
|
||||||
if strings.HasSuffix(n.Key, "/"+d.name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peers = append(peers, n.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(peers) == 0 {
|
|
||||||
return nil, errors.New("Discovery found an initialized cluster but no reachable peers are registered.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *discoverer) heartbeat(stopc <-chan struct{}) {
|
|
||||||
// In case of errors we should attempt to heartbeat fairly frequently
|
|
||||||
heartbeatInterval := defaultTTL / 8
|
|
||||||
ticker := time.NewTicker(time.Second * time.Duration(heartbeatInterval))
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
if _, err := d.client.Set(path.Join(d.prefix, d.name), d.addr, defaultTTL); err != nil {
|
|
||||||
log.Println("discoverer heartbeatErr=\"%v\"", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
case <-stopc:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,220 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
participantMode int64 = iota
|
|
||||||
standbyMode
|
|
||||||
stopMode
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
cfg *conf.Config
|
|
||||||
name string
|
|
||||||
pubAddr string
|
|
||||||
raftPubAddr string
|
|
||||||
tickDuration time.Duration
|
|
||||||
|
|
||||||
mode atomicInt
|
|
||||||
p *participant
|
|
||||||
s *standby
|
|
||||||
|
|
||||||
client *v2client
|
|
||||||
peerHub *peerHub
|
|
||||||
|
|
||||||
exited chan error
|
|
||||||
stopNotifyc chan struct{}
|
|
||||||
log *log.Logger
|
|
||||||
http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(c *conf.Config) (*Server, error) {
|
|
||||||
if err := c.Sanitize(); err != nil {
|
|
||||||
log.Fatalf("server.new sanitizeErr=\"%v\"\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tc := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
if c.PeerTLSInfo().Scheme() == "https" {
|
|
||||||
tc, err = c.PeerTLSInfo().ClientConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("server.new ClientConfigErr=\"%v\"\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := new(http.Transport)
|
|
||||||
tr.TLSClientConfig = tc
|
|
||||||
tr.Dial = (&net.Dialer{Timeout: 200 * time.Millisecond}).Dial
|
|
||||||
tr.TLSHandshakeTimeout = 10 * time.Second
|
|
||||||
tr.ResponseHeaderTimeout = defaultTickDuration * defaultHeartbeat
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
cfg: c,
|
|
||||||
name: c.Name,
|
|
||||||
pubAddr: c.Addr,
|
|
||||||
raftPubAddr: c.Peer.Addr,
|
|
||||||
tickDuration: defaultTickDuration,
|
|
||||||
|
|
||||||
mode: atomicInt(stopMode),
|
|
||||||
|
|
||||||
client: newClient(tc),
|
|
||||||
|
|
||||||
exited: make(chan error, 1),
|
|
||||||
stopNotifyc: make(chan struct{}),
|
|
||||||
}
|
|
||||||
followersStats := NewRaftFollowersStats(s.name)
|
|
||||||
s.peerHub = newPeerHub(client, followersStats)
|
|
||||||
m := http.NewServeMux()
|
|
||||||
m.HandleFunc("/", s.requestHandler)
|
|
||||||
m.HandleFunc("/version", versionHandler)
|
|
||||||
s.Handler = m
|
|
||||||
|
|
||||||
log.Printf("name=%s server.new raftPubAddr=%s\n", s.name, s.raftPubAddr)
|
|
||||||
if err = os.MkdirAll(s.cfg.DataDir, 0700); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) SetTick(tick time.Duration) {
|
|
||||||
s.tickDuration = tick
|
|
||||||
log.Printf("name=%s server.setTick tick=%q\n", s.name, s.tickDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the server elegently.
|
|
||||||
func (s *Server) Stop() error {
|
|
||||||
s.mode.Set(stopMode)
|
|
||||||
close(s.stopNotifyc)
|
|
||||||
err := <-s.exited
|
|
||||||
s.client.CloseConnections()
|
|
||||||
log.Printf("name=%s server.stop\n", s.name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) requestHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch s.mode.Get() {
|
|
||||||
case participantMode:
|
|
||||||
s.p.ServeHTTP(w, r)
|
|
||||||
case standbyMode:
|
|
||||||
s.s.ServeHTTP(w, r)
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) RaftHandler() http.Handler {
|
|
||||||
return http.HandlerFunc(s.ServeRaftHTTP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ServeRaftHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch s.mode.Get() {
|
|
||||||
case participantMode:
|
|
||||||
s.p.raftHandler().ServeHTTP(w, r)
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Run() error {
|
|
||||||
var d *discoverer
|
|
||||||
var seeds []string
|
|
||||||
var exit error
|
|
||||||
defer func() { s.exited <- exit }()
|
|
||||||
|
|
||||||
durl := s.cfg.Discovery
|
|
||||||
if durl != "" {
|
|
||||||
u, err := url.Parse(durl)
|
|
||||||
if err != nil {
|
|
||||||
exit = err
|
|
||||||
return fmt.Errorf("bad discovery URL error: %v", err)
|
|
||||||
}
|
|
||||||
d = newDiscoverer(u, s.name, s.raftPubAddr)
|
|
||||||
if seeds, err = d.discover(); err != nil {
|
|
||||||
exit = err
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("name=%s server.run source=-discovery seeds=%q", s.name, seeds)
|
|
||||||
} else {
|
|
||||||
for _, p := range s.cfg.Peers {
|
|
||||||
u, err := url.Parse(p)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("name=%s server.run err=%q", s.name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if u.Scheme == "" {
|
|
||||||
u.Scheme = s.cfg.PeerTLSInfo().Scheme()
|
|
||||||
}
|
|
||||||
seeds = append(seeds, u.String())
|
|
||||||
}
|
|
||||||
log.Printf("name=%s server.run source=-peers seeds=%q", s.name, seeds)
|
|
||||||
}
|
|
||||||
s.peerHub.setSeeds(seeds)
|
|
||||||
|
|
||||||
next := participantMode
|
|
||||||
for {
|
|
||||||
switch next {
|
|
||||||
case participantMode:
|
|
||||||
p, err := newParticipant(s.cfg, s.client, s.peerHub, s.tickDuration)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("name=%s server.run newParicipanteErr=\"%v\"\n", s.name, err)
|
|
||||||
exit = err
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.p = p
|
|
||||||
s.mode.Set(participantMode)
|
|
||||||
log.Printf("name=%s server.run mode=participantMode\n", s.name)
|
|
||||||
dStopc := make(chan struct{})
|
|
||||||
if d != nil {
|
|
||||||
go d.heartbeat(dStopc)
|
|
||||||
}
|
|
||||||
s.p.run(s.stopNotifyc)
|
|
||||||
if d != nil {
|
|
||||||
close(dStopc)
|
|
||||||
}
|
|
||||||
next = standbyMode
|
|
||||||
case standbyMode:
|
|
||||||
s.s = newStandby(s.client, s.peerHub)
|
|
||||||
s.mode.Set(standbyMode)
|
|
||||||
log.Printf("name=%s server.run mode=standbyMode\n", s.name)
|
|
||||||
s.s.run(s.stopNotifyc)
|
|
||||||
next = participantMode
|
|
||||||
default:
|
|
||||||
panic("unsupport mode")
|
|
||||||
}
|
|
||||||
if s.mode.Get() == stopMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKillLeader(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{3, 5, 9}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
cl := testCluster{Size: tt}
|
|
||||||
cl.Start()
|
|
||||||
for j := 0; j < tt; j++ {
|
|
||||||
lead, _ := cl.Leader()
|
|
||||||
cl.Node(lead).Stop()
|
|
||||||
// wait for leader election timeout
|
|
||||||
time.Sleep(cl.Node(0).e.tickDuration * defaultElection * 2)
|
|
||||||
if g, _ := cl.Leader(); g == lead {
|
|
||||||
t.Errorf("#%d.%d: lead = %d, want not %d", i, j, g, lead)
|
|
||||||
}
|
|
||||||
cl.Node(lead).Start()
|
|
||||||
cl.Node(lead).WaitMode(participantMode)
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKillRandom(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{3, 5, 9}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
cl := testCluster{Size: tt}
|
|
||||||
cl.Start()
|
|
||||||
for j := 0; j < tt; j++ {
|
|
||||||
// we cannot kill the majority
|
|
||||||
// wait for the majority
|
|
||||||
cl.Leader()
|
|
||||||
toKill := make(map[int64]struct{})
|
|
||||||
for len(toKill) != tt/2-1 {
|
|
||||||
toKill[rand.Int63n(int64(tt))] = struct{}{}
|
|
||||||
}
|
|
||||||
for k := range toKill {
|
|
||||||
cl.Node(int(k)).Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for leader election timeout
|
|
||||||
time.Sleep(cl.Node(0).e.tickDuration * defaultElection * 2)
|
|
||||||
cl.Leader()
|
|
||||||
for k := range toKill {
|
|
||||||
cl.Node(int(k)).Start()
|
|
||||||
cl.Node(int(k)).WaitMode(participantMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJoinThroughFollower(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{3, 5, 7}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
bt := &testServer{}
|
|
||||||
bt.Start()
|
|
||||||
cl := testCluster{nodes: []*testServer{bt}}
|
|
||||||
seed := bt.URL
|
|
||||||
|
|
||||||
for i := 1; i < tt; i++ {
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = fmt.Sprint(i)
|
|
||||||
c.Peers = []string{seed}
|
|
||||||
ts := &testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
cl.nodes = append(cl.nodes, ts)
|
|
||||||
cl.Leader()
|
|
||||||
seed = ts.URL
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJoinWithoutHTTPScheme(t *testing.T) {
|
|
||||||
bt := &testServer{}
|
|
||||||
bt.Start()
|
|
||||||
|
|
||||||
cl := testCluster{nodes: []*testServer{bt}}
|
|
||||||
seed := bt.URL
|
|
||||||
u, err := url.Parse(seed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// remove HTTP scheme
|
|
||||||
seed = u.Host + u.Path
|
|
||||||
|
|
||||||
for i := 1; i < 3; i++ {
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = "server-" + fmt.Sprint(i)
|
|
||||||
c.Peers = []string{seed}
|
|
||||||
ts := &testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
cl.nodes = append(cl.nodes, ts)
|
|
||||||
cl.Leader()
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClusterConfigReload(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := &testCluster{Size: 5}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
lead, _ := cl.Leader()
|
|
||||||
cc := conf.NewClusterConfig()
|
|
||||||
cc.ActiveSize = 15
|
|
||||||
cc.RemoveDelay = 60
|
|
||||||
if err := cl.Participant(lead).setClusterConfig(cc); err != nil {
|
|
||||||
t.Fatalf("setClusterConfig err = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Stop()
|
|
||||||
cl.Restart()
|
|
||||||
|
|
||||||
lead, _ = cl.Leader()
|
|
||||||
// wait for msgAppResp to commit all entries
|
|
||||||
time.Sleep(2 * defaultHeartbeat * cl.Participant(0).tickDuration)
|
|
||||||
if g := cl.Participant(lead).clusterConfig(); !reflect.DeepEqual(g, cc) {
|
|
||||||
t.Errorf("clusterConfig = %+v, want %+v", g, cc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFiveNodeKillOneAndRecover(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
cl := testCluster{Size: 5}
|
|
||||||
cl.Start()
|
|
||||||
for n := 0; n < 5; n++ {
|
|
||||||
i := rand.Int() % 5
|
|
||||||
cl.Node(i).Stop()
|
|
||||||
cl.Leader()
|
|
||||||
cl.Node(i).Start()
|
|
||||||
cl.Node(i).WaitMode(participantMode)
|
|
||||||
cl.Leader()
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFiveNodeKillAllAndRecover(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 5}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
cl.Leader()
|
|
||||||
c := etcd.NewClient([]string{cl.URL(0)})
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
if _, err := c.Set("foo", "bar", 0); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Stop()
|
|
||||||
|
|
||||||
cl.Restart()
|
|
||||||
cl.Leader()
|
|
||||||
res, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("set err after recovery: %v", err)
|
|
||||||
}
|
|
||||||
if g := res.Node.ModifiedIndex; g != 16 {
|
|
||||||
t.Errorf("modifiedIndex = %d, want %d", g, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestModeSwitch tests switch mode between standby and peer.
|
|
||||||
func TestModeSwitch(t *testing.T) {
|
|
||||||
t.Skip("not implemented")
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
bootstrapName = "BEEF"
|
|
||||||
)
|
|
||||||
|
|
||||||
type garbageHandler struct {
|
|
||||||
t *testing.T
|
|
||||||
success bool
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *garbageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintln(w, "Hello, client")
|
|
||||||
wp := fmt.Sprint("/v2/keys/_etcd/registry/1/", bootstrapName)
|
|
||||||
if gp := r.URL.String(); gp != wp {
|
|
||||||
g.t.Fatalf("url = %s, want %s", gp, wp)
|
|
||||||
}
|
|
||||||
g.Lock()
|
|
||||||
defer g.Unlock()
|
|
||||||
|
|
||||||
g.success = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBadDiscoveryService(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
g := garbageHandler{t: t}
|
|
||||||
httpts := httptest.NewServer(&g)
|
|
||||||
defer httpts.Close()
|
|
||||||
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = bootstrapName
|
|
||||||
c.Discovery = httpts.URL + "/v2/keys/_etcd/registry/1"
|
|
||||||
ts := testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
|
|
||||||
err := ts.Destroy()
|
|
||||||
w := `discovery service error`
|
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), w) {
|
|
||||||
t.Errorf("err = %v, want %s prefix", err, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Lock()
|
|
||||||
defer g.Unlock()
|
|
||||||
if !g.success {
|
|
||||||
t.Fatal("Discovery server never called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBadDiscoveryServiceWithAdvisedPeers(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
g := garbageHandler{t: t}
|
|
||||||
httpts := httptest.NewServer(&g)
|
|
||||||
defer httpts.Close()
|
|
||||||
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = bootstrapName
|
|
||||||
c.Discovery = httpts.URL + "/v2/keys/_etcd/registry/1"
|
|
||||||
c.Peers = []string{"a peer"}
|
|
||||||
ts := testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
|
|
||||||
err := ts.Destroy()
|
|
||||||
w := `discovery service error`
|
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), w) {
|
|
||||||
t.Errorf("err = %v, want %s prefix", err, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBootstrapByEmptyPeers(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
ts := testServer{}
|
|
||||||
ts.Start()
|
|
||||||
defer ts.Destroy()
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
if ts.Participant().node.Leader() != ts.Participant().id {
|
|
||||||
t.Errorf("leader = %x, want %x", ts.Participant().node.Leader(), ts.Participant().id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBootstrapByDiscoveryService(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
discoverService := testCluster{Size: 1}
|
|
||||||
discoverService.Start()
|
|
||||||
defer discoverService.Destroy()
|
|
||||||
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = bootstrapName
|
|
||||||
c.Discovery = discoverService.URL(0) + "/v2/keys/_etcd/registry/1"
|
|
||||||
ts := testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
err := ts.Destroy()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("server stop err = %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunByAdvisedPeers(t *testing.T) {
|
|
||||||
t.Skip("test covered by TestMultipleNodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunByDiscoveryService(t *testing.T) {
|
|
||||||
ds := testCluster{Size: 1}
|
|
||||||
ds.Start()
|
|
||||||
defer ds.Destroy()
|
|
||||||
|
|
||||||
tc := NewTestClient()
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("value", "started")
|
|
||||||
resp, _ := tc.PutForm(fmt.Sprintf("%s%s", ds.URL(0), "/v2/keys/_etcd/registry/1/_state"), v)
|
|
||||||
if g := resp.StatusCode; g != http.StatusCreated {
|
|
||||||
t.Fatalf("put status = %d, want %d", g, http.StatusCreated)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
v.Set("value", ds.URL(0))
|
|
||||||
resp, _ = tc.PutForm(fmt.Sprintf("%s%s%d", ds.URL(0), "/v2/keys/_etcd/registry/1/", ds.Participant(0).id), v)
|
|
||||||
if g := resp.StatusCode; g != http.StatusCreated {
|
|
||||||
t.Fatalf("put status = %d, want %d", g, http.StatusCreated)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = bootstrapName
|
|
||||||
c.Discovery = ds.URL(0) + "/v2/keys/_etcd/registry/1"
|
|
||||||
ts := &testServer{Config: c}
|
|
||||||
ds.Add(ts)
|
|
||||||
|
|
||||||
// wait for the leader to do a heartbeat
|
|
||||||
// it will update the lead field of the follower
|
|
||||||
time.Sleep(ds.Node(0).e.tickDuration * defaultHeartbeat * 2)
|
|
||||||
w := ds.Participant(0).id
|
|
||||||
if g := ts.Lead().lead; g != w {
|
|
||||||
t.Errorf("leader = %d, want %d", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunByDataDir(t *testing.T) {
|
|
||||||
t.Skip("test covered by TestSingleNodeRecovery")
|
|
||||||
}
|
|
@ -1,656 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMultipleNodes(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{1, 3, 5, 9, 11}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
c := &testCluster{Size: tt}
|
|
||||||
c.Start()
|
|
||||||
c.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleTLSNodes(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{1, 3, 5}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
c := &testCluster{Size: tt, TLS: true}
|
|
||||||
c.Start()
|
|
||||||
c.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestV2Redirect(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
c := &testCluster{Size: 3}
|
|
||||||
c.Start()
|
|
||||||
defer c.Destroy()
|
|
||||||
|
|
||||||
u := c.URL(1)
|
|
||||||
ru := fmt.Sprintf("%s%s", c.URL(0), "/v2/keys/foo")
|
|
||||||
tc := NewTestClient()
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("value", "XXX")
|
|
||||||
resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v)
|
|
||||||
if resp.StatusCode != http.StatusTemporaryRedirect {
|
|
||||||
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
location, err := resp.Location()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("want err = %, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if location.String() != ru {
|
|
||||||
t.Errorf("location = %v, want %v", location.String(), ru)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemove(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
tests := []int{3, 4, 5, 6}
|
|
||||||
for aa := 0; aa < 1; aa++ {
|
|
||||||
for k, tt := range tests {
|
|
||||||
cl := testCluster{Size: tt}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
lead, _ := cl.Leader()
|
|
||||||
config := conf.NewClusterConfig()
|
|
||||||
config.ActiveSize = 0
|
|
||||||
if err := cl.Participant(lead).setClusterConfig(config); err != nil {
|
|
||||||
t.Fatalf("#%d: setClusterConfig err = %v", k, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we don't remove the machine from 2-node cluster because it is
|
|
||||||
// not 100 percent safe in our raft.
|
|
||||||
// TODO(yichengq): improve it later.
|
|
||||||
for i := 0; i < tt-2; i++ {
|
|
||||||
id := cl.Id(i)
|
|
||||||
for {
|
|
||||||
n := cl.Node(i)
|
|
||||||
if n.e.mode.Get() == standbyMode {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
err := n.Participant().remove(id)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch err {
|
|
||||||
case tmpErr:
|
|
||||||
time.Sleep(defaultElection * 5 * time.Millisecond)
|
|
||||||
case raftStopErr, stopErr:
|
|
||||||
default:
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cl.Node(i).WaitMode(standbyMode)
|
|
||||||
}
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(yicheng) Add test for becoming standby
|
|
||||||
// maxSize -> standby
|
|
||||||
// auto-demote -> standby
|
|
||||||
// remove -> standby
|
|
||||||
|
|
||||||
func TestReleaseVersion(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
resp, err := http.Get(cl.URL(0) + "/version")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
g, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
gs := string(g)
|
|
||||||
w := fmt.Sprintf("etcd %s", releaseVersion)
|
|
||||||
if gs != w {
|
|
||||||
t.Errorf("version = %v, want %v", gs, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionCheck(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
u := cl.URL(0)
|
|
||||||
|
|
||||||
currentVersion := 2
|
|
||||||
tests := []struct {
|
|
||||||
version int
|
|
||||||
wStatus int
|
|
||||||
}{
|
|
||||||
{currentVersion - 1, http.StatusForbidden},
|
|
||||||
{currentVersion, http.StatusOK},
|
|
||||||
{currentVersion + 1, http.StatusForbidden},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/raft/version/%d/check", u, tt.version))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode != tt.wStatus {
|
|
||||||
t.Fatal("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSingleNodeRecovery(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
c := newTestConfig()
|
|
||||||
ts := testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
defer ts.Destroy()
|
|
||||||
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
|
|
||||||
key := "/foo"
|
|
||||||
ev, err := ts.Participant().Set(key, false, "bar", time.Now().Add(time.Second*100))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ts.Stop()
|
|
||||||
|
|
||||||
ts = testServer{Config: c}
|
|
||||||
ts.Start()
|
|
||||||
ts.WaitMode(participantMode)
|
|
||||||
w, err := ts.Participant().Store.Watch(key, false, false, ev.Index())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// give testing server time to load the previous WAL file
|
|
||||||
select {
|
|
||||||
case <-w.EventChan:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("watch timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTakingSnapshot(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
// TODO(xiangli): tunable compact; reduce testing time
|
|
||||||
for i := 0; i < defaultCompact; i++ {
|
|
||||||
cl.Participant(0).Set("/foo", false, "bar", store.Permanent)
|
|
||||||
}
|
|
||||||
snap := cl.Participant(0).node.GetSnap()
|
|
||||||
if snap.Index != int64(defaultCompact) {
|
|
||||||
t.Errorf("snap.Index = %d, want %d", snap.Index, defaultCompact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRestoreSnapshotFromLeader(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
// let leader do snapshot
|
|
||||||
for i := 0; i < defaultCompact; i++ {
|
|
||||||
cl.Participant(0).Set(fmt.Sprint("/foo", i), false, fmt.Sprint("bar", i), store.Permanent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create one to join the cluster
|
|
||||||
c := newTestConfig()
|
|
||||||
c.Name = "1"
|
|
||||||
c.Peers = []string{cl.URL(0)}
|
|
||||||
ts := &testServer{Config: c}
|
|
||||||
cl.Add(ts)
|
|
||||||
|
|
||||||
// check new proposal could be submitted
|
|
||||||
if _, err := cl.Participant(0).Set("/foo", false, "bar", store.Permanent); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check store is recovered
|
|
||||||
for i := 0; i < defaultCompact; i++ {
|
|
||||||
ev, err := ts.Participant().Store.Get(fmt.Sprint("/foo", i), false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("get err = %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w := fmt.Sprint("bar", i)
|
|
||||||
if g := *ev.Node.Value; g != w {
|
|
||||||
t.Errorf("value = %v, want %v", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check new proposal could be committed in the new machine
|
|
||||||
wch, err := ts.Participant().Watch("/foo", false, false, uint64(defaultCompact))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("watch err = %v", err)
|
|
||||||
}
|
|
||||||
<-wch.EventChan
|
|
||||||
|
|
||||||
// check node map of two machines are the same
|
|
||||||
g := ts.Participant().node.Nodes()
|
|
||||||
w := cl.Participant(0).node.Nodes()
|
|
||||||
if !reflect.DeepEqual(g, w) {
|
|
||||||
t.Errorf("nodes = %v, want %v", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveSnapshot(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
cl := testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
n := cl.Node(0)
|
|
||||||
// TODO(xiangli): tunable compact; reduce testing time
|
|
||||||
for i := 0; i < defaultCompact; i++ {
|
|
||||||
n.Participant().Set("/foo", false, "bar", store.Permanent)
|
|
||||||
}
|
|
||||||
snapname := fmt.Sprintf("%016x-%016x-%016x.snap", n.Participant().clusterId, 1, defaultCompact)
|
|
||||||
snappath := path.Join(n.Config.DataDir, "snap", snapname)
|
|
||||||
if _, err := os.Stat(snappath); err != nil {
|
|
||||||
t.Errorf("err = %v, want nil", err)
|
|
||||||
}
|
|
||||||
walname := fmt.Sprintf("%016x-%016x.wal", 1, defaultCompact)
|
|
||||||
walpath := path.Join(n.Config.DataDir, "wal", walname)
|
|
||||||
if _, err := os.Stat(walpath); err != nil {
|
|
||||||
t.Errorf("err = %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRestoreSnapshotFromDisk(t *testing.T) {
|
|
||||||
defer afterTest(t)
|
|
||||||
|
|
||||||
tests := []int{1, 3, 5}
|
|
||||||
|
|
||||||
// TODO(xiangli): tunable compact; reduce testing time
|
|
||||||
oldDefaultCompact := defaultCompact
|
|
||||||
defaultCompact = 10
|
|
||||||
defer func() { defaultCompact = oldDefaultCompact }()
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
cl := testCluster{Size: tt}
|
|
||||||
cl.Start()
|
|
||||||
defer cl.Destroy()
|
|
||||||
|
|
||||||
lead, _ := cl.Leader()
|
|
||||||
for i := 0; i < defaultCompact+10; i++ {
|
|
||||||
cl.Participant(lead).Set(fmt.Sprint("/foo", i), false, fmt.Sprint("bar", i), store.Permanent)
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Stop()
|
|
||||||
cl.Restart()
|
|
||||||
|
|
||||||
lead, _ = cl.Leader()
|
|
||||||
// check store is recovered
|
|
||||||
for i := 0; i < defaultCompact+10; i++ {
|
|
||||||
ev, err := cl.Participant(lead).Store.Get(fmt.Sprint("/foo", i), false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("get err = %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w := fmt.Sprint("bar", i)
|
|
||||||
if g := *ev.Node.Value; g != w {
|
|
||||||
t.Errorf("value = %v, want %v", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check new proposal could be submitted
|
|
||||||
if _, err := cl.Participant(lead).Set("/foo", false, "bar", store.Permanent); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testCluster struct {
|
|
||||||
Size int
|
|
||||||
TLS bool
|
|
||||||
|
|
||||||
nodes []*testServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Start() {
|
|
||||||
if c.Size <= 0 {
|
|
||||||
panic("cluster size <= 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make([]*testServer, c.Size)
|
|
||||||
c.nodes = nodes
|
|
||||||
cfg := newTestConfig()
|
|
||||||
cfg.Name = "testServer-0"
|
|
||||||
nodes[0] = &testServer{Config: cfg, TLS: c.TLS}
|
|
||||||
nodes[0].Start()
|
|
||||||
nodes[0].WaitMode(participantMode)
|
|
||||||
|
|
||||||
seed := nodes[0].URL
|
|
||||||
for i := 1; i < c.Size; i++ {
|
|
||||||
cfg := newTestConfig()
|
|
||||||
cfg.Name = "testServer-" + fmt.Sprint(i)
|
|
||||||
cfg.Peers = []string{seed}
|
|
||||||
s := &testServer{Config: cfg, TLS: c.TLS}
|
|
||||||
s.Start()
|
|
||||||
nodes[i] = s
|
|
||||||
|
|
||||||
// Wait for the previous configuration change to be committed
|
|
||||||
// or this configuration request might be dropped.
|
|
||||||
// Or it could be a slow join because it needs to retry.
|
|
||||||
// TODO: this might not be true if we add param for retry interval.
|
|
||||||
s.WaitMode(participantMode)
|
|
||||||
w, err := s.Participant().Watch(v2machineKVPrefix, true, false, uint64(i))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
<-w.EventChan
|
|
||||||
}
|
|
||||||
c.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) wait() {
|
|
||||||
size := c.Size
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
for k := 0; k < size; k++ {
|
|
||||||
s := c.Node(i)
|
|
||||||
wp := v2machineKVPrefix + fmt.Sprintf("/%d", c.Id(k))
|
|
||||||
w, err := s.Participant().Watch(wp, false, false, 1)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
<-w.EventChan
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterId := c.Participant(0).node.ClusterId()
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
if g := c.Participant(i).node.ClusterId(); g != clusterId {
|
|
||||||
panic(fmt.Sprintf("#%d: clusterId = %x, want %x", i, g, clusterId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Add(s *testServer) {
|
|
||||||
lead, _ := c.Leader()
|
|
||||||
// wait for the node to join the cluster
|
|
||||||
// TODO(yichengq): remove this when we get rid of all timeouts
|
|
||||||
wch, err := c.Participant(int(lead)).Watch(v2machineKVPrefix, true, false, 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
s.Start()
|
|
||||||
<-wch.EventChan
|
|
||||||
c.Size++
|
|
||||||
c.nodes = append(c.nodes, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Node(i int) *testServer {
|
|
||||||
return c.nodes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Participant(i int) *participant {
|
|
||||||
return c.Node(i).Participant()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Standby(i int) *standby {
|
|
||||||
return c.Node(i).Standby()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) URL(i int) string {
|
|
||||||
return c.nodes[i].h.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Id(i int) int64 {
|
|
||||||
return c.Participant(i).id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Restart() {
|
|
||||||
for _, s := range c.nodes {
|
|
||||||
s.Start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Stop() {
|
|
||||||
for _, s := range c.nodes {
|
|
||||||
s.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCluster) Destroy() {
|
|
||||||
for _, s := range c.nodes {
|
|
||||||
s.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leader returns the index of leader in the cluster and its leader term.
|
|
||||||
func (c *testCluster) Leader() (leadIdx int, term int64) {
|
|
||||||
ids := make(map[int64]int)
|
|
||||||
for {
|
|
||||||
ls := make([]leadterm, 0, c.Size)
|
|
||||||
for i := range c.nodes {
|
|
||||||
switch c.Node(i).e.mode.Get() {
|
|
||||||
case participantMode:
|
|
||||||
ls = append(ls, c.Node(i).Lead())
|
|
||||||
ids[c.Id(i)] = i
|
|
||||||
case standbyMode:
|
|
||||||
//TODO(xiangli) add standby support
|
|
||||||
case stopMode:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isSameLead(ls) {
|
|
||||||
return ids[ls[0].lead], ls[0].term
|
|
||||||
}
|
|
||||||
time.Sleep(c.Node(0).e.tickDuration * defaultElection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type leadterm struct {
|
|
||||||
lead int64
|
|
||||||
term int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSameLead(ls []leadterm) bool {
|
|
||||||
m := make(map[leadterm]int)
|
|
||||||
for i := range ls {
|
|
||||||
m[ls[i]] = m[ls[i]] + 1
|
|
||||||
}
|
|
||||||
if len(m) == 1 {
|
|
||||||
if ls[0].lead == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// todo(xiangli): printout the current cluster status for debugging....
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type testServer struct {
|
|
||||||
Config *conf.Config
|
|
||||||
TLS bool
|
|
||||||
|
|
||||||
// base URL of form http://ipaddr:port with no trailing slash
|
|
||||||
URL string
|
|
||||||
|
|
||||||
e *Server
|
|
||||||
h *httptest.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Start() {
|
|
||||||
if s.Config == nil {
|
|
||||||
s.Config = newTestConfig()
|
|
||||||
}
|
|
||||||
c := s.Config
|
|
||||||
if !strings.HasPrefix(c.DataDir, os.TempDir()) {
|
|
||||||
panic("dataDir may pollute file system")
|
|
||||||
}
|
|
||||||
if c.Peer.CAFile != "" || c.Peer.CertFile != "" || c.Peer.KeyFile != "" {
|
|
||||||
panic("use TLS field instead")
|
|
||||||
}
|
|
||||||
|
|
||||||
nc := *c
|
|
||||||
e, err := New(&nc)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
s.e = e
|
|
||||||
tick := time.Duration(c.Peer.HeartbeatInterval) * time.Millisecond
|
|
||||||
e.SetTick(tick)
|
|
||||||
|
|
||||||
m := http.NewServeMux()
|
|
||||||
m.Handle("/", e)
|
|
||||||
m.Handle("/raft", e.RaftHandler())
|
|
||||||
m.Handle("/raft/", e.RaftHandler())
|
|
||||||
m.Handle("/v2/admin/", e.RaftHandler())
|
|
||||||
|
|
||||||
addr := c.Addr
|
|
||||||
if s.URL != "" {
|
|
||||||
addr = urlHost(s.URL)
|
|
||||||
}
|
|
||||||
s.h = startServingAddr(addr, m, s.TLS)
|
|
||||||
s.URL = s.h.URL
|
|
||||||
|
|
||||||
e.pubAddr = s.URL
|
|
||||||
e.raftPubAddr = s.URL
|
|
||||||
e.cfg.Addr = s.URL
|
|
||||||
e.cfg.Peer.Addr = s.URL
|
|
||||||
go e.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) WaitMode(mode int64) {
|
|
||||||
for i := 0; i < 30; i++ {
|
|
||||||
if s.e.mode.Get() == mode {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
panic("waitMode should never take more than 30ms.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Participant() *participant {
|
|
||||||
if s.e.mode.Get() != participantMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.e.p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Standby() *standby {
|
|
||||||
return s.e.s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Lead() leadterm {
|
|
||||||
return leadterm{s.Participant().node.Leader(), s.Participant().node.Term()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Stop() error {
|
|
||||||
err := s.e.Stop()
|
|
||||||
s.h.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testServer) Destroy() error {
|
|
||||||
err := s.Stop()
|
|
||||||
if err := os.RemoveAll(s.Config.DataDir); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServingAddr(addr string, h http.Handler, tls bool) *httptest.Server {
|
|
||||||
var l net.Listener
|
|
||||||
var err error
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
l, err = net.Listen("tcp", addr)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "address already in use") {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if l == nil {
|
|
||||||
panic("cannot listen on " + addr)
|
|
||||||
}
|
|
||||||
hs := &httptest.Server{
|
|
||||||
Listener: l,
|
|
||||||
Config: &http.Server{Handler: h},
|
|
||||||
}
|
|
||||||
if tls {
|
|
||||||
hs.StartTLS()
|
|
||||||
} else {
|
|
||||||
hs.Start()
|
|
||||||
}
|
|
||||||
return hs
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestConfig() *conf.Config {
|
|
||||||
c := conf.New()
|
|
||||||
c.Addr = "127.0.0.1:0"
|
|
||||||
c.Peer.Addr = "127.0.0.1:0"
|
|
||||||
c.Peer.HeartbeatInterval = 5
|
|
||||||
c.Peer.ElectionTimeout = 25
|
|
||||||
c.RetryInterval = 1 / 10.0
|
|
||||||
dataDir, err := ioutil.TempDir(os.TempDir(), "etcd")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
c.DataDir = dataDir
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func urlHost(urlStr string) string {
|
|
||||||
u, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return u.Host
|
|
||||||
}
|
|
@ -17,11 +17,12 @@ import (
|
|||||||
|
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"code.google.com/p/go.net/context"
|
"code.google.com/p/go.net/context"
|
||||||
"github.com/coreos/etcd/elog"
|
"github.com/coreos/etcd/elog"
|
||||||
etcderrors "github.com/coreos/etcd/error"
|
etcderrors "github.com/coreos/etcd/error"
|
||||||
etcdserver "github.com/coreos/etcd/etcdserver2"
|
"github.com/coreos/etcd/etcdserver"
|
||||||
"github.com/coreos/etcd/etcdserver2/etcdserverpb"
|
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
"github.com/coreos/etcd/raft/raftpb"
|
"github.com/coreos/etcd/raft/raftpb"
|
||||||
"github.com/coreos/etcd/store"
|
"github.com/coreos/etcd/store"
|
||||||
)
|
)
|
@ -8,9 +8,10 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/go.net/context"
|
"code.google.com/p/go.net/context"
|
||||||
|
|
||||||
etcdserver "github.com/coreos/etcd/etcdserver2"
|
"github.com/coreos/etcd/etcdserver"
|
||||||
"github.com/coreos/etcd/raft"
|
"github.com/coreos/etcd/raft"
|
||||||
"github.com/coreos/etcd/raft/raftpb"
|
"github.com/coreos/etcd/raft/raftpb"
|
||||||
"github.com/coreos/etcd/store"
|
"github.com/coreos/etcd/store"
|
@ -1,25 +0,0 @@
|
|||||||
package etcdserver
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,522 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
"github.com/coreos/etcd/raft"
|
|
||||||
"github.com/coreos/etcd/snap"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
"github.com/coreos/etcd/wal"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultHeartbeat = 1
|
|
||||||
defaultElection = 5
|
|
||||||
|
|
||||||
maxBufferedProposal = 128
|
|
||||||
|
|
||||||
defaultTickDuration = time.Millisecond * 100
|
|
||||||
|
|
||||||
v2machineKVPrefix = "/_etcd/machines"
|
|
||||||
v2configKVPrefix = "/_etcd/config"
|
|
||||||
|
|
||||||
v2Prefix = "/v2/keys"
|
|
||||||
v2machinePrefix = "/v2/machines"
|
|
||||||
v2peersPrefix = "/v2/peers"
|
|
||||||
v2LeaderPrefix = "/v2/leader"
|
|
||||||
v2SelfStatsPrefix = "/v2/stats/self"
|
|
||||||
v2LeaderStatsPrefix = "/v2/stats/leader"
|
|
||||||
v2StoreStatsPrefix = "/v2/stats/store"
|
|
||||||
v2adminConfigPrefix = "/v2/admin/config"
|
|
||||||
v2adminMachinesPrefix = "/v2/admin/machines/"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultCompact = 10000
|
|
||||||
|
|
||||||
tmpErr = fmt.Errorf("try again")
|
|
||||||
stopErr = fmt.Errorf("server is stopped")
|
|
||||||
raftStopErr = fmt.Errorf("raft is stopped")
|
|
||||||
)
|
|
||||||
|
|
||||||
type participant struct {
|
|
||||||
id int64
|
|
||||||
clusterId int64
|
|
||||||
cfg *conf.Config
|
|
||||||
pubAddr string
|
|
||||||
raftPubAddr string
|
|
||||||
tickDuration time.Duration
|
|
||||||
|
|
||||||
client *v2client
|
|
||||||
peerHub *peerHub
|
|
||||||
|
|
||||||
proposal chan v2Proposal
|
|
||||||
addNodeC chan raft.Config
|
|
||||||
removeNodeC chan raft.Config
|
|
||||||
node *v2Raft
|
|
||||||
store.Store
|
|
||||||
rh *raftHandler
|
|
||||||
w *wal.WAL
|
|
||||||
snapshotter *snap.Snapshotter
|
|
||||||
serverStats *raftServerStats
|
|
||||||
|
|
||||||
stopNotifyc chan struct{}
|
|
||||||
|
|
||||||
*http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
func newParticipant(c *conf.Config, client *v2client, peerHub *peerHub, tickDuration time.Duration) (*participant, error) {
|
|
||||||
p := &participant{
|
|
||||||
clusterId: -1,
|
|
||||||
cfg: c,
|
|
||||||
tickDuration: tickDuration,
|
|
||||||
|
|
||||||
client: client,
|
|
||||||
peerHub: peerHub,
|
|
||||||
|
|
||||||
proposal: make(chan v2Proposal, maxBufferedProposal),
|
|
||||||
addNodeC: make(chan raft.Config, 1),
|
|
||||||
removeNodeC: make(chan raft.Config, 1),
|
|
||||||
node: &v2Raft{
|
|
||||||
result: make(map[wait]chan interface{}),
|
|
||||||
},
|
|
||||||
Store: store.New(),
|
|
||||||
serverStats: NewRaftServerStats(c.Name),
|
|
||||||
|
|
||||||
stopNotifyc: make(chan struct{}),
|
|
||||||
|
|
||||||
ServeMux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
p.rh = newRaftHandler(peerHub, p.Store.Version(), p.serverStats)
|
|
||||||
p.peerHub.setServerStats(p.serverStats)
|
|
||||||
|
|
||||||
snapDir := path.Join(p.cfg.DataDir, "snap")
|
|
||||||
if err := os.Mkdir(snapDir, 0700); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
log.Printf("id=%x participant.new.mkdir err=%v", p.id, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.snapshotter = snap.New(snapDir)
|
|
||||||
|
|
||||||
walDir := path.Join(p.cfg.DataDir, "wal")
|
|
||||||
if err := os.Mkdir(walDir, 0700); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
log.Printf("id=%x participant.new.mkdir err=%v", p.id, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var w *wal.WAL
|
|
||||||
var err error
|
|
||||||
if !wal.Exist(walDir) {
|
|
||||||
p.id = genId()
|
|
||||||
p.pubAddr = c.Addr
|
|
||||||
p.raftPubAddr = c.Peer.Addr
|
|
||||||
if w, err = wal.Create(walDir); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.node.Node = raft.New(p.id, defaultHeartbeat, defaultElection)
|
|
||||||
info := p.node.Info()
|
|
||||||
if err = w.SaveInfo(&info); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("id=%x participant.new path=%s\n", p.id, walDir)
|
|
||||||
} else {
|
|
||||||
s, err := p.snapshotter.Load()
|
|
||||||
if err != nil && err != snap.ErrNoSnapshot {
|
|
||||||
log.Printf("participant.snapload err=%s\n", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var snapIndex int64
|
|
||||||
if s != nil {
|
|
||||||
if err := p.Recovery(s.Data); err != nil {
|
|
||||||
log.Printf("store.recover err=%v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("participant.store.recovered index=%d\n", s.Index)
|
|
||||||
|
|
||||||
for _, node := range s.Nodes {
|
|
||||||
pp := path.Join(v2machineKVPrefix, fmt.Sprint(node))
|
|
||||||
ev, err := p.Store.Get(pp, false, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("store.get err=%v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q, err := url.ParseQuery(*ev.Node.Value)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("url.parse err=%v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
peer, err := p.peerHub.add(node, q["raft"][0])
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("peerHub.add err=%v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
peer.participate()
|
|
||||||
}
|
|
||||||
|
|
||||||
snapIndex = s.Index
|
|
||||||
}
|
|
||||||
n, err := wal.Read(walDir, snapIndex)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.id = n.Id
|
|
||||||
p.node.Node = raft.Recover(n.Id, s, n.Ents, n.State, defaultHeartbeat, defaultElection)
|
|
||||||
p.apply(p.node.Next())
|
|
||||||
log.Printf("id=%x participant.load path=%s snapIndex=%d state=\"%+v\" len(ents)=%d", p.id, p.cfg.DataDir, snapIndex, n.State, len(n.Ents))
|
|
||||||
if w, err = wal.Open(walDir); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.w = w
|
|
||||||
|
|
||||||
p.Handle(v2Prefix+"/", handlerErr(p.serveValue))
|
|
||||||
p.Handle(v2machinePrefix, handlerErr(p.serveMachines))
|
|
||||||
p.Handle(v2peersPrefix, handlerErr(p.serveMachines))
|
|
||||||
p.Handle(v2LeaderPrefix, handlerErr(p.serveLeader))
|
|
||||||
p.Handle(v2SelfStatsPrefix, handlerErr(p.serveSelfStats))
|
|
||||||
p.Handle(v2LeaderStatsPrefix, handlerErr(p.serveLeaderStats))
|
|
||||||
p.Handle(v2StoreStatsPrefix, handlerErr(p.serveStoreStats))
|
|
||||||
p.rh.Handle(v2adminConfigPrefix, handlerErr(p.serveAdminConfig))
|
|
||||||
p.rh.Handle(v2adminMachinesPrefix, handlerErr(p.serveAdminMachines))
|
|
||||||
|
|
||||||
// TODO: remind to set application/json for /v2/stats endpoint
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) run(stop chan struct{}) error {
|
|
||||||
defer p.cleanup()
|
|
||||||
|
|
||||||
if p.node.IsEmpty() {
|
|
||||||
seeds := p.peerHub.getSeeds()
|
|
||||||
if len(seeds) == 0 {
|
|
||||||
log.Printf("id=%x participant.run action=bootstrap\n", p.id)
|
|
||||||
p.node.Campaign()
|
|
||||||
p.node.InitCluster(genId())
|
|
||||||
p.node.Add(p.id, p.raftPubAddr, []byte(p.pubAddr))
|
|
||||||
p.apply(p.node.Next())
|
|
||||||
} else {
|
|
||||||
log.Printf("id=%x participant.run action=join seeds=\"%v\"\n", p.id, seeds)
|
|
||||||
if err := p.join(); err != nil {
|
|
||||||
log.Printf("id=%x participant.join err=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.rh.start()
|
|
||||||
defer p.rh.stop()
|
|
||||||
|
|
||||||
node := p.node
|
|
||||||
recv := p.rh.recv
|
|
||||||
|
|
||||||
ticker := time.NewTicker(p.tickDuration)
|
|
||||||
defer ticker.Stop()
|
|
||||||
v2SyncTicker := time.NewTicker(time.Millisecond * 500)
|
|
||||||
defer v2SyncTicker.Stop()
|
|
||||||
|
|
||||||
var proposal chan v2Proposal
|
|
||||||
var addNodeC, removeNodeC chan raft.Config
|
|
||||||
for {
|
|
||||||
if node.HasLeader() {
|
|
||||||
proposal = p.proposal
|
|
||||||
addNodeC = p.addNodeC
|
|
||||||
removeNodeC = p.removeNodeC
|
|
||||||
} else {
|
|
||||||
proposal = nil
|
|
||||||
addNodeC = nil
|
|
||||||
removeNodeC = nil
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case p := <-proposal:
|
|
||||||
node.Propose(p)
|
|
||||||
case c := <-addNodeC:
|
|
||||||
node.UpdateConf(raft.AddNode, &c)
|
|
||||||
case c := <-removeNodeC:
|
|
||||||
node.UpdateConf(raft.RemoveNode, &c)
|
|
||||||
case msg := <-recv:
|
|
||||||
node.Step(*msg)
|
|
||||||
case <-ticker.C:
|
|
||||||
node.Tick()
|
|
||||||
case <-v2SyncTicker.C:
|
|
||||||
node.Sync()
|
|
||||||
case <-stop:
|
|
||||||
log.Printf("id=%x participant.stop\n", p.id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if s := node.UnstableSnapshot(); !s.IsEmpty() {
|
|
||||||
if err := p.Recovery(s.Data); err != nil {
|
|
||||||
log.Printf("id=%x participant.recover err=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("id=%x participant.recovered index=%d", p.id, s.Index)
|
|
||||||
}
|
|
||||||
p.apply(node.Next())
|
|
||||||
if err := p.save(node.UnstableEnts(), node.UnstableState()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.send(node.Msgs())
|
|
||||||
if node.IsRemoved() {
|
|
||||||
log.Printf("id=%x participant.end\n", p.id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if p.node.EntsLen() > defaultCompact {
|
|
||||||
d, err := p.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("id=%x participant.compact err=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.node.Compact(d)
|
|
||||||
snap := p.node.GetSnap()
|
|
||||||
log.Printf("id=%x compacted index=%d", p.id, snap.Index)
|
|
||||||
if err := p.snapshotter.Save(&snap); err != nil {
|
|
||||||
log.Printf("id=%x snapshot.save err=%v", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.w.Cut(p.node.Index()); err != nil {
|
|
||||||
log.Printf("id=%x wal.cut err=%v", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
info := p.node.Info()
|
|
||||||
if err = p.w.SaveInfo(&info); err != nil {
|
|
||||||
log.Printf("id=%x wal.saveInfo err=%v", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) cleanup() {
|
|
||||||
p.w.Close()
|
|
||||||
close(p.stopNotifyc)
|
|
||||||
p.peerHub.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) raftHandler() http.Handler {
|
|
||||||
return p.rh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) add(id int64, raftPubAddr string, pubAddr string) error {
|
|
||||||
log.Printf("id=%x participant.add nodeId=%x raftPubAddr=%s pubAddr=%s\n", p.id, id, raftPubAddr, pubAddr)
|
|
||||||
pp := path.Join(v2machineKVPrefix, fmt.Sprint(id))
|
|
||||||
|
|
||||||
_, err := p.Store.Get(pp, false, false)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if v, ok := err.(*etcdErr.Error); !ok || v.ErrorCode != etcdErr.EcodeKeyNotFound {
|
|
||||||
log.Printf("id=%x participant.add getErr=\"%v\"\n", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := p.Watch(pp, true, false, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("id=%x participant.add watchErr=\"%v\"\n", p.id, err)
|
|
||||||
return tmpErr
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.addNodeC <- raft.Config{NodeId: id, Addr: raftPubAddr, Context: []byte(pubAddr)}:
|
|
||||||
default:
|
|
||||||
w.Remove()
|
|
||||||
log.Printf("id=%x participant.add proposeErr=\"unable to send out addNode proposal\"\n", p.id)
|
|
||||||
return tmpErr
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case v := <-w.EventChan:
|
|
||||||
if v.Action == store.Set {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Printf("id=%x participant.add watchErr=\"unexpected action\" action=%s\n", p.id, v.Action)
|
|
||||||
return tmpErr
|
|
||||||
case <-time.After(6 * defaultHeartbeat * p.tickDuration):
|
|
||||||
w.Remove()
|
|
||||||
log.Printf("id=%x participant.add watchErr=timeout\n", p.id)
|
|
||||||
return tmpErr
|
|
||||||
case <-p.stopNotifyc:
|
|
||||||
return stopErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) remove(id int64) error {
|
|
||||||
log.Printf("id=%x participant.remove nodeId=%x\n", p.id, id)
|
|
||||||
pp := path.Join(v2machineKVPrefix, fmt.Sprint(id))
|
|
||||||
|
|
||||||
v, err := p.Store.Get(pp, false, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.removeNodeC <- raft.Config{NodeId: id}:
|
|
||||||
default:
|
|
||||||
log.Printf("id=%x participant.remove proposeErr=\"unable to send out removeNode proposal\"\n", p.id)
|
|
||||||
return tmpErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(xiangli): do not need to watch if the
|
|
||||||
// removal target is self
|
|
||||||
w, err := p.Watch(pp, true, false, v.Index()+1)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("id=%x participant.remove watchErr=\"%v\"\n", p.id, err)
|
|
||||||
return tmpErr
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case v := <-w.EventChan:
|
|
||||||
if v.Action == store.Delete {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Printf("id=%x participant.remove watchErr=\"unexpected action\" action=%s\n", p.id, v.Action)
|
|
||||||
return tmpErr
|
|
||||||
case <-time.After(6 * defaultHeartbeat * p.tickDuration):
|
|
||||||
w.Remove()
|
|
||||||
log.Printf("id=%x participant.remove watchErr=timeout\n", p.id)
|
|
||||||
return tmpErr
|
|
||||||
case <-p.stopNotifyc:
|
|
||||||
return stopErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) apply(ents []raft.Entry) {
|
|
||||||
offset := p.node.Applied() - int64(len(ents)) + 1
|
|
||||||
for i, ent := range ents {
|
|
||||||
switch ent.Type {
|
|
||||||
// expose raft entry type
|
|
||||||
case raft.Normal:
|
|
||||||
if len(ent.Data) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.v2apply(offset+int64(i), ent)
|
|
||||||
case raft.ClusterInit:
|
|
||||||
p.clusterId = p.node.ClusterId()
|
|
||||||
log.Printf("id=%x participant.cluster.setId clusterId=%x\n", p.id, p.clusterId)
|
|
||||||
case raft.AddNode:
|
|
||||||
cfg := new(raft.Config)
|
|
||||||
if err := json.Unmarshal(ent.Data, cfg); err != nil {
|
|
||||||
log.Printf("id=%x participant.cluster.addNode unmarshalErr=\"%v\"\n", p.id, err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
pp := path.Join(v2machineKVPrefix, fmt.Sprint(cfg.NodeId))
|
|
||||||
if ev, _ := p.Store.Get(pp, false, false); ev != nil {
|
|
||||||
log.Printf("id=%x participant.cluster.addNode err=existed value=%q", p.id, *ev.Node.Value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
peer, err := p.peerHub.add(cfg.NodeId, cfg.Addr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("id=%x participant.cluster.addNode peerAddErr=\"%v\"\n", p.id, err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
peer.participate()
|
|
||||||
p.Store.Set(pp, false, fmt.Sprintf("raft=%v&etcd=%v", cfg.Addr, string(cfg.Context)), store.Permanent)
|
|
||||||
if p.id == cfg.NodeId {
|
|
||||||
p.raftPubAddr = cfg.Addr
|
|
||||||
p.pubAddr = string(cfg.Context)
|
|
||||||
}
|
|
||||||
log.Printf("id=%x participant.cluster.addNode nodeId=%x addr=%s context=%s\n", p.id, cfg.NodeId, cfg.Addr, cfg.Context)
|
|
||||||
case raft.RemoveNode:
|
|
||||||
cfg := new(raft.Config)
|
|
||||||
if err := json.Unmarshal(ent.Data, cfg); err != nil {
|
|
||||||
log.Printf("id=%x participant.cluster.removeNode unmarshalErr=\"%v\"\n", p.id, err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
peer, err := p.peerHub.peer(cfg.NodeId)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("id=%x participant.apply getPeerErr=\"%v\"", p.id, err)
|
|
||||||
}
|
|
||||||
peer.idle()
|
|
||||||
pp := path.Join(v2machineKVPrefix, fmt.Sprint(cfg.NodeId))
|
|
||||||
p.Store.Delete(pp, false, false)
|
|
||||||
log.Printf("id=%x participant.cluster.removeNode nodeId=%x\n", p.id, cfg.NodeId)
|
|
||||||
default:
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) save(ents []raft.Entry, state raft.State) error {
|
|
||||||
for _, ent := range ents {
|
|
||||||
if err := p.w.SaveEntry(&ent); err != nil {
|
|
||||||
log.Printf("id=%x participant.save saveEntryErr=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !state.IsEmpty() {
|
|
||||||
if err := p.w.SaveState(&state); err != nil {
|
|
||||||
log.Printf("id=%x participant.save saveStateErr=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := p.w.Sync(); err != nil {
|
|
||||||
log.Printf("id=%x participant.save syncErr=%q", p.id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) send(msgs []raft.Message) {
|
|
||||||
for i := range msgs {
|
|
||||||
if err := p.peerHub.send(msgs[i]); err != nil {
|
|
||||||
log.Printf("id=%x participant.send err=\"%v\"\n", p.id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) join() error {
|
|
||||||
info := &context{
|
|
||||||
MinVersion: store.MinVersion(),
|
|
||||||
MaxVersion: store.MaxVersion(),
|
|
||||||
ClientURL: p.pubAddr,
|
|
||||||
PeerURL: p.raftPubAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
max := p.cfg.MaxRetryAttempts
|
|
||||||
for attempt := 0; ; attempt++ {
|
|
||||||
for seed := range p.peerHub.getSeeds() {
|
|
||||||
if err := p.client.AddMachine(seed, fmt.Sprint(p.id), info); err == nil {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
log.Printf("id=%x participant.join addMachineErr=\"%v\"\n", p.id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if attempt == max {
|
|
||||||
return fmt.Errorf("etcd: cannot join cluster after %d attempts", max)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond * time.Duration(p.cfg.RetryInterval*1000))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func genId() int64 {
|
|
||||||
r := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
|
||||||
return r.Int63()
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxInflight = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
participantPeer = iota
|
|
||||||
idlePeer
|
|
||||||
stoppedPeer
|
|
||||||
)
|
|
||||||
|
|
||||||
type peer struct {
|
|
||||||
url string
|
|
||||||
queue chan []byte
|
|
||||||
status int
|
|
||||||
inflight atomicInt
|
|
||||||
c *http.Client
|
|
||||||
followerStats *raftFollowerStats
|
|
||||||
mu sync.RWMutex
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeer(url string, c *http.Client, followerStats *raftFollowerStats) *peer {
|
|
||||||
return &peer{
|
|
||||||
url: url,
|
|
||||||
status: idlePeer,
|
|
||||||
c: c,
|
|
||||||
followerStats: followerStats,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) participate() {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.queue = make(chan []byte)
|
|
||||||
p.status = participantPeer
|
|
||||||
for i := 0; i < maxInflight; i++ {
|
|
||||||
p.wg.Add(1)
|
|
||||||
go p.handle(p.queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) idle() {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
if p.status == participantPeer {
|
|
||||||
close(p.queue)
|
|
||||||
}
|
|
||||||
p.status = idlePeer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) stop() {
|
|
||||||
p.mu.Lock()
|
|
||||||
if p.status == participantPeer {
|
|
||||||
close(p.queue)
|
|
||||||
}
|
|
||||||
p.status = stoppedPeer
|
|
||||||
p.mu.Unlock()
|
|
||||||
p.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) handle(queue chan []byte) {
|
|
||||||
defer p.wg.Done()
|
|
||||||
for d := range queue {
|
|
||||||
p.post(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) send(d []byte) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
switch p.status {
|
|
||||||
case participantPeer:
|
|
||||||
select {
|
|
||||||
case p.queue <- d:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("reach max serving")
|
|
||||||
}
|
|
||||||
case idlePeer:
|
|
||||||
if p.inflight.Get() > maxInflight {
|
|
||||||
return fmt.Errorf("reach max idle")
|
|
||||||
}
|
|
||||||
p.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
p.post(d)
|
|
||||||
p.wg.Done()
|
|
||||||
}()
|
|
||||||
case stoppedPeer:
|
|
||||||
return fmt.Errorf("sender stopped")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peer) post(d []byte) {
|
|
||||||
p.inflight.Add(1)
|
|
||||||
defer p.inflight.Add(-1)
|
|
||||||
buf := bytes.NewBuffer(d)
|
|
||||||
start := time.Now()
|
|
||||||
resp, err := p.c.Post(p.url, "application/octet-stream", buf)
|
|
||||||
end := time.Now()
|
|
||||||
// TODO: Have no way to detect RPC success or failure now
|
|
||||||
p.followerStats.Succ(end.Sub(start))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("peer.post url=%s err=\"%v\"", p.url, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// An AtomicInt is an int64 to be accessed atomically.
|
|
||||||
type atomicInt int64
|
|
||||||
|
|
||||||
func (i *atomicInt) Add(d int64) {
|
|
||||||
atomic.AddInt64((*int64)(i), d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *atomicInt) Get() int64 {
|
|
||||||
return atomic.LoadInt64((*int64)(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *atomicInt) Set(n int64) {
|
|
||||||
atomic.StoreInt64((*int64)(i), n)
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/raft"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errUnknownPeer = errors.New("unknown peer")
|
|
||||||
)
|
|
||||||
|
|
||||||
type peerGetter interface {
|
|
||||||
peer(id int64) (*peer, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type peerHub struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
stopped bool
|
|
||||||
seeds map[string]bool
|
|
||||||
peers map[int64]*peer
|
|
||||||
c *http.Client
|
|
||||||
followersStats *raftFollowersStats
|
|
||||||
serverStats *raftServerStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeerHub(c *http.Client, followersStats *raftFollowersStats) *peerHub {
|
|
||||||
h := &peerHub{
|
|
||||||
peers: make(map[int64]*peer),
|
|
||||||
seeds: make(map[string]bool),
|
|
||||||
c: c,
|
|
||||||
followersStats: followersStats,
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) setServerStats(serverStats *raftServerStats) {
|
|
||||||
h.serverStats = serverStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) setSeeds(seeds []string) {
|
|
||||||
for _, seed := range seeds {
|
|
||||||
h.seeds[seed] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) getSeeds() map[string]bool {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
s := make(map[string]bool)
|
|
||||||
for k, v := range h.seeds {
|
|
||||||
s[k] = v
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) stop() {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.stopped = true
|
|
||||||
for _, p := range h.peers {
|
|
||||||
p.stop()
|
|
||||||
}
|
|
||||||
h.followersStats.Reset()
|
|
||||||
// http.Transport needs some time to put used connections
|
|
||||||
// into idle queues.
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
tr := h.c.Transport.(*http.Transport)
|
|
||||||
tr.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) peer(id int64) (*peer, error) {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
if h.stopped {
|
|
||||||
return nil, fmt.Errorf("peerHub stopped")
|
|
||||||
}
|
|
||||||
if p, ok := h.peers[id]; ok {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("peer %d not found", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) add(id int64, rawurl string) (*peer, error) {
|
|
||||||
u, err := url.Parse(rawurl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u.Path = raftPrefix
|
|
||||||
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
if h.stopped {
|
|
||||||
return nil, fmt.Errorf("peerHub stopped")
|
|
||||||
}
|
|
||||||
h.peers[id] = newPeer(u.String(), h.c, h.followersStats.Follower(fmt.Sprint(id)))
|
|
||||||
return h.peers[id], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) send(msg raft.Message) error {
|
|
||||||
if p, err := h.fetch(msg.To); err == nil {
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if msg.IsMsgApp() {
|
|
||||||
h.serverStats.SendAppendReq(len(data))
|
|
||||||
}
|
|
||||||
p.send(data)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errUnknownPeer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) fetch(nodeId int64) (*peer, error) {
|
|
||||||
if p, err := h.peer(nodeId); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
for seed := range h.seeds {
|
|
||||||
if p, err := h.seedFetch(seed, nodeId); err == nil {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cannot fetch the address of node %d", nodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *peerHub) seedFetch(seedurl string, id int64) (*peer, error) {
|
|
||||||
u, err := url.Parse(seedurl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse the url of the given seed")
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Path = path.Join("/raft/cfg", fmt.Sprint(id))
|
|
||||||
resp, err := h.c.Get(u.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot reach %v", u)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("cannot find node %d via %s", id, seedurl)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot reach %v", u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.add(id, string(b))
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type raftFollowersStats struct {
|
|
||||||
Leader string `json:"leader"`
|
|
||||||
Followers map[string]*raftFollowerStats `json:"followers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRaftFollowersStats(name string) *raftFollowersStats {
|
|
||||||
return &raftFollowersStats{
|
|
||||||
Leader: name,
|
|
||||||
Followers: make(map[string]*raftFollowerStats),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *raftFollowersStats) Follower(name string) *raftFollowerStats {
|
|
||||||
follower, ok := fs.Followers[name]
|
|
||||||
if !ok {
|
|
||||||
follower = &raftFollowerStats{}
|
|
||||||
follower.Latency.Minimum = 1 << 63
|
|
||||||
fs.Followers[name] = follower
|
|
||||||
}
|
|
||||||
return follower
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *raftFollowersStats) Reset() {
|
|
||||||
fs.Followers = make(map[string]*raftFollowerStats)
|
|
||||||
}
|
|
||||||
|
|
||||||
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++
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/raft"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
raftPrefix = "/raft"
|
|
||||||
)
|
|
||||||
|
|
||||||
type raftHandler struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
serving bool
|
|
||||||
|
|
||||||
peerGetter peerGetter
|
|
||||||
storeVersion int
|
|
||||||
serverStats *raftServerStats
|
|
||||||
|
|
||||||
recv chan *raft.Message
|
|
||||||
*http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRaftHandler(p peerGetter, version int, serverStats *raftServerStats) *raftHandler {
|
|
||||||
h := &raftHandler{
|
|
||||||
recv: make(chan *raft.Message, 512),
|
|
||||||
peerGetter: p,
|
|
||||||
storeVersion: version,
|
|
||||||
serverStats: serverStats,
|
|
||||||
}
|
|
||||||
h.ServeMux = http.NewServeMux()
|
|
||||||
h.ServeMux.HandleFunc(raftPrefix+"/cfg/", h.serveCfg)
|
|
||||||
h.ServeMux.HandleFunc(raftPrefix, h.serveRaft)
|
|
||||||
h.ServeMux.HandleFunc(raftPrefix+"/version", h.serveVersion)
|
|
||||||
h.ServeMux.HandleFunc(raftPrefix+"/version/", h.serveVersionCheck)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) start() {
|
|
||||||
h.mu.Lock()
|
|
||||||
h.serving = true
|
|
||||||
h.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) stop() {
|
|
||||||
h.mu.Lock()
|
|
||||||
h.serving = false
|
|
||||||
h.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) serveRaft(w http.ResponseWriter, r *http.Request) {
|
|
||||||
h.mu.RLock()
|
|
||||||
serving := h.serving
|
|
||||||
h.mu.RUnlock()
|
|
||||||
if !serving {
|
|
||||||
http.Error(w, "404 page not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := new(raft.Message)
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(msg); err != nil {
|
|
||||||
log.Printf("raftHandler.serve decodeErr=\"%v\"\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if msg.IsMsgApp() {
|
|
||||||
h.serverStats.RecvAppendReq(fmt.Sprint(msg.From), int(r.ContentLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case h.recv <- msg:
|
|
||||||
default:
|
|
||||||
log.Printf("raftHandler.serve pushErr=\"recv channel is full\"\n")
|
|
||||||
// drop the incoming package at network layer if the upper layer
|
|
||||||
// cannot consume them in time.
|
|
||||||
// TODO(xiangli): not return 200.
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) serveCfg(w http.ResponseWriter, r *http.Request) {
|
|
||||||
h.mu.RLock()
|
|
||||||
serving := h.serving
|
|
||||||
h.mu.RUnlock()
|
|
||||||
if !serving {
|
|
||||||
http.Error(w, "404 page not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.ParseInt(r.URL.Path[len("/raft/cfg/"):], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p, err := h.peerGetter.peer(id)
|
|
||||||
if err == nil {
|
|
||||||
w.Write([]byte(p.url))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) serveVersion(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Write([]byte(strconv.Itoa(h.storeVersion)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *raftHandler) serveVersionCheck(w http.ResponseWriter, req *http.Request) {
|
|
||||||
var version int
|
|
||||||
n, err := fmt.Sscanf(req.URL.Path, raftPrefix+"/version/%d/check", &version)
|
|
||||||
if err != nil || n != 1 {
|
|
||||||
http.Error(w, "error version check format: "+req.URL.Path, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if version < store.MinVersion() || version > store.MaxVersion() {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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:"startTime"`
|
|
||||||
} `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
|
|
||||||
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRaftServerStats(name string) *raftServerStats {
|
|
||||||
stats := &raftServerStats{
|
|
||||||
Name: name,
|
|
||||||
StartTime: time.Now(),
|
|
||||||
sendRateQueue: &statsQueue{
|
|
||||||
back: -1,
|
|
||||||
},
|
|
||||||
recvRateQueue: &statsQueue{
|
|
||||||
back: -1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
stats.LeaderInfo.StartTime = time.Now()
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *raftServerStats) Reset() {
|
|
||||||
name := ss.Name
|
|
||||||
ss = NewRaftServerStats(name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *raftServerStats) RecvAppendReq(leaderName string, pkgSize int) {
|
|
||||||
ss.Lock()
|
|
||||||
defer ss.Unlock()
|
|
||||||
|
|
||||||
ss.State = stateFollower
|
|
||||||
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) {
|
|
||||||
ss.Lock()
|
|
||||||
defer ss.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if ss.State != stateLeader {
|
|
||||||
ss.State = stateLeader
|
|
||||||
ss.LeaderInfo.Name = ss.Name
|
|
||||||
ss.LeaderInfo.StartTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
ss.sendRateQueue.Insert(NewPackageStats(now, pkgSize))
|
|
||||||
|
|
||||||
ss.SendAppendRequestCnt++
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/go.net/context"
|
"code.google.com/p/go.net/context"
|
||||||
pb "github.com/coreos/etcd/etcdserver2/etcdserverpb"
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
"github.com/coreos/etcd/raft"
|
"github.com/coreos/etcd/raft"
|
||||||
"github.com/coreos/etcd/raft/raftpb"
|
"github.com/coreos/etcd/raft/raftpb"
|
||||||
"github.com/coreos/etcd/store"
|
"github.com/coreos/etcd/store"
|
@ -5,9 +5,10 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.google.com/p/go.net/context"
|
"code.google.com/p/go.net/context"
|
||||||
|
|
||||||
pb "github.com/coreos/etcd/etcdserver2/etcdserverpb"
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
"github.com/coreos/etcd/raft"
|
"github.com/coreos/etcd/raft"
|
||||||
"github.com/coreos/etcd/raft/raftpb"
|
"github.com/coreos/etcd/raft/raftpb"
|
||||||
"github.com/coreos/etcd/store"
|
"github.com/coreos/etcd/store"
|
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
noneId int64 = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
type standby struct {
|
|
||||||
client *v2client
|
|
||||||
peerHub *peerHub
|
|
||||||
|
|
||||||
leader int64
|
|
||||||
leaderAddr string
|
|
||||||
mu sync.RWMutex
|
|
||||||
clusterConf *conf.ClusterConfig
|
|
||||||
|
|
||||||
*http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStandby(client *v2client, peerHub *peerHub) *standby {
|
|
||||||
s := &standby{
|
|
||||||
client: client,
|
|
||||||
peerHub: peerHub,
|
|
||||||
|
|
||||||
leader: noneId,
|
|
||||||
leaderAddr: "",
|
|
||||||
clusterConf: conf.NewClusterConfig(),
|
|
||||||
|
|
||||||
ServeMux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
s.Handle("/", handlerErr(s.serveRedirect))
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *standby) run(stop chan struct{}) {
|
|
||||||
syncDuration := time.Millisecond * 100
|
|
||||||
nodes := s.peerHub.getSeeds()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(syncDuration):
|
|
||||||
case <-stop:
|
|
||||||
log.Printf("standby.stop\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if update, err := s.syncCluster(nodes); err != nil {
|
|
||||||
log.Printf("standby.run syncErr=\"%v\"", err)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
nodes = update
|
|
||||||
}
|
|
||||||
syncDuration = time.Duration(s.clusterConf.SyncInterval * float64(time.Second))
|
|
||||||
if s.clusterConf.ActiveSize <= len(nodes) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("standby.end\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *standby) leaderInfo() (int64, string) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
return s.leader, s.leaderAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *standby) setLeaderInfo(leader int64, leaderAddr string) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.leader, s.leaderAddr = leader, leaderAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *standby) serveRedirect(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
leader, leaderAddr := s.leaderInfo()
|
|
||||||
if leader == noneId {
|
|
||||||
return fmt.Errorf("no leader in the cluster")
|
|
||||||
}
|
|
||||||
redirectAddr, err := buildRedirectURL(leaderAddr, r.URL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, redirectAddr, http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *standby) syncCluster(nodes map[string]bool) (map[string]bool, error) {
|
|
||||||
for node := range nodes {
|
|
||||||
machines, err := s.client.GetMachines(node)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg, err := s.client.GetClusterConfig(node)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nn := make(map[string]bool)
|
|
||||||
for _, machine := range machines {
|
|
||||||
nn[machine.PeerURL] = true
|
|
||||||
if machine.State == stateLeader {
|
|
||||||
id, err := strconv.ParseInt(machine.Name, 0, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.setLeaderInfo(id, machine.PeerURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.clusterConf = cfg
|
|
||||||
return nn, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unreachable cluster")
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package etcdserver
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,182 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stateFollower = "follower"
|
|
||||||
stateCandidate = "candidate"
|
|
||||||
stateLeader = "leader"
|
|
||||||
)
|
|
||||||
|
|
||||||
// machineMessage represents information about a peer or standby in the registry.
|
|
||||||
type machineMessage struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
State string `json:"state"`
|
|
||||||
ClientURL string `json:"clientURL"`
|
|
||||||
PeerURL string `json:"peerURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type context struct {
|
|
||||||
MinVersion int `json:"minVersion"`
|
|
||||||
MaxVersion int `json:"maxVersion"`
|
|
||||||
ClientURL string `json:"clientURL"`
|
|
||||||
PeerURL string `json:"peerURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveAdminConfig(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
case "PUT":
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, r, p.node.Leader())
|
|
||||||
}
|
|
||||||
c := p.clusterConfig()
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Sanitize()
|
|
||||||
if err := p.setClusterConfig(c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return allow(w, "GET", "PUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(p.clusterConfig())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveAdminMachines(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
name := strings.TrimPrefix(r.URL.Path, v2adminMachinesPrefix)
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
var info interface{}
|
|
||||||
var err error
|
|
||||||
if name != "" {
|
|
||||||
info, err = p.machineMessage(name)
|
|
||||||
} else {
|
|
||||||
info, err = p.allMachineMessages()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(info)
|
|
||||||
case "PUT":
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, r, p.node.Leader())
|
|
||||||
}
|
|
||||||
id, err := strconv.ParseInt(name, 0, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
info := &context{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(info); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return p.add(id, info.PeerURL, info.ClientURL)
|
|
||||||
case "DELETE":
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, r, p.node.Leader())
|
|
||||||
}
|
|
||||||
id, err := strconv.ParseInt(name, 0, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return p.remove(id)
|
|
||||||
default:
|
|
||||||
return allow(w, "GET", "PUT", "DELETE")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) clusterConfig() *conf.ClusterConfig {
|
|
||||||
c := conf.NewClusterConfig()
|
|
||||||
// This is used for backward compatibility because it doesn't
|
|
||||||
// set cluster config in older version.
|
|
||||||
if e, err := p.Store.Get(v2configKVPrefix, false, false); err == nil {
|
|
||||||
json.Unmarshal([]byte(*e.Node.Value), c)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) setClusterConfig(c *conf.ClusterConfig) error {
|
|
||||||
b, err := json.Marshal(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := p.Set(v2configKVPrefix, false, string(b), store.Permanent); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// machineMessage returns the machineMessage of the given name
|
|
||||||
func (p *participant) machineMessage(name string) (*machineMessage, error) {
|
|
||||||
pp := filepath.Join(v2machineKVPrefix, name)
|
|
||||||
e, err := p.Store.Get(pp, false, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
lead := fmt.Sprint(p.node.Leader())
|
|
||||||
return newMachineMessage(e.Node, lead), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) allMachineMessages() ([]*machineMessage, error) {
|
|
||||||
e, err := p.Store.Get(v2machineKVPrefix, false, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
lead := fmt.Sprint(p.node.Leader())
|
|
||||||
ms := make([]*machineMessage, len(e.Node.Nodes))
|
|
||||||
for i, n := range e.Node.Nodes {
|
|
||||||
ms[i] = newMachineMessage(n, lead)
|
|
||||||
}
|
|
||||||
return ms, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMachineMessage(n *store.NodeExtern, lead string) *machineMessage {
|
|
||||||
_, name := filepath.Split(n.Key)
|
|
||||||
q, err := url.ParseQuery(*n.Value)
|
|
||||||
if err != nil {
|
|
||||||
panic("fail to parse the info for machine " + name)
|
|
||||||
}
|
|
||||||
m := &machineMessage{
|
|
||||||
Name: name,
|
|
||||||
State: stateFollower,
|
|
||||||
ClientURL: q["etcd"][0],
|
|
||||||
PeerURL: q["raft"][0],
|
|
||||||
}
|
|
||||||
if name == lead {
|
|
||||||
m.State = stateLeader
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/raft"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) v2apply(index int64, ent raft.Entry) {
|
|
||||||
var ret interface{}
|
|
||||||
var e *store.Event
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var cmd Cmd
|
|
||||||
if err := cmd.Unmarshal(ent.Data); err != nil {
|
|
||||||
log.Printf("id=%x participant.store.apply decodeErr=\"%v\"\n", p.id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cmd.Type {
|
|
||||||
case stset:
|
|
||||||
e, err = p.Store.Set(cmd.Key, *cmd.Dir, *cmd.Value, mustUnmarshalTime(cmd.Time))
|
|
||||||
case stupdate:
|
|
||||||
e, err = p.Store.Update(cmd.Key, *cmd.Value, mustUnmarshalTime(cmd.Time))
|
|
||||||
case stcreate:
|
|
||||||
e, err = p.Store.Create(cmd.Key, *cmd.Dir, *cmd.Value, *cmd.Unique, mustUnmarshalTime(cmd.Time))
|
|
||||||
case stdelete:
|
|
||||||
e, err = p.Store.Delete(cmd.Key, *cmd.Dir, *cmd.Recursive)
|
|
||||||
case stcad:
|
|
||||||
e, err = p.Store.CompareAndDelete(cmd.Key, *cmd.PrevValue, *cmd.PrevIndex)
|
|
||||||
case stcas:
|
|
||||||
e, err = p.Store.CompareAndSwap(cmd.Key, *cmd.PrevValue, *cmd.PrevIndex, *cmd.Value, mustUnmarshalTime(cmd.Time))
|
|
||||||
case stqget:
|
|
||||||
e, err = p.Store.Get(cmd.Key, *cmd.Recursive, *cmd.Sorted)
|
|
||||||
case stsync:
|
|
||||||
p.Store.DeleteExpiredKeys(mustUnmarshalTime(cmd.Time))
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
log.Printf("id=%x participant.store.apply err=\"unexpected command type %s\"\n", p.id, cmd.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ent.Term > p.node.term {
|
|
||||||
p.node.term = ent.Term
|
|
||||||
for k, v := range p.node.result {
|
|
||||||
if k.term < p.node.term {
|
|
||||||
v <- fmt.Errorf("proposal lost due to leader election")
|
|
||||||
delete(p.node.result, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w := wait{index, ent.Term}
|
|
||||||
if p.node.result[w] == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ret = err
|
|
||||||
} else {
|
|
||||||
ret = e
|
|
||||||
}
|
|
||||||
p.node.result[w] <- ret
|
|
||||||
delete(p.node.result, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustMarshalTime(t *time.Time) []byte {
|
|
||||||
b, err := t.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustUnmarshalTime(b []byte) time.Time {
|
|
||||||
var time time.Time
|
|
||||||
if err := time.UnmarshalBinary(b); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return time
|
|
||||||
}
|
|
@ -1,254 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
// v2client sends various requests using HTTP API.
|
|
||||||
// It is different from raft communication, and doesn't record anything in the log.
|
|
||||||
// The argument url is required to contain scheme and host only, and
|
|
||||||
// there is no trailing slash in it.
|
|
||||||
// Public functions return "etcd/error".Error intentionally to figure out
|
|
||||||
// etcd error code easily.
|
|
||||||
type v2client struct {
|
|
||||||
stopped bool
|
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
http.Client
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClient(tc *tls.Config) *v2client {
|
|
||||||
tr := new(http.Transport)
|
|
||||||
tr.TLSClientConfig = tc
|
|
||||||
return &v2client{Client: http.Client{Transport: tr}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) CloseConnections() {
|
|
||||||
c.stop()
|
|
||||||
c.wg.Wait()
|
|
||||||
tr := c.Transport.(*http.Transport)
|
|
||||||
tr.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckVersion returns true when the version check on the server returns 200.
|
|
||||||
func (c *v2client) CheckVersion(url string, version int) (bool, *etcdErr.Error) {
|
|
||||||
if c.runOne() == false {
|
|
||||||
return false, clientError(errors.New("v2_client is stopped"))
|
|
||||||
}
|
|
||||||
defer c.finishOne()
|
|
||||||
|
|
||||||
resp, err := c.Get(url + fmt.Sprintf("/version/%d/check", version))
|
|
||||||
if err != nil {
|
|
||||||
return false, clientError(err)
|
|
||||||
}
|
|
||||||
c.readBody(resp.Body)
|
|
||||||
|
|
||||||
return resp.StatusCode == 200, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVersion fetches the peer version of a cluster.
|
|
||||||
func (c *v2client) GetVersion(url string) (int, *etcdErr.Error) {
|
|
||||||
if c.runOne() == false {
|
|
||||||
return 0, clientError(errors.New("v2_client is stopped"))
|
|
||||||
}
|
|
||||||
defer c.finishOne()
|
|
||||||
|
|
||||||
resp, err := c.Get(url + "/version")
|
|
||||||
if err != nil {
|
|
||||||
return 0, clientError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := c.readBody(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return 0, clientError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse version number.
|
|
||||||
version, err := strconv.Atoi(string(body))
|
|
||||||
if err != nil {
|
|
||||||
return 0, clientError(err)
|
|
||||||
}
|
|
||||||
return version, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) GetMachines(url string) ([]*machineMessage, *etcdErr.Error) {
|
|
||||||
if c.runOne() == false {
|
|
||||||
return nil, clientError(errors.New("v2_client is stopped"))
|
|
||||||
}
|
|
||||||
defer c.finishOne()
|
|
||||||
|
|
||||||
resp, err := c.Get(url + "/v2/admin/machines/")
|
|
||||||
if err != nil {
|
|
||||||
return nil, clientError(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, c.readErrorBody(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := new([]*machineMessage)
|
|
||||||
if uerr := c.readJSONBody(resp.Body, msgs); uerr != nil {
|
|
||||||
return nil, uerr
|
|
||||||
}
|
|
||||||
return *msgs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) GetClusterConfig(url string) (*conf.ClusterConfig, *etcdErr.Error) {
|
|
||||||
if c.runOne() == false {
|
|
||||||
return nil, clientError(errors.New("v2_client is stopped"))
|
|
||||||
}
|
|
||||||
defer c.finishOne()
|
|
||||||
|
|
||||||
resp, err := c.Get(url + "/v2/admin/config")
|
|
||||||
if err != nil {
|
|
||||||
return nil, clientError(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, c.readErrorBody(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := new(conf.ClusterConfig)
|
|
||||||
if uerr := c.readJSONBody(resp.Body, cfg); uerr != nil {
|
|
||||||
return nil, uerr
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddMachine adds machine to the cluster.
|
|
||||||
// The first return value is the commit index of join command.
|
|
||||||
func (c *v2client) AddMachine(url string, name string, info *context) *etcdErr.Error {
|
|
||||||
if c.runOne() == false {
|
|
||||||
return clientError(errors.New("v2_client is stopped"))
|
|
||||||
}
|
|
||||||
defer c.finishOne()
|
|
||||||
|
|
||||||
b, _ := json.Marshal(info)
|
|
||||||
url = url + "/v2/admin/machines/" + name
|
|
||||||
|
|
||||||
resp, err := c.put(url, b)
|
|
||||||
if err != nil {
|
|
||||||
return clientError(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return c.readErrorBody(resp.Body)
|
|
||||||
}
|
|
||||||
c.readBody(resp.Body)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) readErrorBody(body io.ReadCloser) *etcdErr.Error {
|
|
||||||
b, err := c.readBody(body)
|
|
||||||
if err != nil {
|
|
||||||
return clientError(err)
|
|
||||||
}
|
|
||||||
uerr := &etcdErr.Error{}
|
|
||||||
if err := json.Unmarshal(b, uerr); err != nil {
|
|
||||||
str := strings.TrimSpace(string(b))
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeClientInternal, str, 0)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) readJSONBody(body io.ReadCloser, val interface{}) *etcdErr.Error {
|
|
||||||
if err := json.NewDecoder(body).Decode(val); err != nil {
|
|
||||||
return clientError(err)
|
|
||||||
}
|
|
||||||
c.readBody(body)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) readBody(body io.ReadCloser) ([]byte, error) {
|
|
||||||
b, err := ioutil.ReadAll(body)
|
|
||||||
body.Close()
|
|
||||||
return b, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// put sends server side PUT request.
|
|
||||||
// It always follows redirects instead of stopping according to RFC 2616.
|
|
||||||
func (c *v2client) put(urlStr string, body []byte) (*http.Response, error) {
|
|
||||||
return c.doAlwaysFollowingRedirects("PUT", urlStr, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) doAlwaysFollowingRedirects(method string, urlStr string, body []byte) (resp *http.Response, err error) {
|
|
||||||
var req *http.Request
|
|
||||||
|
|
||||||
for redirect := 0; redirect < 10; redirect++ {
|
|
||||||
req, err = http.NewRequest(method, urlStr, bytes.NewBuffer(body))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp, err = c.Do(req); err != nil {
|
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusMovedPermanently || resp.StatusCode == http.StatusTemporaryRedirect {
|
|
||||||
resp.Body.Close()
|
|
||||||
if urlStr = resp.Header.Get("Location"); urlStr == "" {
|
|
||||||
err = errors.New(fmt.Sprintf("%d response missing Location header", resp.StatusCode))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New("stopped after 10 redirects")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) runOne() bool {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
if c.stopped {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
c.wg.Add(1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) finishOne() {
|
|
||||||
c.wg.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *v2client) stop() {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.stopped = true
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func clientError(err error) *etcdErr.Error {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeClientInternal, err.Error(), 0)
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) serveValue(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
return p.GetHandler(w, r)
|
|
||||||
case "HEAD":
|
|
||||||
w = &HEADResponseWriter{w}
|
|
||||||
return p.GetHandler(w, r)
|
|
||||||
case "PUT":
|
|
||||||
return p.PutHandler(w, r)
|
|
||||||
case "POST":
|
|
||||||
return p.PostHandler(w, r)
|
|
||||||
case "DELETE":
|
|
||||||
return p.DeleteHandler(w, r)
|
|
||||||
}
|
|
||||||
return allow(w, "GET", "PUT", "POST", "DELETE", "HEAD")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveMachines(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
return allow(w, "GET")
|
|
||||||
}
|
|
||||||
v, err := p.Store.Get(v2machineKVPrefix, false, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
ns := make([]string, len(v.Node.Nodes))
|
|
||||||
for i, n := range v.Node.Nodes {
|
|
||||||
m, err := url.ParseQuery(*n.Value)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ns[i] = m["etcd"][0]
|
|
||||||
}
|
|
||||||
w.Write([]byte(strings.Join(ns, ",")))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveLeader(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
return allow(w, "GET")
|
|
||||||
}
|
|
||||||
if p, ok := p.peerHub.peers[p.node.Leader()]; ok {
|
|
||||||
w.Write([]byte(p.url))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("no leader")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveSelfStats(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
p.serverStats.LeaderInfo.Uptime = time.Now().Sub(p.serverStats.LeaderInfo.StartTime).String()
|
|
||||||
|
|
||||||
if p.node.IsLeader() {
|
|
||||||
p.serverStats.LeaderInfo.Name = fmt.Sprint(p.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
queue := p.serverStats.sendRateQueue
|
|
||||||
p.serverStats.SendingPkgRate, p.serverStats.SendingBandwidthRate = queue.Rate()
|
|
||||||
|
|
||||||
queue = p.serverStats.recvRateQueue
|
|
||||||
p.serverStats.RecvingPkgRate, p.serverStats.RecvingBandwidthRate = queue.Rate()
|
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(p.serverStats)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveLeaderStats(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, req, p.node.Leader())
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
return json.NewEncoder(w).Encode(p.peerHub.followersStats)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveStoreStats(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(p.Store.JsonStats())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type handlerErr func(w http.ResponseWriter, r *http.Request) error
|
|
||||||
|
|
||||||
func (eh handlerErr) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := eh(w, r)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
w = &HEADResponseWriter{w}
|
|
||||||
}
|
|
||||||
|
|
||||||
if etcdErr, ok := err.(*etcdErr.Error); ok {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
etcdErr.Write(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("HTTP.serve: req=%s err=\"%v\"\n", r.URL, err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func allow(w http.ResponseWriter, m ...string) error {
|
|
||||||
w.Header().Set("Allow", strings.Join(m, ","))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HEADResponseWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *HEADResponseWriter) Write([]byte) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) redirect(w http.ResponseWriter, r *http.Request, id int64) error {
|
|
||||||
e, err := p.Store.Get(fmt.Sprintf("%v/%d", v2machineKVPrefix, p.node.Leader()), false, false)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("redirect cannot find node %d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := url.ParseQuery(*e.Node.Value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse node entry: %s", *e.Node.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectAddr, err := buildRedirectURL(m["etcd"][0], r.URL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, redirectAddr, http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRedirectURL(redirectAddr string, originalURL *url.URL) (string, error) {
|
|
||||||
redirectURL, err := url.Parse(redirectAddr)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("cannot parse url: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectURL.Path = originalURL.Path
|
|
||||||
redirectURL.RawQuery = originalURL.RawQuery
|
|
||||||
redirectURL.Fragment = originalURL.Fragment
|
|
||||||
return redirectURL.String(), nil
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) DeleteHandler(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, req, p.node.Leader())
|
|
||||||
}
|
|
||||||
|
|
||||||
key := req.URL.Path[len("/v2/keys"):]
|
|
||||||
|
|
||||||
recursive := (req.FormValue("recursive") == "true")
|
|
||||||
dir := (req.FormValue("dir") == "true")
|
|
||||||
|
|
||||||
req.ParseForm()
|
|
||||||
_, valueOk := req.Form["prevValue"]
|
|
||||||
_, indexOk := req.Form["prevIndex"]
|
|
||||||
|
|
||||||
if !valueOk && !indexOk {
|
|
||||||
return p.serveDelete(w, req, key, dir, recursive)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
prevIndex := uint64(0)
|
|
||||||
prevValue := req.Form.Get("prevValue")
|
|
||||||
|
|
||||||
if indexOk {
|
|
||||||
prevIndexStr := req.Form.Get("prevIndex")
|
|
||||||
prevIndex, err = strconv.ParseUint(prevIndexStr, 10, 64)
|
|
||||||
|
|
||||||
// bad previous index
|
|
||||||
if err != nil {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "CompareAndDelete", p.Store.Index())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if valueOk {
|
|
||||||
if prevValue == "" {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodePrevValueRequired, "CompareAndDelete", p.Store.Index())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p.serveCAD(w, req, key, prevValue, prevIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveDelete(w http.ResponseWriter, req *http.Request, key string, dir, recursive bool) error {
|
|
||||||
ret, err := p.Delete(key, dir, recursive)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveCAD(w http.ResponseWriter, req *http.Request, key string, prevValue string, prevIndex uint64) error {
|
|
||||||
ret, err := p.CAD(key, prevValue, prevIndex)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,293 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/conf"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMachinesEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 3}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
w := make([]string, cl.Size)
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
w[i] = cl.URL(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
r, err := http.Get(cl.URL(i) + v2machinePrefix)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadAll(r.Body)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
g := strings.Split(string(b), ",")
|
|
||||||
sort.Strings(g)
|
|
||||||
if !reflect.DeepEqual(w, g) {
|
|
||||||
t.Errorf("machines = %v, want %v", g, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLeaderEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 3}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
us := make([]string, cl.Size)
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
us[i] = cl.URL(i)
|
|
||||||
}
|
|
||||||
// todo(xiangli) change this to raft port...
|
|
||||||
w := cl.URL(0) + "/raft"
|
|
||||||
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
r, err := http.Get(cl.URL(i) + v2LeaderPrefix)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadAll(r.Body)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if string(b) != w {
|
|
||||||
t.Errorf("leader = %v, want %v", string(b), w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreStatsEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
resp, err := http.Get(cl.URL(0) + v2StoreStatsPrefix)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
stats := new(store.Stats)
|
|
||||||
d := json.NewDecoder(resp.Body)
|
|
||||||
err = d.Decode(stats)
|
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats.SetSuccess != 1 {
|
|
||||||
t.Errorf("setSuccess = %d, want 1", stats.SetSuccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAdminConfigEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
r, err := http.Get(cl.URL(0) + v2adminConfigPrefix)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
if g := r.StatusCode; g != 200 {
|
|
||||||
t.Errorf("status = %d, want %d", g, 200)
|
|
||||||
}
|
|
||||||
if g := r.Header.Get("Content-Type"); g != "application/json" {
|
|
||||||
t.Errorf("ContentType = %s, want application/json", g)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := new(conf.ClusterConfig)
|
|
||||||
err = json.NewDecoder(r.Body).Decode(cfg)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
w := conf.NewClusterConfig()
|
|
||||||
if !reflect.DeepEqual(cfg, w) {
|
|
||||||
t.Errorf("config = %+v, want %+v", cfg, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutAdminConfigEndPoint(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
c, wc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
`{"activeSize":1,"removeDelay":1,"syncInterval":1}`,
|
|
||||||
`{"activeSize":3,"removeDelay":2,"syncInterval":1}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`{"activeSize":5,"removeDelay":20.5,"syncInterval":1.5}`,
|
|
||||||
`{"activeSize":5,"removeDelay":20.5,"syncInterval":1.5}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`{"activeSize":5 , "removeDelay":20 , "syncInterval": 2 }`,
|
|
||||||
`{"activeSize":5,"removeDelay":20,"syncInterval":2}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`{"activeSize":3, "removeDelay":60}`,
|
|
||||||
`{"activeSize":3,"removeDelay":60,"syncInterval":5}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
cl := &testCluster{Size: 1}
|
|
||||||
cl.Start()
|
|
||||||
index := cl.Participant(0).Index()
|
|
||||||
|
|
||||||
r, err := NewTestClient().Put(cl.URL(0)+v2adminConfigPrefix, "application/json", bytes.NewBufferString(tt.c))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadAll(r.Body)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
if wbody := append([]byte(tt.wc), '\n'); !reflect.DeepEqual(b, wbody) {
|
|
||||||
t.Errorf("#%d: put result = %s, want %s", i, b, wbody)
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := cl.Participant(0).Watch(v2configKVPrefix, false, false, index)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
e := <-w.EventChan
|
|
||||||
if g := *e.Node.Value; g != tt.wc {
|
|
||||||
t.Errorf("#%d: %s = %s, want %s", i, v2configKVPrefix, g, tt.wc)
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAdminMachineEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 3}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
for j := 0; j < cl.Size; j++ {
|
|
||||||
name := fmt.Sprint(cl.Id(i))
|
|
||||||
r, err := http.Get(cl.URL(j) + v2adminMachinesPrefix + name)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if g := r.StatusCode; g != 200 {
|
|
||||||
t.Errorf("#%d on %d: status = %d, want %d", i, j, g, 200)
|
|
||||||
}
|
|
||||||
if g := r.Header.Get("Content-Type"); g != "application/json" {
|
|
||||||
t.Errorf("#%d on %d: ContentType = %s, want application/json", i, j, g)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := new(machineMessage)
|
|
||||||
err = json.NewDecoder(r.Body).Decode(m)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wm := &machineMessage{
|
|
||||||
Name: name,
|
|
||||||
State: stateFollower,
|
|
||||||
ClientURL: cl.URL(i),
|
|
||||||
PeerURL: cl.URL(i),
|
|
||||||
}
|
|
||||||
if i == 0 {
|
|
||||||
wm.State = stateLeader
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(m, wm) {
|
|
||||||
t.Errorf("#%d on %d: body = %+v, want %+v", i, j, m, wm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAdminMachinesEndPoint(t *testing.T) {
|
|
||||||
cl := &testCluster{Size: 3}
|
|
||||||
cl.Start()
|
|
||||||
|
|
||||||
w := make([]*machineMessage, cl.Size)
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
w[i] = &machineMessage{
|
|
||||||
Name: fmt.Sprint(cl.Id(i)),
|
|
||||||
State: stateFollower,
|
|
||||||
ClientURL: cl.URL(i),
|
|
||||||
PeerURL: cl.URL(i),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w[0].State = stateLeader
|
|
||||||
|
|
||||||
for i := 0; i < cl.Size; i++ {
|
|
||||||
r, err := http.Get(cl.URL(i) + v2adminMachinesPrefix)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m := make([]*machineMessage, 0)
|
|
||||||
err = json.NewDecoder(r.Body).Decode(&m)
|
|
||||||
r.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sm := machineSlice(m)
|
|
||||||
sw := machineSlice(w)
|
|
||||||
sort.Sort(sm)
|
|
||||||
sort.Sort(sw)
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(sm, sw) {
|
|
||||||
t.Errorf("on %d: machines = %+v, want %+v", i, sm, sw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
// int64Slice implements sort interface
|
|
||||||
type machineSlice []*machineMessage
|
|
||||||
|
|
||||||
func (s machineSlice) Len() int { return len(s) }
|
|
||||||
func (s machineSlice) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
|
||||||
func (s machineSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) GetHandler(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if req.FormValue("consistent") == "true" && !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, req, p.node.Leader())
|
|
||||||
}
|
|
||||||
|
|
||||||
key := req.URL.Path[len("/v2/keys"):]
|
|
||||||
recursive := (req.FormValue("recursive") == "true")
|
|
||||||
sort := (req.FormValue("sorted") == "true")
|
|
||||||
waitIndex := req.FormValue("waitIndex")
|
|
||||||
stream := (req.FormValue("stream") == "true")
|
|
||||||
if req.FormValue("quorum") == "true" {
|
|
||||||
return p.handleQuorumGet(key, recursive, sort, w, req)
|
|
||||||
}
|
|
||||||
if req.FormValue("wait") == "true" {
|
|
||||||
return p.handleWatch(key, recursive, stream, waitIndex, w, req)
|
|
||||||
}
|
|
||||||
return p.handleGet(key, recursive, sort, w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) handleWatch(key string, recursive, stream bool, waitIndex string, w http.ResponseWriter, req *http.Request) error {
|
|
||||||
// Create a command to watch from a given index (default 0).
|
|
||||||
var sinceIndex uint64 = 0
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if waitIndex != "" {
|
|
||||||
sinceIndex, err = strconv.ParseUint(waitIndex, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Watch From Index", p.Store.Index())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher, err := p.Store.Watch(key, recursive, stream, sinceIndex)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cn, _ := w.(http.CloseNotifier)
|
|
||||||
closeChan := cn.CloseNotify()
|
|
||||||
|
|
||||||
p.writeHeaders(w)
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
|
|
||||||
if stream {
|
|
||||||
// watcher hub will not help to remove stream watcher
|
|
||||||
// so we need to remove here
|
|
||||||
defer watcher.Remove()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-closeChan:
|
|
||||||
return nil
|
|
||||||
case event, ok := <-watcher.EventChan:
|
|
||||||
if !ok {
|
|
||||||
// If the channel is closed this may be an indication of
|
|
||||||
// that notifications are much more than we are able to
|
|
||||||
// send to the client in time. Then we simply end streaming.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if req.Method == "HEAD" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := json.Marshal(event)
|
|
||||||
_, err := w.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-closeChan:
|
|
||||||
watcher.Remove()
|
|
||||||
case event := <-watcher.EventChan:
|
|
||||||
if req.Method == "HEAD" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, _ := json.Marshal(event)
|
|
||||||
w.Write(b)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) handleGet(key string, recursive, sort bool, w http.ResponseWriter, req *http.Request) error {
|
|
||||||
event, err := p.Store.Get(key, recursive, sort)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.writeHeaders(w)
|
|
||||||
if req.Method == "HEAD" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(event)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("handleGet: ", err))
|
|
||||||
}
|
|
||||||
w.Write(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) handleQuorumGet(key string, recursive, sort bool, w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if req.Method == "HEAD" {
|
|
||||||
return fmt.Errorf("not support HEAD")
|
|
||||||
}
|
|
||||||
event, err := p.QuorumGet(key, recursive, sort)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.handleRet(w, event)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) writeHeaders(w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(p.Store.Index()))
|
|
||||||
// TODO(xiangli): raft-index and term
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) PostHandler(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, req, p.node.Leader())
|
|
||||||
}
|
|
||||||
|
|
||||||
key := req.URL.Path[len("/v2/keys"):]
|
|
||||||
|
|
||||||
value := req.FormValue("value")
|
|
||||||
dir := (req.FormValue("dir") == "true")
|
|
||||||
expireTime, err := store.TTL(req.FormValue("ttl"))
|
|
||||||
if err != nil {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Create", p.Store.Index())
|
|
||||||
}
|
|
||||||
|
|
||||||
ret, err := p.Create(key, dir, value, expireTime, true)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
etcdErr "github.com/coreos/etcd/error"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) PutHandler(w http.ResponseWriter, req *http.Request) error {
|
|
||||||
if !p.node.IsLeader() {
|
|
||||||
return p.redirect(w, req, p.node.Leader())
|
|
||||||
}
|
|
||||||
|
|
||||||
key := req.URL.Path[len("/v2/keys"):]
|
|
||||||
|
|
||||||
req.ParseForm()
|
|
||||||
|
|
||||||
value := req.Form.Get("value")
|
|
||||||
dir := (req.FormValue("dir") == "true")
|
|
||||||
|
|
||||||
expireTime, err := store.TTL(req.Form.Get("ttl"))
|
|
||||||
if err != nil {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Update", p.Store.Index())
|
|
||||||
}
|
|
||||||
|
|
||||||
prevValue, valueOk := firstValue(req.Form, "prevValue")
|
|
||||||
prevIndexStr, indexOk := firstValue(req.Form, "prevIndex")
|
|
||||||
prevExist, existOk := firstValue(req.Form, "prevExist")
|
|
||||||
|
|
||||||
// Set handler: create a new node or replace the old one.
|
|
||||||
if !valueOk && !indexOk && !existOk {
|
|
||||||
return p.serveSet(w, req, key, dir, value, expireTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update with test
|
|
||||||
if existOk {
|
|
||||||
if prevExist == "false" {
|
|
||||||
// Create command: create a new node. Fail, if a node already exists
|
|
||||||
// Ignore prevIndex and prevValue
|
|
||||||
return p.serveCreate(w, req, key, dir, value, expireTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prevExist == "true" && !indexOk && !valueOk {
|
|
||||||
return p.serveUpdate(w, req, key, value, expireTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var prevIndex uint64
|
|
||||||
|
|
||||||
if indexOk {
|
|
||||||
prevIndex, err = strconv.ParseUint(prevIndexStr, 10, 64)
|
|
||||||
|
|
||||||
// bad previous index
|
|
||||||
if err != nil {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "CompareAndSwap", p.Store.Index())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
prevIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if valueOk {
|
|
||||||
if prevValue == "" {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodePrevValueRequired, "CompareAndSwap", p.Store.Index())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.serveCAS(w, req, key, value, prevValue, prevIndex, expireTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) handleRet(w http.ResponseWriter, ret *store.Event) {
|
|
||||||
b, _ := json.Marshal(ret)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
// etcd index should be the same as the event index
|
|
||||||
// which is also the last modified index of the node
|
|
||||||
w.Header().Add("X-Etcd-Index", fmt.Sprint(ret.Index()))
|
|
||||||
// w.Header().Add("X-Raft-Index", fmt.Sprint(p.CommitIndex()))
|
|
||||||
// w.Header().Add("X-Raft-Term", fmt.Sprint(p.Term()))
|
|
||||||
|
|
||||||
if ret.IsCreated() {
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveSet(w http.ResponseWriter, req *http.Request, key string, dir bool, value string, expireTime time.Time) error {
|
|
||||||
ret, err := p.Set(key, dir, value, expireTime)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveCreate(w http.ResponseWriter, req *http.Request, key string, dir bool, value string, expireTime time.Time) error {
|
|
||||||
ret, err := p.Create(key, dir, value, expireTime, false)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveUpdate(w http.ResponseWriter, req *http.Request, key, value string, expireTime time.Time) error {
|
|
||||||
// Update should give at least one option
|
|
||||||
if value == "" && expireTime.Sub(store.Permanent) == 0 {
|
|
||||||
return etcdErr.NewError(etcdErr.EcodeValueOrTTLRequired, "Update", p.Store.Index())
|
|
||||||
}
|
|
||||||
ret, err := p.Update(key, value, expireTime)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) serveCAS(w http.ResponseWriter, req *http.Request, key, value, prevValue string, prevIndex uint64, expireTime time.Time) error {
|
|
||||||
ret, err := p.CAS(key, value, prevValue, prevIndex, expireTime)
|
|
||||||
if err == nil {
|
|
||||||
p.handleRet(w, ret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstValue(f url.Values, key string) (string, bool) {
|
|
||||||
l, ok := f[key]
|
|
||||||
if !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return l[0], true
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/raft"
|
|
||||||
)
|
|
||||||
|
|
||||||
type v2Proposal struct {
|
|
||||||
data []byte
|
|
||||||
ret chan interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type wait struct {
|
|
||||||
index int64
|
|
||||||
term int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type v2Raft struct {
|
|
||||||
*raft.Node
|
|
||||||
result map[wait]chan interface{}
|
|
||||||
term int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *v2Raft) Propose(p v2Proposal) {
|
|
||||||
if !r.Node.IsLeader() {
|
|
||||||
p.ret <- fmt.Errorf("not leader")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.Node.Propose(p.data)
|
|
||||||
r.result[wait{r.Index(), r.Term()}] = p.ret
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *v2Raft) Sync() {
|
|
||||||
if !r.Node.IsLeader() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t := time.Now()
|
|
||||||
sync := &Cmd{Type: stsync, Time: mustMarshalTime(&t)}
|
|
||||||
data, err := sync.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
r.Node.Propose(data)
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stset = iota
|
|
||||||
stcreate
|
|
||||||
stupdate
|
|
||||||
stcas
|
|
||||||
stdelete
|
|
||||||
stcad
|
|
||||||
stqget
|
|
||||||
stsync
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *participant) Set(key string, dir bool, value string, expireTime time.Time) (*store.Event, error) {
|
|
||||||
set := &Cmd{Type: stset, Key: key, Dir: &dir, Value: &value, Time: mustMarshalTime(&expireTime)}
|
|
||||||
return p.do(set)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) Create(key string, dir bool, value string, expireTime time.Time, unique bool) (*store.Event, error) {
|
|
||||||
create := &Cmd{Type: stcreate, Key: key, Dir: &dir, Value: &value, Time: mustMarshalTime(&expireTime), Unique: &unique}
|
|
||||||
return p.do(create)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) Update(key string, value string, expireTime time.Time) (*store.Event, error) {
|
|
||||||
update := &Cmd{Type: stupdate, Key: key, Value: &value, Time: mustMarshalTime(&expireTime)}
|
|
||||||
return p.do(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) CAS(key, value, prevValue string, prevIndex uint64, expireTime time.Time) (*store.Event, error) {
|
|
||||||
cas := &Cmd{Type: stcas, Key: key, Value: &value, PrevValue: &prevValue, PrevIndex: &prevIndex, Time: mustMarshalTime(&expireTime)}
|
|
||||||
return p.do(cas)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) Delete(key string, dir, recursive bool) (*store.Event, error) {
|
|
||||||
d := &Cmd{Type: stdelete, Key: key, Dir: &dir, Recursive: &recursive}
|
|
||||||
return p.do(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) CAD(key string, prevValue string, prevIndex uint64) (*store.Event, error) {
|
|
||||||
cad := &Cmd{Type: stcad, Key: key, PrevValue: &prevValue, PrevIndex: &prevIndex}
|
|
||||||
return p.do(cad)
|
|
||||||
}
|
|
||||||
func (p *participant) QuorumGet(key string, recursive, sorted bool) (*store.Event, error) {
|
|
||||||
get := &Cmd{Type: stqget, Key: key, Recursive: &recursive, Sorted: &sorted}
|
|
||||||
return p.do(get)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *participant) do(c *Cmd) (*store.Event, error) {
|
|
||||||
data, err := c.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pp := v2Proposal{
|
|
||||||
data: data,
|
|
||||||
ret: make(chan interface{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.proposal <- pp:
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unable to send out the proposal")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret interface{}
|
|
||||||
select {
|
|
||||||
case ret = <-pp.ret:
|
|
||||||
case <-p.stopNotifyc:
|
|
||||||
return nil, fmt.Errorf("stop serving")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t := ret.(type) {
|
|
||||||
case *store.Event:
|
|
||||||
return t, nil
|
|
||||||
case error:
|
|
||||||
return nil, t
|
|
||||||
default:
|
|
||||||
panic("server.do: unexpected return type")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,454 +0,0 @@
|
|||||||
// Code generated by protoc-gen-gogo.
|
|
||||||
// source: v2_store_cmd.proto
|
|
||||||
// DO NOT EDIT!
|
|
||||||
|
|
||||||
/*
|
|
||||||
Package etcd is a generated protocol buffer package.
|
|
||||||
|
|
||||||
It is generated from these files:
|
|
||||||
v2_store_cmd.proto
|
|
||||||
|
|
||||||
It has these top-level messages:
|
|
||||||
Cmd
|
|
||||||
*/
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import proto "code.google.com/p/gogoprotobuf/proto"
|
|
||||||
import json "encoding/json"
|
|
||||||
import math "math"
|
|
||||||
|
|
||||||
// discarding unused import gogoproto "code.google.com/p/gogoprotobuf/gogoproto/gogo.pb"
|
|
||||||
|
|
||||||
import io "io"
|
|
||||||
import code_google_com_p_gogoprotobuf_proto "code.google.com/p/gogoprotobuf/proto"
|
|
||||||
|
|
||||||
// Reference proto, json, and math imports to suppress error if they are not otherwise used.
|
|
||||||
var _ = proto.Marshal
|
|
||||||
var _ = &json.SyntaxError{}
|
|
||||||
var _ = math.Inf
|
|
||||||
|
|
||||||
type Cmd struct {
|
|
||||||
Type int32 `protobuf:"varint,1,req,name=type" json:"type"`
|
|
||||||
Key string `protobuf:"bytes,2,req,name=key" json:"key"`
|
|
||||||
Value *string `protobuf:"bytes,3,opt,name=value" json:"value,omitempty"`
|
|
||||||
PrevValue *string `protobuf:"bytes,4,opt,name=prevValue" json:"prevValue,omitempty"`
|
|
||||||
PrevIndex *uint64 `protobuf:"varint,5,opt,name=prevIndex" json:"prevIndex,omitempty"`
|
|
||||||
Dir *bool `protobuf:"varint,6,opt,name=dir" json:"dir,omitempty"`
|
|
||||||
Recursive *bool `protobuf:"varint,7,opt,name=recursive" json:"recursive,omitempty"`
|
|
||||||
Unique *bool `protobuf:"varint,8,opt,name=unique" json:"unique,omitempty"`
|
|
||||||
Sorted *bool `protobuf:"varint,9,opt,name=sorted" json:"sorted,omitempty"`
|
|
||||||
Time []byte `protobuf:"bytes,10,opt,name=time" json:"time,omitempty"`
|
|
||||||
XXX_unrecognized []byte `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Cmd) Reset() { *m = Cmd{} }
|
|
||||||
func (m *Cmd) String() string { return proto.CompactTextString(m) }
|
|
||||||
func (*Cmd) ProtoMessage() {}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
}
|
|
||||||
func (m *Cmd) Unmarshal(data []byte) error {
|
|
||||||
l := len(data)
|
|
||||||
index := 0
|
|
||||||
for index < l {
|
|
||||||
var wire uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
wire |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fieldNum := int32(wire >> 3)
|
|
||||||
wireType := int(wire & 0x7)
|
|
||||||
switch fieldNum {
|
|
||||||
case 1:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
m.Type |= (int32(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
if wireType != 2 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var stringLen uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
stringLen |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postIndex := index + int(stringLen)
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Key = string(data[index:postIndex])
|
|
||||||
index = postIndex
|
|
||||||
case 3:
|
|
||||||
if wireType != 2 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var stringLen uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
stringLen |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postIndex := index + int(stringLen)
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
s := string(data[index:postIndex])
|
|
||||||
m.Value = &s
|
|
||||||
index = postIndex
|
|
||||||
case 4:
|
|
||||||
if wireType != 2 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var stringLen uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
stringLen |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postIndex := index + int(stringLen)
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
s := string(data[index:postIndex])
|
|
||||||
m.PrevValue = &s
|
|
||||||
index = postIndex
|
|
||||||
case 5:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var v uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
v |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.PrevIndex = &v
|
|
||||||
case 6:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var v int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
v |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b := bool(v != 0)
|
|
||||||
m.Dir = &b
|
|
||||||
case 7:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var v int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
v |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b := bool(v != 0)
|
|
||||||
m.Recursive = &b
|
|
||||||
case 8:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var v int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
v |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b := bool(v != 0)
|
|
||||||
m.Unique = &b
|
|
||||||
case 9:
|
|
||||||
if wireType != 0 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var v int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
v |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b := bool(v != 0)
|
|
||||||
m.Sorted = &b
|
|
||||||
case 10:
|
|
||||||
if wireType != 2 {
|
|
||||||
return code_google_com_p_gogoprotobuf_proto.ErrWrongType
|
|
||||||
}
|
|
||||||
var byteLen int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if index >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := data[index]
|
|
||||||
index++
|
|
||||||
byteLen |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postIndex := index + byteLen
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Time = append(m.Time, data[index:postIndex]...)
|
|
||||||
index = postIndex
|
|
||||||
default:
|
|
||||||
var sizeOfWire int
|
|
||||||
for {
|
|
||||||
sizeOfWire++
|
|
||||||
wire >>= 7
|
|
||||||
if wire == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index -= sizeOfWire
|
|
||||||
skippy, err := code_google_com_p_gogoprotobuf_proto.Skip(data[index:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if (index + skippy) > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.XXX_unrecognized = append(m.XXX_unrecognized, data[index:index+skippy]...)
|
|
||||||
index += skippy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *Cmd) Size() (n int) {
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
n += 1 + sovV2StoreCmd(uint64(uint32(m.Type)))
|
|
||||||
l = len(m.Key)
|
|
||||||
n += 1 + l + sovV2StoreCmd(uint64(l))
|
|
||||||
if m.Value != nil {
|
|
||||||
l = len(*m.Value)
|
|
||||||
n += 1 + l + sovV2StoreCmd(uint64(l))
|
|
||||||
}
|
|
||||||
if m.PrevValue != nil {
|
|
||||||
l = len(*m.PrevValue)
|
|
||||||
n += 1 + l + sovV2StoreCmd(uint64(l))
|
|
||||||
}
|
|
||||||
if m.PrevIndex != nil {
|
|
||||||
n += 1 + sovV2StoreCmd(uint64(*m.PrevIndex))
|
|
||||||
}
|
|
||||||
if m.Dir != nil {
|
|
||||||
n += 2
|
|
||||||
}
|
|
||||||
if m.Recursive != nil {
|
|
||||||
n += 2
|
|
||||||
}
|
|
||||||
if m.Unique != nil {
|
|
||||||
n += 2
|
|
||||||
}
|
|
||||||
if m.Sorted != nil {
|
|
||||||
n += 2
|
|
||||||
}
|
|
||||||
if m.Time != nil {
|
|
||||||
l = len(m.Time)
|
|
||||||
n += 1 + l + sovV2StoreCmd(uint64(l))
|
|
||||||
}
|
|
||||||
if m.XXX_unrecognized != nil {
|
|
||||||
n += len(m.XXX_unrecognized)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func sovV2StoreCmd(x uint64) (n int) {
|
|
||||||
for {
|
|
||||||
n++
|
|
||||||
x >>= 7
|
|
||||||
if x == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
func sozV2StoreCmd(x uint64) (n int) {
|
|
||||||
return sovV2StoreCmd(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
|
||||||
}
|
|
||||||
func (m *Cmd) Marshal() (data []byte, err error) {
|
|
||||||
size := m.Size()
|
|
||||||
data = make([]byte, size)
|
|
||||||
n, err := m.MarshalTo(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Cmd) MarshalTo(data []byte) (n int, err error) {
|
|
||||||
var i int
|
|
||||||
_ = i
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
data[i] = 0x8
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(uint32(m.Type)))
|
|
||||||
data[i] = 0x12
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(len(m.Key)))
|
|
||||||
i += copy(data[i:], m.Key)
|
|
||||||
if m.Value != nil {
|
|
||||||
data[i] = 0x1a
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(len(*m.Value)))
|
|
||||||
i += copy(data[i:], *m.Value)
|
|
||||||
}
|
|
||||||
if m.PrevValue != nil {
|
|
||||||
data[i] = 0x22
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(len(*m.PrevValue)))
|
|
||||||
i += copy(data[i:], *m.PrevValue)
|
|
||||||
}
|
|
||||||
if m.PrevIndex != nil {
|
|
||||||
data[i] = 0x28
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(*m.PrevIndex))
|
|
||||||
}
|
|
||||||
if m.Dir != nil {
|
|
||||||
data[i] = 0x30
|
|
||||||
i++
|
|
||||||
if *m.Dir {
|
|
||||||
data[i] = 1
|
|
||||||
} else {
|
|
||||||
data[i] = 0
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if m.Recursive != nil {
|
|
||||||
data[i] = 0x38
|
|
||||||
i++
|
|
||||||
if *m.Recursive {
|
|
||||||
data[i] = 1
|
|
||||||
} else {
|
|
||||||
data[i] = 0
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if m.Unique != nil {
|
|
||||||
data[i] = 0x40
|
|
||||||
i++
|
|
||||||
if *m.Unique {
|
|
||||||
data[i] = 1
|
|
||||||
} else {
|
|
||||||
data[i] = 0
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if m.Sorted != nil {
|
|
||||||
data[i] = 0x48
|
|
||||||
i++
|
|
||||||
if *m.Sorted {
|
|
||||||
data[i] = 1
|
|
||||||
} else {
|
|
||||||
data[i] = 0
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if m.Time != nil {
|
|
||||||
data[i] = 0x52
|
|
||||||
i++
|
|
||||||
i = encodeVarintV2StoreCmd(data, i, uint64(len(m.Time)))
|
|
||||||
i += copy(data[i:], m.Time)
|
|
||||||
}
|
|
||||||
if m.XXX_unrecognized != nil {
|
|
||||||
i += copy(data[i:], m.XXX_unrecognized)
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
func encodeFixed64V2StoreCmd(data []byte, offset int, v uint64) int {
|
|
||||||
data[offset] = uint8(v)
|
|
||||||
data[offset+1] = uint8(v >> 8)
|
|
||||||
data[offset+2] = uint8(v >> 16)
|
|
||||||
data[offset+3] = uint8(v >> 24)
|
|
||||||
data[offset+4] = uint8(v >> 32)
|
|
||||||
data[offset+5] = uint8(v >> 40)
|
|
||||||
data[offset+6] = uint8(v >> 48)
|
|
||||||
data[offset+7] = uint8(v >> 56)
|
|
||||||
return offset + 8
|
|
||||||
}
|
|
||||||
func encodeFixed32V2StoreCmd(data []byte, offset int, v uint32) int {
|
|
||||||
data[offset] = uint8(v)
|
|
||||||
data[offset+1] = uint8(v >> 8)
|
|
||||||
data[offset+2] = uint8(v >> 16)
|
|
||||||
data[offset+3] = uint8(v >> 24)
|
|
||||||
return offset + 4
|
|
||||||
}
|
|
||||||
func encodeVarintV2StoreCmd(data []byte, offset int, v uint64) int {
|
|
||||||
for v >= 1<<7 {
|
|
||||||
data[offset] = uint8(v&0x7f | 0x80)
|
|
||||||
v >>= 7
|
|
||||||
offset++
|
|
||||||
}
|
|
||||||
data[offset] = uint8(v)
|
|
||||||
return offset + 1
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package etcdserver;
|
|
||||||
|
|
||||||
import "code.google.com/p/gogoprotobuf/gogoproto/gogo.proto";
|
|
||||||
|
|
||||||
option (gogoproto.marshaler_all) = true;
|
|
||||||
option (gogoproto.sizer_all) = true;
|
|
||||||
option (gogoproto.unmarshaler_all) = true;
|
|
||||||
option (gogoproto.goproto_getters_all) = false;
|
|
||||||
|
|
||||||
message cmd {
|
|
||||||
required int32 type = 1 [(gogoproto.nullable) = false];
|
|
||||||
required string key = 2 [(gogoproto.nullable) = false];
|
|
||||||
optional string value = 3;
|
|
||||||
optional string prevValue = 4;
|
|
||||||
optional uint64 prevIndex = 5;
|
|
||||||
optional bool dir = 6;
|
|
||||||
optional bool recursive = 7;
|
|
||||||
optional bool unique = 8;
|
|
||||||
optional bool sorted = 9;
|
|
||||||
optional bytes time = 10;
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// usage defines the message shown when a help flag is passed to etcd.
|
|
||||||
var usage = `
|
|
||||||
etcd
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
etcd -name <name>
|
|
||||||
etcd -name <name> [-data-dir=<path>]
|
|
||||||
etcd -h | -help
|
|
||||||
etcd -version
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h -help Show this screen.
|
|
||||||
--version Show version.
|
|
||||||
-f -force Force a new configuration to be used.
|
|
||||||
-config=<path> Path to configuration file.
|
|
||||||
-name=<name> Name of this node in the etcd cluster.
|
|
||||||
-data-dir=<path> Path to the data directory.
|
|
||||||
-cors=<origins> Comma-separated list of CORS origins.
|
|
||||||
-v Enabled verbose logging.
|
|
||||||
-vv Enabled very verbose logging.
|
|
||||||
|
|
||||||
Cluster Configuration Options:
|
|
||||||
-discovery=<url> Discovery service used to find a peer list.
|
|
||||||
-peers-file=<path> Path to a file containing the peer list.
|
|
||||||
-peers=<host:port>,<host:port> Comma-separated list of peers. The members
|
|
||||||
should match the peer's '-peer-addr' flag.
|
|
||||||
|
|
||||||
Client Communication Options:
|
|
||||||
-addr=<host:port> The public host:port used for client communication.
|
|
||||||
-bind-addr=<host[:port]> The listening host:port used for client communication.
|
|
||||||
-ca-file=<path> Path to the client CA file.
|
|
||||||
-cert-file=<path> Path to the client cert file.
|
|
||||||
-key-file=<path> Path to the client key file.
|
|
||||||
|
|
||||||
Peer Communication Options:
|
|
||||||
-peer-addr=<host:port> The public host:port used for peer communication.
|
|
||||||
-peer-bind-addr=<host[:port]> The listening host:port used for peer communication.
|
|
||||||
-peer-ca-file=<path> Path to the peer CA file.
|
|
||||||
-peer-cert-file=<path> Path to the peer cert file.
|
|
||||||
-peer-key-file=<path> Path to the peer key file.
|
|
||||||
-peer-heartbeat-interval=<time>
|
|
||||||
Time (in milliseconds) of a heartbeat interval.
|
|
||||||
-peer-election-timeout=<time>
|
|
||||||
Time (in milliseconds) for an election to timeout.
|
|
||||||
|
|
||||||
Other Options:
|
|
||||||
-max-result-buffer Max size of the result buffer.
|
|
||||||
-max-retry-attempts Number of times a node will try to join a cluster.
|
|
||||||
-retry-interval Seconds to wait between cluster join retry attempts.
|
|
||||||
-snapshot=false Disable log snapshots
|
|
||||||
-snapshot-count Number of transactions before issuing a snapshot.
|
|
||||||
-cluster-active-size Number of active nodes in the cluster.
|
|
||||||
-cluster-remove-delay Seconds before one node is removed.
|
|
||||||
-cluster-sync-interval Seconds between synchronizations for standby mode.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Usage returns the usage message for etcd.
|
|
||||||
func Usage() string {
|
|
||||||
return strings.TrimSpace(usage)
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
releaseVersion = "0.5rc1+git"
|
|
||||||
)
|
|
||||||
|
|
||||||
func versionHandler(w http.ResponseWriter, req *http.Request) {
|
|
||||||
fmt.Fprintf(w, "etcd %s", releaseVersion)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
// Copyright 2013 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.
|
|
||||||
|
|
||||||
package etcdserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func interestingGoroutines() (gs []string) {
|
|
||||||
buf := make([]byte, 2<<20)
|
|
||||||
buf = buf[:runtime.Stack(buf, true)]
|
|
||||||
for _, g := range strings.Split(string(buf), "\n\n") {
|
|
||||||
sl := strings.SplitN(g, "\n", 2)
|
|
||||||
if len(sl) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stack := strings.TrimSpace(sl[1])
|
|
||||||
if stack == "" ||
|
|
||||||
strings.Contains(stack, "created by testing.RunTests") ||
|
|
||||||
strings.Contains(stack, "testing.Main(") ||
|
|
||||||
strings.Contains(stack, "runtime.goexit") ||
|
|
||||||
strings.Contains(stack, "created by runtime.gc") ||
|
|
||||||
strings.Contains(stack, "runtime.MHeap_Scavenger") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
gs = append(gs, stack)
|
|
||||||
}
|
|
||||||
sort.Strings(gs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the other tests didn't leave any goroutines running.
|
|
||||||
// This is in a file named z_last_test.go so it sorts at the end.
|
|
||||||
func TestGoroutinesRunning(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("not counting goroutines for leakage in -short mode")
|
|
||||||
}
|
|
||||||
gs := interestingGoroutines()
|
|
||||||
|
|
||||||
n := 0
|
|
||||||
stackCount := make(map[string]int)
|
|
||||||
for _, g := range gs {
|
|
||||||
stackCount[g]++
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("num goroutines = %d", n)
|
|
||||||
if n > 0 {
|
|
||||||
t.Error("Too many goroutines.")
|
|
||||||
for stack, count := range stackCount {
|
|
||||||
t.Logf("%d instances of:\n%s", count, stack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func afterTest(t *testing.T) {
|
|
||||||
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
|
|
||||||
if testing.Short() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var bad string
|
|
||||||
badSubstring := map[string]string{
|
|
||||||
").readLoop(": "a Transport",
|
|
||||||
").writeLoop(": "a Transport",
|
|
||||||
"created by net/http/httptest.(*Server).Start": "an httptest.Server",
|
|
||||||
"timeoutHandler": "a TimeoutHandler",
|
|
||||||
"net.(*netFD).connect(": "a timing out dial",
|
|
||||||
").noteClientGone(": "a closenotifier sender",
|
|
||||||
}
|
|
||||||
var stacks string
|
|
||||||
for i := 0; i < 6; i++ {
|
|
||||||
bad = ""
|
|
||||||
stacks = strings.Join(interestingGoroutines(), "\n\n")
|
|
||||||
for substr, what := range badSubstring {
|
|
||||||
if strings.Contains(stacks, substr) {
|
|
||||||
bad = what
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bad == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Bad stuff found, but goroutines might just still be
|
|
||||||
// shutting down, so give it some time.
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
t.Errorf("Test appears to have leaked %s:\n%s", bad, stacks)
|
|
||||||
}
|
|
4
main.go
4
main.go
@ -7,8 +7,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
etcdserver "github.com/coreos/etcd/etcdserver2"
|
"github.com/coreos/etcd/etcdserver"
|
||||||
"github.com/coreos/etcd/etcdserver2/etcdhttp"
|
"github.com/coreos/etcd/etcdserver/etcdhttp"
|
||||||
"github.com/coreos/etcd/raft"
|
"github.com/coreos/etcd/raft"
|
||||||
"github.com/coreos/etcd/raft/raftpb"
|
"github.com/coreos/etcd/raft/raftpb"
|
||||||
"github.com/coreos/etcd/store"
|
"github.com/coreos/etcd/store"
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/server"
|
|
||||||
etcdtest "github.com/coreos/etcd/tests"
|
|
||||||
goetcd "github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
type garbageHandler struct {
|
|
||||||
t *testing.T
|
|
||||||
success bool
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *garbageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintln(w, "Hello, client")
|
|
||||||
if r.URL.String() != "/v2/keys/_etcd/registry/1/node1" {
|
|
||||||
g.t.Fatalf("Unexpected web request")
|
|
||||||
}
|
|
||||||
g.Lock()
|
|
||||||
defer g.Unlock()
|
|
||||||
|
|
||||||
g.success = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDiscoverySecondPeerFirstNoResponse ensures that if the first etcd
|
|
||||||
// machine stops after heartbeating that the second machine fails too.
|
|
||||||
func TestDiscoverySecondPeerFirstNoResponse(t *testing.T) {
|
|
||||||
etcdtest.RunServer(func(s *server.Server) {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("value", "started")
|
|
||||||
resp, err := etcdtest.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/_etcd/registry/2/_state"), v)
|
|
||||||
assert.Equal(t, resp.StatusCode, http.StatusCreated)
|
|
||||||
|
|
||||||
v = url.Values{}
|
|
||||||
v.Set("value", "http://127.0.0.1:49151")
|
|
||||||
resp, err = etcdtest.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/_etcd/registry/2/ETCDTEST"), v)
|
|
||||||
assert.Equal(t, resp.StatusCode, http.StatusCreated)
|
|
||||||
|
|
||||||
proc, err := startServer([]string{"-retry-interval", "0.2", "-discovery", s.URL() + "/v2/keys/_etcd/registry/2"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
defer stopServer(proc)
|
|
||||||
|
|
||||||
// TODO(bp): etcd will take 30 seconds to shutdown, figure this
|
|
||||||
// out instead
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
_, err = client.Get("/")
|
|
||||||
if err != nil && strings.Contains(err.Error(), "connection reset by peer") {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertServerNotUp(client http.Client, scheme string) error {
|
|
||||||
path := fmt.Sprintf("%s://127.0.0.1:4001/v2/keys/foo", scheme)
|
|
||||||
fields := url.Values(map[string][]string{"value": {"bar"}})
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err := client.PostForm(path, fields)
|
|
||||||
if err == nil {
|
|
||||||
return errors.New("Expected error during POST, got nil")
|
|
||||||
} else {
|
|
||||||
errString := err.Error()
|
|
||||||
if strings.Contains(errString, "connection refused") {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkEtcdDirectCall(b *testing.B) {
|
|
||||||
templateBenchmarkEtcdDirectCall(b, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkEtcdDirectCallTls(b *testing.B) {
|
|
||||||
templateBenchmarkEtcdDirectCall(b, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func templateBenchmarkEtcdDirectCall(b *testing.B, tls bool) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 3
|
|
||||||
_, etcds, _ := CreateCluster(clusterSize, procAttr, tls)
|
|
||||||
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
resp, _ := http.Get("http://127.0.0.1:4001/test/speed")
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,270 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestTLSOff asserts that non-TLS-encrypted communication between the
|
|
||||||
// etcd server and an unauthenticated client works
|
|
||||||
func TestTLSOff(t *testing.T) {
|
|
||||||
proc, err := startServer([]string{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
defer stopServer(proc)
|
|
||||||
|
|
||||||
client := buildClient()
|
|
||||||
err = assertServerFunctional(client, "http")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTLSAnonymousClient asserts that TLS-encrypted communication between the etcd
|
|
||||||
// server and an anonymous client works
|
|
||||||
func TestTLSAnonymousClient(t *testing.T) {
|
|
||||||
proc, err := startServer([]string{
|
|
||||||
"-cert-file=../../fixtures/ca/server.crt",
|
|
||||||
"-key-file=../../fixtures/ca/server.key.insecure",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
defer stopServer(proc)
|
|
||||||
|
|
||||||
cacertfile := "../../fixtures/ca/ca.crt"
|
|
||||||
|
|
||||||
cp := x509.NewCertPool()
|
|
||||||
bytes, err := ioutil.ReadFile(cacertfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cp.AppendCertsFromPEM(bytes)
|
|
||||||
|
|
||||||
cfg := tls.Config{}
|
|
||||||
cfg.RootCAs = cp
|
|
||||||
|
|
||||||
client := buildTLSClient(&cfg)
|
|
||||||
err = assertServerFunctional(client, "https")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTLSAuthenticatedClient asserts that TLS-encrypted communication
|
|
||||||
// between the etcd server and an authenticated client works
|
|
||||||
func TestTLSAuthenticatedClient(t *testing.T) {
|
|
||||||
proc, err := startServer([]string{
|
|
||||||
"-cert-file=../../fixtures/ca/server.crt",
|
|
||||||
"-key-file=../../fixtures/ca/server.key.insecure",
|
|
||||||
"-ca-file=../../fixtures/ca/ca.crt",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
defer stopServer(proc)
|
|
||||||
|
|
||||||
cacertfile := "../../fixtures/ca/ca.crt"
|
|
||||||
certfile := "../../fixtures/ca/server2.crt"
|
|
||||||
keyfile := "../../fixtures/ca/server2.key.insecure"
|
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(certfile, keyfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cp := x509.NewCertPool()
|
|
||||||
bytes, err := ioutil.ReadFile(cacertfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cp.AppendCertsFromPEM(bytes)
|
|
||||||
|
|
||||||
cfg := tls.Config{}
|
|
||||||
cfg.Certificates = []tls.Certificate{cert}
|
|
||||||
cfg.RootCAs = cp
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
client := buildTLSClient(&cfg)
|
|
||||||
err = assertServerFunctional(client, "https")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTLSUnathenticatedClient asserts that TLS-encrypted communication
|
|
||||||
// between the etcd server and an unauthenticated client fails
|
|
||||||
func TestTLSUnauthenticatedClient(t *testing.T) {
|
|
||||||
proc, err := startServer([]string{
|
|
||||||
"-cert-file=../../fixtures/ca/server.crt",
|
|
||||||
"-key-file=../../fixtures/ca/server.key.insecure",
|
|
||||||
"-ca-file=../../fixtures/ca/ca.crt",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
defer stopServer(proc)
|
|
||||||
|
|
||||||
cacertfile := "../../fixtures/ca/ca.crt"
|
|
||||||
certfile := "../../fixtures/ca/broken_server.crt"
|
|
||||||
keyfile := "../../fixtures/ca/broken_server.key.insecure"
|
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(certfile, keyfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cp := x509.NewCertPool()
|
|
||||||
bytes, err := ioutil.ReadFile(cacertfile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cp.AppendCertsFromPEM(bytes)
|
|
||||||
|
|
||||||
cfg := tls.Config{}
|
|
||||||
cfg.Certificates = []tls.Certificate{cert}
|
|
||||||
cfg.RootCAs = cp
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
client := buildTLSClient(&cfg)
|
|
||||||
err = assertServerNotFunctional(client, "https")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildClient() http.Client {
|
|
||||||
return http.Client{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildTLSClient(tlsConf *tls.Config) http.Client {
|
|
||||||
tr := http.Transport{TLSClientConfig: tlsConf}
|
|
||||||
return http.Client{Transport: &tr}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServer(extra []string) (*os.Process, error) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
cmd := []string{"etcd", "-f", "-data-dir=/tmp/node1", "-name=node1"}
|
|
||||||
cmd = append(cmd, extra...)
|
|
||||||
|
|
||||||
println(strings.Join(cmd, " "))
|
|
||||||
|
|
||||||
return os.StartProcess(EtcdBinPath, cmd, procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(yichengq): refactor these helper functions in #645
|
|
||||||
func startServer2(extra []string) (*os.Process, error) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
cmd := []string{"etcd", "-f", "-data-dir=/tmp/node2", "-name=node2"}
|
|
||||||
cmd = append(cmd, extra...)
|
|
||||||
|
|
||||||
fmt.Println(strings.Join(cmd, " "))
|
|
||||||
|
|
||||||
return os.StartProcess(EtcdBinPath, cmd, procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServerWithDataDir(extra []string) (*os.Process, error) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
cmd := []string{"etcd", "-data-dir=/tmp/node1", "-name=node1"}
|
|
||||||
cmd = append(cmd, extra...)
|
|
||||||
|
|
||||||
fmt.Println(strings.Join(cmd, " "))
|
|
||||||
|
|
||||||
return os.StartProcess(EtcdBinPath, cmd, procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServer2WithDataDir(extra []string) (*os.Process, error) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
cmd := []string{"etcd", "-data-dir=/tmp/node2", "-name=node2"}
|
|
||||||
cmd = append(cmd, extra...)
|
|
||||||
|
|
||||||
println(strings.Join(cmd, " "))
|
|
||||||
|
|
||||||
return os.StartProcess(EtcdBinPath, cmd, procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopServer(proc *os.Process) {
|
|
||||||
err := proc.Kill()
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
proc.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertServerFunctional(client http.Client, scheme string) error {
|
|
||||||
path := fmt.Sprintf("%s://127.0.0.1:4001/v2/keys/foo", scheme)
|
|
||||||
fields := url.Values(map[string][]string{"value": {"bar"}})
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
resp, err := client.PostForm(path, fields)
|
|
||||||
// If the status is Temporary Redirect, we should follow the
|
|
||||||
// new location, because the request did not go to the leader yet.
|
|
||||||
// TODO(yichengq): the difference between Temporary Redirect(307)
|
|
||||||
// and Created(201) could distinguish between leader and followers
|
|
||||||
for err == nil && resp.StatusCode == http.StatusTemporaryRedirect {
|
|
||||||
loc, _ := resp.Location()
|
|
||||||
newPath := loc.String()
|
|
||||||
resp, err = client.PostForm(newPath, fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
// Internal error may mean that servers are in leader election
|
|
||||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusInternalServerError {
|
|
||||||
return errors.New(fmt.Sprintf("resp.StatusCode == %s", resp.Status))
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("etcd server was not reachable in time / had internal error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertServerNotFunctional(client http.Client, scheme string) error {
|
|
||||||
path := fmt.Sprintf("%s://127.0.0.1:4001/v2/keys/foo", scheme)
|
|
||||||
fields := url.Values(map[string][]string{"value": {"bar"}})
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err := client.PostForm(path, fields)
|
|
||||||
if err == nil {
|
|
||||||
return errors.New("Expected error during POST, got nil")
|
|
||||||
} else {
|
|
||||||
errString := err.Error()
|
|
||||||
if strings.Contains(errString, "connection refused") {
|
|
||||||
continue
|
|
||||||
} else if strings.Contains(errString, "bad certificate") {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("Expected server to fail with 'bad certificate'")
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go/build"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var EtcdBinPath string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Initialize the 'etcd' binary path or default it to the etcd diretory.
|
|
||||||
EtcdBinPath = os.Getenv("ETCD_BIN_PATH")
|
|
||||||
if EtcdBinPath == "" {
|
|
||||||
EtcdBinPath = filepath.Join(build.Default.GOPATH, "src", "github.com", "coreos", "etcd", "etcd")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure that etcd does not come up if the internal raft versions do not match.
|
|
||||||
func TestInternalVersion(t *testing.T) {
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
checkedVersion := false
|
|
||||||
testMux := http.NewServeMux()
|
|
||||||
|
|
||||||
testMux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintln(w, "This is not a version number")
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
checkedVersion = true
|
|
||||||
})
|
|
||||||
|
|
||||||
testMux.HandleFunc("/join", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t.Fatal("should not attempt to join!")
|
|
||||||
})
|
|
||||||
|
|
||||||
ts := httptest.NewServer(testMux)
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
fakeURL, _ := url.Parse(ts.URL)
|
|
||||||
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
args := []string{"etcd", "-name=node1", "-f", "-data-dir=/tmp/node1", "-peers=" + fakeURL.Host}
|
|
||||||
|
|
||||||
process, err := os.StartProcess(EtcdBinPath, args, procAttr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("start process failed:" + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
process.Kill()
|
|
||||||
|
|
||||||
_, err = http.Get("http://127.0.0.1:4001")
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("etcd node should not be up")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
if checkedVersion == false {
|
|
||||||
t.Fatal("etcd did not check the version")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/server"
|
|
||||||
"github.com/coreos/etcd/tests"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test will kill the current leader and wait for the etcd cluster to elect a new leader for 200 times.
|
|
||||||
// It will print out the election time and the average election time.
|
|
||||||
// It runs in a cluster with standby nodes.
|
|
||||||
func TestKillLeaderWithStandbys(t *testing.T) {
|
|
||||||
// https://github.com/goraft/raft/issues/222
|
|
||||||
t.Skip("stuck on raft issue")
|
|
||||||
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
leaderChan := make(chan string, 1)
|
|
||||||
all := make(chan bool, 1)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
go Monitor(clusterSize, 1, leaderChan, all, stop)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
// Reconfigure with a small active size.
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":3, "removeDelay":2, "syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for two monitor cycles before checking for demotion.
|
|
||||||
time.Sleep((2 * server.ActiveMonitorTimeout) + (2 * time.Second))
|
|
||||||
|
|
||||||
// Verify that we have 3 peers.
|
|
||||||
result, err := c.Get("_etcd/machines", true, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 3)
|
|
||||||
|
|
||||||
var totalTime time.Duration
|
|
||||||
|
|
||||||
leader := "http://127.0.0.1:7001"
|
|
||||||
|
|
||||||
for i := 0; i < clusterSize; i++ {
|
|
||||||
t.Log("leader is ", leader)
|
|
||||||
port, _ := strconv.Atoi(strings.Split(leader, ":")[2])
|
|
||||||
num := port - 7001
|
|
||||||
t.Log("kill server ", num)
|
|
||||||
etcds[num].Kill()
|
|
||||||
etcds[num].Release()
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
for {
|
|
||||||
newLeader := <-leaderChan
|
|
||||||
if newLeader != leader {
|
|
||||||
leader = newLeader
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
take := time.Now().Sub(start)
|
|
||||||
|
|
||||||
totalTime += take
|
|
||||||
avgTime := totalTime / (time.Duration)(i+1)
|
|
||||||
fmt.Println("Total time:", totalTime, "; Avg time:", avgTime)
|
|
||||||
|
|
||||||
time.Sleep(server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// Verify that we have 3 peers.
|
|
||||||
result, err = c.Get("_etcd/machines", true, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 3)
|
|
||||||
|
|
||||||
// Verify that killed node is not one of those peers.
|
|
||||||
_, err = c.Get(fmt.Sprintf("_etcd/machines/node%d", num+1), false, false)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
etcds[num], err = os.StartProcess(EtcdBinPath, argGroup[num], procAttr)
|
|
||||||
}
|
|
||||||
stop <- true
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/server"
|
|
||||||
"github.com/coreos/etcd/tests"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestTLSMultiNodeKillAllAndRecovery create a five nodes
|
|
||||||
// then kill all the nodes and restart
|
|
||||||
func TestTLSMultiNodeKillAllAndRecovery(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
leaderChan := make(chan string, 1)
|
|
||||||
all := make(chan bool, 1)
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, true)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
go Monitor(clusterSize, clusterSize, leaderChan, all, stop)
|
|
||||||
<-all
|
|
||||||
<-leaderChan
|
|
||||||
stop <- true
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
// send 10 commands
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
// Test Set
|
|
||||||
_, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
// kill all
|
|
||||||
DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
stop = make(chan bool)
|
|
||||||
leaderChan = make(chan string, 1)
|
|
||||||
all = make(chan bool, 1)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < clusterSize; i++ {
|
|
||||||
etcds[i], err = os.StartProcess(EtcdBinPath, argGroup[i], procAttr)
|
|
||||||
// See util.go for the reason to wait for server
|
|
||||||
client := buildClient()
|
|
||||||
err = WaitForServer("127.0.0.1:400"+strconv.Itoa(i+1), client, "http")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("node start error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go Monitor(clusterSize, 1, leaderChan, all, stop)
|
|
||||||
|
|
||||||
<-all
|
|
||||||
<-leaderChan
|
|
||||||
|
|
||||||
result, err := c.Set("foo", "bar", 0)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Recovery error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Node.ModifiedIndex != 17 {
|
|
||||||
t.Fatalf("recovery failed! [%d/17]", result.Node.ModifiedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a five-node cluster
|
|
||||||
// Kill all the nodes and restart
|
|
||||||
func TestMultiNodeKillAllAndRecoveryWithStandbys(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
leaderChan := make(chan string, 1)
|
|
||||||
all := make(chan bool, 1)
|
|
||||||
|
|
||||||
clusterSize := 15
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
go Monitor(clusterSize, clusterSize, leaderChan, all, stop)
|
|
||||||
<-all
|
|
||||||
<-leaderChan
|
|
||||||
stop <- true
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
// Reconfigure with smaller active size (7 nodes) and wait for remove.
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":7}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(2*server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
|
|
||||||
// Verify that there is three machines in peer mode.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 7)
|
|
||||||
|
|
||||||
// send set commands
|
|
||||||
for i := 0; i < 2*clusterSize; i++ {
|
|
||||||
// Test Set
|
|
||||||
_, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
// kill all
|
|
||||||
DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
stop = make(chan bool)
|
|
||||||
leaderChan = make(chan string, 1)
|
|
||||||
all = make(chan bool, 1)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < clusterSize; i++ {
|
|
||||||
etcds[i], err = os.StartProcess(EtcdBinPath, append(argGroup[i], "-peers="), procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// send set commands
|
|
||||||
for i := 0; i < 2*clusterSize; i++ {
|
|
||||||
// Test Set
|
|
||||||
_, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Recovery error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that we have seven machines.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 7)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a five nodes
|
|
||||||
// Kill all the nodes and restart, then remove the leader
|
|
||||||
func TestMultiNodeKillAllAndRecoveryAndRemoveLeader(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
leaderChan := make(chan string, 1)
|
|
||||||
all := make(chan bool, 1)
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
go Monitor(clusterSize, clusterSize, leaderChan, all, stop)
|
|
||||||
<-all
|
|
||||||
<-leaderChan
|
|
||||||
stop <- true
|
|
||||||
|
|
||||||
// It needs some time to sync current commits and write it to disk.
|
|
||||||
// Or some instance may be restarted as a new peer, and we don't support
|
|
||||||
// to connect back the old cluster that doesn't have majority alive
|
|
||||||
// without log now.
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
// kill all
|
|
||||||
DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
stop = make(chan bool)
|
|
||||||
leaderChan = make(chan string, 1)
|
|
||||||
all = make(chan bool, 1)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < clusterSize; i++ {
|
|
||||||
etcds[i], err = os.StartProcess(EtcdBinPath, argGroup[i], procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
go Monitor(clusterSize, 1, leaderChan, all, stop)
|
|
||||||
|
|
||||||
<-all
|
|
||||||
leader := <-leaderChan
|
|
||||||
|
|
||||||
_, err = c.Set("foo", "bar", 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Recovery error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
port, _ := strconv.Atoi(strings.Split(leader, ":")[2])
|
|
||||||
num := port - 7000
|
|
||||||
resp, _ := tests.Delete(leader+"/v2/admin/machines/node"+strconv.Itoa(num), "application/json", nil)
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the old leader is in standby mode now
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
resp, _ = tests.Get(leader + "/name")
|
|
||||||
assert.Equal(t, resp.StatusCode, 404)
|
|
||||||
}
|
|
@ -1,195 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func increasePeerAddressPort(args []string, delta int) []string {
|
|
||||||
for i, arg := range args {
|
|
||||||
if !strings.Contains(arg, "peer-addr") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
splitArg := strings.Split(arg, ":")
|
|
||||||
port, _ := strconv.Atoi(splitArg[len(splitArg)-1])
|
|
||||||
args[i] = "-peer-addr=127.0.0.1:" + strconv.Itoa(port+delta)
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
return append(args, "-peer-addr=127.0.0.1:"+strconv.Itoa(7001+delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
func increaseAddressPort(args []string, delta int) []string {
|
|
||||||
for i, arg := range args {
|
|
||||||
if !strings.HasPrefix(arg, "-addr") && !strings.HasPrefix(arg, "--addr") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
splitArg := strings.Split(arg, ":")
|
|
||||||
port, _ := strconv.Atoi(splitArg[len(splitArg)-1])
|
|
||||||
args[i] = "-addr=127.0.0.1:" + strconv.Itoa(port+delta)
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
return append(args, "-addr=127.0.0.1:"+strconv.Itoa(4001+delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
func increaseDataDir(args []string, delta int) []string {
|
|
||||||
for i, arg := range args {
|
|
||||||
if !strings.Contains(arg, "-data-dir") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
splitArg := strings.Split(arg, "node")
|
|
||||||
idx, _ := strconv.Atoi(splitArg[len(splitArg)-1])
|
|
||||||
args[i] = "-data-dir=/tmp/node" + strconv.Itoa(idx+delta)
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a five-node cluster
|
|
||||||
// Random kill one of the nodes and restart it with different peer address
|
|
||||||
func TestRejoinWithDifferentPeerAddress(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
num := rand.Int() % clusterSize
|
|
||||||
fmt.Println("kill node", num+1)
|
|
||||||
|
|
||||||
etcds[num].Kill()
|
|
||||||
etcds[num].Release()
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
argGroup[num] = increasePeerAddressPort(argGroup[num], clusterSize)
|
|
||||||
// restart
|
|
||||||
etcds[num], err = os.StartProcess(EtcdBinPath, argGroup[num], procAttr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
result, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil || result.Node.Key != "/foo" || result.Node.Value != "bar" {
|
|
||||||
t.Fatal("Failed to set value in etcd cluster")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a five-node cluster
|
|
||||||
// Replace one of the nodes with different peer address
|
|
||||||
func TestReplaceWithDifferentPeerAddress(t *testing.T) {
|
|
||||||
// TODO(yichengq): find some way to avoid the error that will be
|
|
||||||
// caused if some node joins the cluster with the collided name.
|
|
||||||
// Possible solutions:
|
|
||||||
// 1. Remove itself when executing a join command with the same name
|
|
||||||
// and different peer address. However, it should find some way to
|
|
||||||
// trigger that execution because the leader may update its address
|
|
||||||
// and stop heartbeat.
|
|
||||||
// 2. Remove the node with the same name before join each time.
|
|
||||||
// But this way could be rather overkill.
|
|
||||||
t.Skip("Unimplemented functionality")
|
|
||||||
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
rand.Int()
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
num := rand.Int() % clusterSize
|
|
||||||
fmt.Println("replace node", num+1)
|
|
||||||
|
|
||||||
argGroup[num] = increasePeerAddressPort(argGroup[num], clusterSize)
|
|
||||||
argGroup[num] = increaseAddressPort(argGroup[num], clusterSize)
|
|
||||||
argGroup[num] = increaseDataDir(argGroup[num], clusterSize)
|
|
||||||
// restart
|
|
||||||
newEtcd, err := os.StartProcess(EtcdBinPath, append(argGroup[num], "-f"), procAttr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
etcds[num].Wait()
|
|
||||||
etcds[num] = newEtcd
|
|
||||||
}
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
result, err := c.Set("foo", "bar", 0)
|
|
||||||
if err != nil || result.Node.Key != "/foo" || result.Node.Value != "bar" {
|
|
||||||
t.Fatal("Failed to set value in etcd cluster")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a five-node cluster
|
|
||||||
// Let the sixth instance join with different name and existing peer address
|
|
||||||
func TestRejoinWithDifferentName(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 5
|
|
||||||
argGroup, etcds, err := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
num := rand.Int() % clusterSize
|
|
||||||
fmt.Println("join node 6 that collides with node", num+1)
|
|
||||||
|
|
||||||
// kill
|
|
||||||
etcds[num].Kill()
|
|
||||||
etcds[num].Release()
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
// restart
|
|
||||||
if i == 0 {
|
|
||||||
etcds[num], err = os.StartProcess(EtcdBinPath, append(argGroup[num], "-name=node6", "-peers=127.0.0.1:7002"), procAttr)
|
|
||||||
} else {
|
|
||||||
etcds[num], err = os.StartProcess(EtcdBinPath, append(argGroup[num], "-f", "-name=node6", "-peers=127.0.0.1:7002"), procAttr)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("failed to start process:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
timer := time.AfterFunc(10*time.Second, func() {
|
|
||||||
t.Fatal("new etcd should fail immediately")
|
|
||||||
})
|
|
||||||
etcds[num].Wait()
|
|
||||||
etcds[num] = nil
|
|
||||||
timer.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/tests"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// remove the node and node rejoin with previous log
|
|
||||||
func TestRemoveNode(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 4
|
|
||||||
argGroup, etcds, _ := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":4, "syncInterval":5}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
rmReq, _ := http.NewRequest("DELETE", "http://127.0.0.1:7001/remove/node3", nil)
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
client.Do(rmReq)
|
|
||||||
|
|
||||||
fmt.Println("send remove to node3 and wait for its exiting")
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
resp, err := c.Get("_etcd/machines", false, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Node.Nodes) != 3 {
|
|
||||||
t.Fatal("cannot remove peer")
|
|
||||||
}
|
|
||||||
|
|
||||||
etcds[2].Kill()
|
|
||||||
etcds[2].Wait()
|
|
||||||
|
|
||||||
if i == 1 {
|
|
||||||
// rejoin with log
|
|
||||||
etcds[2], err = os.StartProcess(EtcdBinPath, argGroup[2], procAttr)
|
|
||||||
} else {
|
|
||||||
// rejoin without log
|
|
||||||
etcds[2], err = os.StartProcess(EtcdBinPath, append(argGroup[2], "-f"), procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second + 5*time.Second)
|
|
||||||
|
|
||||||
resp, err = c.Get("_etcd/machines", false, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Node.Nodes) != 4 {
|
|
||||||
t.Fatalf("add peer fails #1 (%d != 4)", len(resp.Node.Nodes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// first kill the node, then remove it, then add it back
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
etcds[2].Kill()
|
|
||||||
fmt.Println("kill node3 and wait for its exiting")
|
|
||||||
etcds[2].Wait()
|
|
||||||
|
|
||||||
client.Do(rmReq)
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
resp, err := c.Get("_etcd/machines", false, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Node.Nodes) != 3 {
|
|
||||||
t.Fatal("cannot remove peer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == 1 {
|
|
||||||
// rejoin with log
|
|
||||||
etcds[2], err = os.StartProcess(EtcdBinPath, append(argGroup[2]), procAttr)
|
|
||||||
} else {
|
|
||||||
// rejoin without log
|
|
||||||
etcds[2], err = os.StartProcess(EtcdBinPath, append(argGroup[2], "-f"), procAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second + time.Second)
|
|
||||||
|
|
||||||
resp, err = c.Get("_etcd/machines", false, false)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Node.Nodes) != 4 {
|
|
||||||
t.Fatalf("add peer fails #2 (%d != 4)", len(resp.Node.Nodes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemovePausedNode(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 4
|
|
||||||
_, etcds, _ := CreateCluster(clusterSize, procAttr, false)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
r, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":3, "removeDelay":1, "syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, r.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
// Wait for standby instances to update its cluster config
|
|
||||||
time.Sleep(6 * time.Second)
|
|
||||||
|
|
||||||
resp, err := c.Get("_etcd/machines", false, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(resp.Node.Nodes) != 3 {
|
|
||||||
t.Fatal("cannot remove peer")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < clusterSize; i++ {
|
|
||||||
// first pause the node, then remove it, then resume it
|
|
||||||
idx := rand.Int() % clusterSize
|
|
||||||
|
|
||||||
etcds[idx].Signal(syscall.SIGSTOP)
|
|
||||||
fmt.Printf("pause node%d and let standby node take its place\n", idx+1)
|
|
||||||
|
|
||||||
time.Sleep(4 * time.Second)
|
|
||||||
|
|
||||||
etcds[idx].Signal(syscall.SIGCONT)
|
|
||||||
// let it change its state to candidate at least
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
stop := make(chan bool)
|
|
||||||
leaderChan := make(chan string, 1)
|
|
||||||
all := make(chan bool, 1)
|
|
||||||
|
|
||||||
go Monitor(clusterSize, clusterSize, leaderChan, all, stop)
|
|
||||||
<-all
|
|
||||||
<-leaderChan
|
|
||||||
stop <- true
|
|
||||||
|
|
||||||
resp, err = c.Get("_etcd/machines", false, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(resp.Node.Nodes) != 3 {
|
|
||||||
t.Fatalf("add peer fails (%d != 3)", len(resp.Node.Nodes))
|
|
||||||
}
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
if resp.Node.Nodes[i].Key == fmt.Sprintf("node%d", idx+1) {
|
|
||||||
t.Fatal("node should be removed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSimpleMultiNode(t *testing.T) {
|
|
||||||
templateTestSimpleMultiNode(t, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSimpleMultiNodeTls(t *testing.T) {
|
|
||||||
templateTestSimpleMultiNode(t, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a three nodes and try to set value
|
|
||||||
func templateTestSimpleMultiNode(t *testing.T, tls bool) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
|
|
||||||
clusterSize := 3
|
|
||||||
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, procAttr, tls)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot create cluster: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
if c.SyncCluster() == false {
|
|
||||||
t.Fatal("Cannot sync cluster!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Set
|
|
||||||
result, err := c.Set("foo", "bar", 100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
node := result.Node
|
|
||||||
if node.Key != "/foo" || node.Value != "bar" || node.TTL < 95 {
|
|
||||||
t.Fatalf("Set 1 failed with %s %s %v", node.Key, node.Value, node.TTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
result, err = c.Set("foo", "bar", 100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
node = result.Node
|
|
||||||
if node.Key != "/foo" || node.Value != "bar" || node.TTL < 95 {
|
|
||||||
t.Fatalf("Set 2 failed with %s %s %v", node.Key, node.Value, node.TTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test creates a single node and then set a value to it to trigger snapshot
|
|
||||||
func TestSnapshot(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
args := []string{"etcd", "-name=node1", "-data-dir=/tmp/node1", "-snapshot=true", "-snapshot-count=500"}
|
|
||||||
|
|
||||||
process, err := os.StartProcess(EtcdBinPath, append(args, "-f"), procAttr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("start process failed:" + err.Error())
|
|
||||||
}
|
|
||||||
defer process.Kill()
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
// issue first 501 commands
|
|
||||||
for i := 0; i < 501; i++ {
|
|
||||||
result, err := c.Set("foo", "bar", 100)
|
|
||||||
node := result.Node
|
|
||||||
|
|
||||||
if err != nil || node.Key != "/foo" || node.Value != "bar" || node.TTL < 95 {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("Set failed with %s %s %v", node.Key, node.Value, node.TTL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for a snapshot interval
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
|
|
||||||
snapshots, err := ioutil.ReadDir("/tmp/node1/snapshot")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("list snapshot failed:" + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snapshots) != 1 {
|
|
||||||
t.Fatal("wrong number of snapshot :[1/", len(snapshots), "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
index, _ := strconv.Atoi(snapshots[0].Name()[2:5])
|
|
||||||
|
|
||||||
if index < 503 || index > 516 {
|
|
||||||
t.Fatal("wrong name of snapshot :", snapshots[0].Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// issue second 501 commands
|
|
||||||
for i := 0; i < 501; i++ {
|
|
||||||
result, err := c.Set("foo", "bar", 100)
|
|
||||||
node := result.Node
|
|
||||||
|
|
||||||
if err != nil || node.Key != "/foo" || node.Value != "bar" || node.TTL < 95 {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("Set failed with %s %s %v", node.Key, node.Value, node.TTL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for a snapshot interval
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
|
|
||||||
snapshots, err = ioutil.ReadDir("/tmp/node1/snapshot")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("list snapshot failed:" + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snapshots) != 1 {
|
|
||||||
t.Fatal("wrong number of snapshot :[1/", len(snapshots), "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
index, _ = strconv.Atoi(snapshots[0].Name()[2:6])
|
|
||||||
|
|
||||||
if index < 1010 || index > 1029 {
|
|
||||||
t.Fatal("wrong name of snapshot :", snapshots[0].Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSnapshotRestart tests etcd restarts with snapshot file
|
|
||||||
func TestSnapshotRestart(t *testing.T) {
|
|
||||||
procAttr := new(os.ProcAttr)
|
|
||||||
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
|
|
||||||
args := []string{"etcd", "-name=node1", "-data-dir=/tmp/node1", "-snapshot=true", "-snapshot-count=500"}
|
|
||||||
|
|
||||||
process, err := os.StartProcess(EtcdBinPath, append(args, "-f"), procAttr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("start process failed:" + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
|
|
||||||
c.SyncCluster()
|
|
||||||
// issue first 501 commands
|
|
||||||
for i := 0; i < 501; i++ {
|
|
||||||
result, err := c.Set("foo", "bar", 100)
|
|
||||||
node := result.Node
|
|
||||||
|
|
||||||
if err != nil || node.Key != "/foo" || node.Value != "bar" || node.TTL < 95 {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("Set failed with %s %s %v", node.Key, node.Value, node.TTL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for a snapshot interval
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
|
|
||||||
_, err = ioutil.ReadDir("/tmp/node1/snapshot")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("list snapshot failed:" + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
process.Kill()
|
|
||||||
|
|
||||||
process, err = os.StartProcess(EtcdBinPath, args, procAttr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("start process failed:" + err.Error())
|
|
||||||
}
|
|
||||||
defer process.Kill()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err = c.Set("foo", "bar", 100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,342 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/server"
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
"github.com/coreos/etcd/tests"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create a full cluster and then change the active size.
|
|
||||||
func TestStandby(t *testing.T) {
|
|
||||||
clusterSize := 15
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
// Verify that we just have default machines.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 9)
|
|
||||||
|
|
||||||
t.Log("Reconfigure with a smaller active size")
|
|
||||||
resp, _ = tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":7, "syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for two monitor cycles before checking for demotion.
|
|
||||||
time.Sleep((2 * server.ActiveMonitorTimeout) + (2 * time.Second))
|
|
||||||
|
|
||||||
// Verify that we now have seven peers.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 7)
|
|
||||||
|
|
||||||
t.Log("Test the functionality of all servers")
|
|
||||||
// Set key.
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
if _, err := c.Set("foo", "bar", 0); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
// Check that all peers and standbys have the value.
|
|
||||||
for i := range etcds {
|
|
||||||
resp, err := tests.Get(fmt.Sprintf("http://localhost:%d/v2/keys/foo", 4000+(i+1)))
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
body := tests.ReadBodyJSON(resp)
|
|
||||||
if node, _ := body["node"].(map[string]interface{}); assert.NotNil(t, node) {
|
|
||||||
assert.Equal(t, node["value"], "bar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Reconfigure with larger active size and wait for join")
|
|
||||||
resp, _ = tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":8, "syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep((1 * time.Second) + (1 * time.Second))
|
|
||||||
|
|
||||||
// Verify that exactly eight machines are in the cluster.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a full cluster, disconnect a peer, wait for removal, wait for standby join.
|
|
||||||
func TestStandbyAutoJoin(t *testing.T) {
|
|
||||||
clusterSize := 5
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
// Wrap this in a closure so that it picks up the updated version of
|
|
||||||
// the "etcds" variable.
|
|
||||||
DestroyCluster(etcds)
|
|
||||||
}()
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
// Verify that we have five machines.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 5)
|
|
||||||
|
|
||||||
// Reconfigure with a short remove delay (2 second).
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":4, "removeDelay":2, "syncInterval":1}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a monitor cycle before checking for removal.
|
|
||||||
time.Sleep(server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
|
|
||||||
// Verify that we now have four peers.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 4)
|
|
||||||
|
|
||||||
// Remove peer.
|
|
||||||
etcd := etcds[1]
|
|
||||||
etcds = append(etcds[:1], etcds[2:]...)
|
|
||||||
if err := etcd.Kill(); err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
etcd.Release()
|
|
||||||
|
|
||||||
// Wait for it to get dropped.
|
|
||||||
time.Sleep(server.PeerActivityMonitorTimeout + (1 * time.Second))
|
|
||||||
|
|
||||||
// Wait for the standby to join.
|
|
||||||
time.Sleep((1 * time.Second) + (1 * time.Second))
|
|
||||||
|
|
||||||
// Verify that we have 4 peers.
|
|
||||||
result, err = c.Get("_etcd/machines", true, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 4)
|
|
||||||
|
|
||||||
// Verify that node2 is not one of those peers.
|
|
||||||
_, err = c.Get("_etcd/machines/node2", false, false)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a full cluster and then change the active size gradually.
|
|
||||||
func TestStandbyGradualChange(t *testing.T) {
|
|
||||||
clusterSize := 9
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
num := clusterSize
|
|
||||||
for inc := 0; inc < 2; inc++ {
|
|
||||||
for i := 0; i < 6; i++ {
|
|
||||||
// Verify that we just have i machines.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), num)
|
|
||||||
|
|
||||||
if inc == 0 {
|
|
||||||
num--
|
|
||||||
} else {
|
|
||||||
num++
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Reconfigure with active size", num)
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(fmt.Sprintf(`{"activeSize":%d, "syncInterval":1}`, num)))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if inc == 0 {
|
|
||||||
// Wait for monitor cycles before checking for demotion.
|
|
||||||
time.Sleep(server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
} else {
|
|
||||||
time.Sleep(time.Second + (1 * time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that we now have peers.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), num)
|
|
||||||
|
|
||||||
t.Log("Test the functionality of all servers")
|
|
||||||
// Set key.
|
|
||||||
if _, err := c.Set("foo", "bar", 0); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Check that all peers and standbys have the value.
|
|
||||||
for i := range etcds {
|
|
||||||
resp, err := tests.Get(fmt.Sprintf("http://localhost:%d/v2/keys/foo", 4000+(i+1)))
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
body := tests.ReadBodyJSON(resp)
|
|
||||||
if node, _ := body["node"].(map[string]interface{}); assert.NotNil(t, node) {
|
|
||||||
assert.Equal(t, node["value"], "bar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a full cluster and then change the active size dramatically.
|
|
||||||
func TestStandbyDramaticChange(t *testing.T) {
|
|
||||||
clusterSize := 9
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
num := clusterSize
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
for inc := 0; inc < 2; inc++ {
|
|
||||||
// Verify that we just have i machines.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), num)
|
|
||||||
|
|
||||||
if inc == 0 {
|
|
||||||
num -= 6
|
|
||||||
} else {
|
|
||||||
num += 6
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Reconfigure with active size", num)
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(fmt.Sprintf(`{"activeSize":%d, "syncInterval":1}`, num)))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if inc == 0 {
|
|
||||||
// Wait for monitor cycles before checking for demotion.
|
|
||||||
time.Sleep(6*server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
} else {
|
|
||||||
time.Sleep(time.Second + (1 * time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that we now have peers.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), num)
|
|
||||||
|
|
||||||
t.Log("Test the functionality of all servers")
|
|
||||||
// Set key.
|
|
||||||
if _, err := c.Set("foo", "bar", 0); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Check that all peers and standbys have the value.
|
|
||||||
for i := range etcds {
|
|
||||||
resp, err := tests.Get(fmt.Sprintf("http://localhost:%d/v2/keys/foo", 4000+(i+1)))
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
body := tests.ReadBodyJSON(resp)
|
|
||||||
if node, _ := body["node"].(map[string]interface{}); assert.NotNil(t, node) {
|
|
||||||
assert.Equal(t, node["value"], "bar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStandbyJoinMiss(t *testing.T) {
|
|
||||||
clusterSize := 2
|
|
||||||
_, etcds, err := CreateCluster(clusterSize, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("cannot create cluster")
|
|
||||||
}
|
|
||||||
defer DestroyCluster(etcds)
|
|
||||||
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
c.SyncCluster()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
// Verify that we have two machines.
|
|
||||||
result, err := c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), clusterSize)
|
|
||||||
|
|
||||||
resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"removeDelay":4, "syncInterval":4}`))
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
resp, _ = tests.Delete("http://localhost:7001/v2/admin/machines/node2", "application/json", nil)
|
|
||||||
if !assert.Equal(t, resp.StatusCode, 200) {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a monitor cycle before checking for removal.
|
|
||||||
time.Sleep(server.ActiveMonitorTimeout + (1 * time.Second))
|
|
||||||
|
|
||||||
// Verify that we now have one peer.
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 1)
|
|
||||||
|
|
||||||
// Simulate the join failure
|
|
||||||
_, err = server.NewClient(nil).AddMachine("http://localhost:7001",
|
|
||||||
&server.JoinCommand{
|
|
||||||
MinVersion: store.MinVersion(),
|
|
||||||
MaxVersion: store.MaxVersion(),
|
|
||||||
Name: "node2",
|
|
||||||
RaftURL: "http://127.0.0.1:7002",
|
|
||||||
EtcdURL: "http://127.0.0.1:4002",
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
time.Sleep(6 * time.Second)
|
|
||||||
|
|
||||||
go tests.Delete("http://localhost:7001/v2/admin/machines/node2", "application/json", nil)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
result, err = c.Get("_etcd/machines", false, true)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, len(result.Node.Nodes), 1)
|
|
||||||
}
|
|
@ -1,258 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2013 CoreOS Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var client = http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Dial: dialTimeoutFast,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sending set commands
|
|
||||||
func Set(stop chan bool) {
|
|
||||||
|
|
||||||
stopSet := false
|
|
||||||
i := 0
|
|
||||||
c := etcd.NewClient(nil)
|
|
||||||
for {
|
|
||||||
key := fmt.Sprintf("%s_%v", "foo", i)
|
|
||||||
|
|
||||||
result, err := c.Set(key, "bar", 0)
|
|
||||||
|
|
||||||
if err != nil || result.Node.Key != "/"+key || result.Node.Value != "bar" {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
stopSet = true
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
stopSet = true
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if stopSet {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
stop <- true
|
|
||||||
}
|
|
||||||
|
|
||||||
func WaitForServer(host string, client http.Client, scheme string) error {
|
|
||||||
path := fmt.Sprintf("%s://%s/v2/keys/", scheme, host)
|
|
||||||
|
|
||||||
var resp *http.Response
|
|
||||||
var err error
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
resp, err = client.Get(path)
|
|
||||||
if err == nil && resp.StatusCode == 200 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(fmt.Sprintf("etcd server was not reachable in a long time, last-time response and error: %v; %v", resp, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a cluster of etcd nodes
|
|
||||||
func CreateCluster(size int, procAttr *os.ProcAttr, ssl bool) ([][]string, []*os.Process, error) {
|
|
||||||
argGroup := make([][]string, size)
|
|
||||||
|
|
||||||
sslServer1 := []string{"-peer-ca-file=../../fixtures/ca/ca.crt",
|
|
||||||
"-peer-cert-file=../../fixtures/ca/server.crt",
|
|
||||||
"-peer-key-file=../../fixtures/ca/server.key.insecure",
|
|
||||||
}
|
|
||||||
|
|
||||||
sslServer2 := []string{"-peer-ca-file=../../fixtures/ca/ca.crt",
|
|
||||||
"-peer-cert-file=../../fixtures/ca/server2.crt",
|
|
||||||
"-peer-key-file=../../fixtures/ca/server2.key.insecure",
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
if i == 0 {
|
|
||||||
argGroup[i] = []string{"etcd", "-data-dir=/tmp/node1", "-name=node1", "-cluster-remove-delay=1800"}
|
|
||||||
if ssl {
|
|
||||||
argGroup[i] = append(argGroup[i], sslServer1...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
strI := strconv.Itoa(i + 1)
|
|
||||||
argGroup[i] = []string{"etcd", "-name=node" + strI, fmt.Sprintf("-addr=127.0.0.1:%d", 4001+i), fmt.Sprintf("-peer-addr=127.0.0.1:%d", 7001+i), "-data-dir=/tmp/node" + strI, "-peers=127.0.0.1:7001", "-cluster-remove-delay=1800"}
|
|
||||||
if ssl {
|
|
||||||
argGroup[i] = append(argGroup[i], sslServer2...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
etcds := make([]*os.Process, size)
|
|
||||||
|
|
||||||
for i := range etcds {
|
|
||||||
var err error
|
|
||||||
etcds[i], err = os.StartProcess(EtcdBinPath, append(argGroup[i], "-f"), procAttr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The problem is that if the master isn't up then the children
|
|
||||||
// have to retry. This retry can take upwards of 15 seconds
|
|
||||||
// which slows tests way down and some of them fail.
|
|
||||||
//
|
|
||||||
// Waiting for each server to start when ssl is a workaround.
|
|
||||||
// Autotest machines are dramatically slow, and it could spend
|
|
||||||
// several seconds to build TSL connections between servers. That
|
|
||||||
// is extremely terribe when the second machine joins the cluster
|
|
||||||
// because the cluster is out of work at this time. The guy
|
|
||||||
// tries to join during this time will fail, and current implementation
|
|
||||||
// makes it fail after just one-time try(bug in #661). This
|
|
||||||
// makes the cluster start with N-1 machines.
|
|
||||||
// TODO(yichengq): It should be fixed.
|
|
||||||
if i == 0 || ssl {
|
|
||||||
client := buildClient()
|
|
||||||
err = WaitForServer("127.0.0.1:400"+strconv.Itoa(i+1), client, "http")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return argGroup, etcds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy all the nodes in the cluster
|
|
||||||
func DestroyCluster(etcds []*os.Process) error {
|
|
||||||
for _, etcd := range etcds {
|
|
||||||
if etcd == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := etcd.Kill()
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
etcd.Release()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
func Monitor(size int, allowDeadNum int, leaderChan chan string, all chan bool, stop chan bool) {
|
|
||||||
leaderMap := make(map[int]string)
|
|
||||||
baseAddrFormat := "http://0.0.0.0:%d"
|
|
||||||
|
|
||||||
for {
|
|
||||||
knownLeader := "unknown"
|
|
||||||
dead := 0
|
|
||||||
var i int
|
|
||||||
|
|
||||||
for i = 0; i < size; i++ {
|
|
||||||
leader, err := getLeader(fmt.Sprintf(baseAddrFormat, i+4001))
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
leaderMap[i] = leader
|
|
||||||
|
|
||||||
if knownLeader == "unknown" {
|
|
||||||
knownLeader = leader
|
|
||||||
} else {
|
|
||||||
if leader != knownLeader {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
dead++
|
|
||||||
if dead > allowDeadNum {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == size {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case <-leaderChan:
|
|
||||||
leaderChan <- knownLeader
|
|
||||||
default:
|
|
||||||
leaderChan <- knownLeader
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if dead == 0 {
|
|
||||||
select {
|
|
||||||
case <-all:
|
|
||||||
all <- true
|
|
||||||
default:
|
|
||||||
all <- true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLeader(addr string) (string, error) {
|
|
||||||
|
|
||||||
resp, err := client.Get(addr + "/v2/leader")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
resp.Body.Close()
|
|
||||||
return "", fmt.Errorf("no leader")
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(b), nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial with timeout
|
|
||||||
func dialTimeoutFast(network, addr string) (net.Conn, error) {
|
|
||||||
return net.DialTimeout(network, addr, time.Millisecond*10)
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Creates a new HTTP client with KeepAlive disabled.
|
|
||||||
func NewHTTPClient() *http.Client {
|
|
||||||
return &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the body from the response and closes it.
|
|
||||||
func ReadBody(resp *http.Response) []byte {
|
|
||||||
if resp == nil {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the body from the response and parses it as JSON.
|
|
||||||
func ReadBodyJSON(resp *http.Response) map[string]interface{} {
|
|
||||||
m := make(map[string]interface{})
|
|
||||||
b := ReadBody(resp)
|
|
||||||
if err := json.Unmarshal(b, &m); err != nil {
|
|
||||||
panic(fmt.Sprintf("HTTP body JSON parse error: %v: %s", err, string(b)))
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func Head(url string) (*http.Response, error) {
|
|
||||||
return send("HEAD", url, "application/json", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(url string) (*http.Response, error) {
|
|
||||||
return send("GET", url, "application/json", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Post(url string, bodyType string, body io.Reader) (*http.Response, error) {
|
|
||||||
return send("POST", url, bodyType, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostForm(url string, data url.Values) (*http.Response, error) {
|
|
||||||
return Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Put(url string, bodyType string, body io.Reader) (*http.Response, error) {
|
|
||||||
return send("PUT", url, bodyType, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutForm(url string, data url.Values) (*http.Response, error) {
|
|
||||||
return Put(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(url string, bodyType string, body io.Reader) (*http.Response, error) {
|
|
||||||
return send("DELETE", url, bodyType, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteForm(url string, data url.Values) (*http.Response, error) {
|
|
||||||
return Delete(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func send(method string, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
|
||||||
c := NewHTTPClient()
|
|
||||||
req, err := http.NewRequest(method, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", bodyType)
|
|
||||||
return c.Do(req)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A mock Store object used for testing.
|
|
||||||
type Store struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore() *Store {
|
|
||||||
return &Store{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Get(nodePath string, recursive, sorted bool, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, recursive, sorted, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Set(nodePath string, value string, expireTime time.Time, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, value, expireTime, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Update(nodePath string, newValue string, expireTime time.Time, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, newValue, expireTime, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Create(nodePath string, value string, incrementalSuffix bool, expireTime time.Time, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, value, incrementalSuffix, expireTime, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CompareAndSwap(nodePath string, prevValue string, prevIndex uint64, value string, expireTime time.Time, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, prevValue, prevIndex, value, expireTime, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Delete(nodePath string, recursive bool, index uint64, term uint64) (*store.Event, error) {
|
|
||||||
args := s.Called(nodePath, recursive, index, term)
|
|
||||||
return args.Get(0).(*store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Watch(prefix string, recursive bool, sinceIndex uint64, index uint64, term uint64) (<-chan *store.Event, error) {
|
|
||||||
args := s.Called(prefix, recursive, sinceIndex, index, term)
|
|
||||||
return args.Get(0).(<-chan *store.Event), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Save() ([]byte, error) {
|
|
||||||
args := s.Called()
|
|
||||||
return args.Get(0).([]byte), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Recovery(b []byte) error {
|
|
||||||
args := s.Called(b)
|
|
||||||
return args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) JsonStats() []byte {
|
|
||||||
args := s.Called()
|
|
||||||
return args.Get(0).([]byte)
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/coreos/etcd/store"
|
|
||||||
"github.com/coreos/etcd/third_party/github.com/goraft/raft"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A mock Server for the v2 handlers.
|
|
||||||
type ServerV2 struct {
|
|
||||||
mock.Mock
|
|
||||||
store store.Store
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServerV2(store store.Store) *ServerV2 {
|
|
||||||
return &ServerV2{
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) State() string {
|
|
||||||
args := s.Called()
|
|
||||||
return args.String(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) Leader() string {
|
|
||||||
args := s.Called()
|
|
||||||
return args.String(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) CommitIndex() uint64 {
|
|
||||||
args := s.Called()
|
|
||||||
return args.Get(0).(uint64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) Term() uint64 {
|
|
||||||
args := s.Called()
|
|
||||||
return args.Get(0).(uint64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) PeerURL(name string) (string, bool) {
|
|
||||||
args := s.Called(name)
|
|
||||||
return args.String(0), args.Bool(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) ClientURL(name string) (string, bool) {
|
|
||||||
args := s.Called(name)
|
|
||||||
return args.String(0), args.Bool(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) Store() store.Store {
|
|
||||||
return s.store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ServerV2) Dispatch(c raft.Command, w http.ResponseWriter, req *http.Request) error {
|
|
||||||
args := s.Called(c, w, req)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// +build ignore
|
|
||||||
|
|
||||||
package tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/coreos/etcd/config"
|
|
||||||
"github.com/coreos/etcd/etcd"
|
|
||||||
"github.com/coreos/etcd/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testName = "ETCDTEST"
|
|
||||||
testClientURL = "localhost:4401"
|
|
||||||
testRaftURL = "localhost:7701"
|
|
||||||
testSnapshotCount = 10000
|
|
||||||
testHeartbeatInterval = 50
|
|
||||||
testElectionTimeout = 200
|
|
||||||
testDataDir = "/tmp/ETCDTEST"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Starts a new server.
|
|
||||||
func RunServer(f func(*server.Server)) {
|
|
||||||
c := cfg.New()
|
|
||||||
|
|
||||||
c.Name = testName
|
|
||||||
c.Addr = testClientURL
|
|
||||||
c.Peer.Addr = testRaftURL
|
|
||||||
|
|
||||||
c.DataDir = testDataDir
|
|
||||||
c.Force = true
|
|
||||||
|
|
||||||
c.Peer.HeartbeatInterval = testHeartbeatInterval
|
|
||||||
c.Peer.ElectionTimeout = testElectionTimeout
|
|
||||||
c.SnapshotCount = testSnapshotCount
|
|
||||||
|
|
||||||
i := etcd.New(c)
|
|
||||||
go i.Run()
|
|
||||||
<-i.ReadyNotify()
|
|
||||||
// Execute the function passed in.
|
|
||||||
f(i.Server)
|
|
||||||
i.Stop()
|
|
||||||
}
|
|
Reference in New Issue
Block a user