diff --git a/Documentation/clients-matrix.md b/Documentation/clients-matrix.md index a28945a3a3b..11de051de36 100644 --- a/Documentation/clients-matrix.md +++ b/Documentation/clients-matrix.md @@ -1,20 +1,6 @@ # Client libraries support matrix for etcd As etcd features support is really uneven between client libraries, a compatibility matrix can be important. -We will consider in detail only the features of clients supporting the v2 API. Clients still supporting the v1 API *only* are listed below. - -## v1-only clients - -Clients supporting only the API version 1 - -- [justinsb/jetcd](https://github.com/justinsb/jetcd) Java -- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py) Python -- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) Python -- [iconara/etcd-rb](https://github.com/iconara/etcd-rb) Ruby -- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby) Ruby -- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure) Clojure -- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl) Erlang - ## v2 clients @@ -29,16 +15,16 @@ The v2 API has a lot of features, we will categorize them in a few categories: ### Supported features matrix -| Client| [go-etcd](https://github.com/coreos/go-etcd) | [jetcd](https://github.com/diwakergupta/jetcd) | [python-etcd](https://github.com/jplana/python-etcd) | [python-etcd-client](https://github.com/dsoprea/PythonEtcdClient) | [node-etcd](https://github.com/stianeikeland/node-etcd) | [nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) | [etcd-ruby](https://github.com/ranjib/etcd-ruby) | [etcd-api](https://github.com/jdarcy/etcd-api) | [cetcd](https://github.com/dwwoelfel/cetcd) | [clj-etcd](https://github.com/rthomas/clj-etcd) | [etcetera](https://github.com/drusellers/etcetera)| [Etcd.jl](https://github.com/forio/Etcd.jl) | [p5-etcd](https://metacpan.org/release/Etcd) -| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| **HTTPS Auth** | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | -| **Reconnect** | Y | - | Y | Y | - | - | - | Y | - | - | - | - | - | -| **Mod/Lock** | - | - | Y | Y | - | - | - | - | - | - | - | Y | - | -| **Mod/Leader** | - | - | - | Y | - | - | - | - | - | - | - | Y | - | -| **GET Features** | F | B | F | F | F | F | F | B | F | G | F | F | F | -| **PUT Features** | F | B | F | F | F | F | F | G | F | G | F | F | F | -| **POST Features** | F | - | F | F | - | F | F | - | - | - | F | F | F | -| **DEL Features** | F | B | F | F | F | F | F | B | G | B | F | F | F | +| Client| [go-etcd](https://github.com/coreos/go-etcd) | [jetcd](https://github.com/diwakergupta/jetcd) | [python-etcd](https://github.com/jplana/python-etcd) | [python-etcd-client](https://github.com/dsoprea/PythonEtcdClient) | [node-etcd](https://github.com/stianeikeland/node-etcd) | [nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) | [etcd-ruby](https://github.com/ranjib/etcd-ruby) | [etcd-api](https://github.com/jdarcy/etcd-api) | [cetcd](https://github.com/dwwoelfel/cetcd) | [clj-etcd](https://github.com/rthomas/clj-etcd) | [etcetera](https://github.com/drusellers/etcetera)| [Etcd.jl](https://github.com/forio/Etcd.jl) | [p5-etcd](https://metacpan.org/release/Etcd) | [justinsb/jetcd](https://github.com/justinsb/jetcd) | [txetcd](https://github.com/russellhaering/txetcd) +| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| **HTTPS Auth** | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | +| **Reconnect** | Y | - | Y | Y | - | - | - | Y | - | - | - | - | - | - | - | +| **Mod/Lock** | - | - | Y | Y | - | - | - | - | - | - | - | Y | - | - | - | +| **Mod/Leader** | - | - | - | Y | - | - | - | - | - | - | - | Y | - | - | - | +| **GET Features** | F | B | F | F | F | F | F | B | F | G | F | F | F | B | G | +| **PUT Features** | F | B | F | F | F | F | F | G | F | G | F | F | F | B | G | +| **POST Features** | F | - | F | F | - | F | F | - | - | - | F | F | F | - | F | +| **DEL Features** | F | B | F | F | F | F | F | B | G | B | F | F | F | B | G | **Legend** diff --git a/README.md b/README.md index 5b04464020a..2ee395203ac 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ curl -L http://127.0.0.1:4001/version #### API Versioning -Clients are encouraged to use the `v2` API. The `v1` API will not change. - The `v2` API responses should not change after the 0.2.0 release but new features will be added over time. +The `v1` API has been deprecated and will not be supported. + During the pre-v1.0.0 series of releases we may break the API as we fix bugs and get feedback. #### 32bit systems diff --git a/config/cluster_config.go b/config/cluster_config.go new file mode 100644 index 00000000000..47df9e3be1d --- /dev/null +++ b/config/cluster_config.go @@ -0,0 +1,37 @@ +package config + +// 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 + } +} diff --git a/config/config.go b/config/config.go index 167c9b8476c..3c0503888f4 100644 --- a/config/config.go +++ b/config/config.go @@ -18,35 +18,11 @@ import ( "github.com/coreos/etcd/log" ustrings "github.com/coreos/etcd/pkg/strings" - "github.com/coreos/etcd/server" ) // The default location for the etcd configuration file. const DefaultSystemConfigPath = "/etc/etcd/etcd.conf" -// A lookup of deprecated flags to their new flag name. -var newFlagNameLookup = map[string]string{ - "C": "peers", - "CF": "peers-file", - "n": "name", - "c": "addr", - "cl": "bind-addr", - "s": "peer-addr", - "sl": "peer-bind-addr", - "d": "data-dir", - "m": "max-result-buffer", - "r": "max-retry-attempts", - "maxsize": "max-cluster-size", - "clientCAFile": "ca-file", - "clientCert": "cert-file", - "clientKey": "key-file", - "serverCAFile": "peer-ca-file", - "serverCert": "peer-cert-file", - "serverKey": "peer-key-file", - "snapshotCount": "snapshot-count", - "peer-heartbeat-timeout": "peer-heartbeat-interval", -} - // Config represents the server configuration. type Config struct { SystemPath string @@ -103,14 +79,14 @@ func New() *Config { c.Snapshot = true c.SnapshotCount = 10000 c.Peer.Addr = "127.0.0.1:7001" - c.Peer.HeartbeatInterval = defaultHeartbeatInterval - c.Peer.ElectionTimeout = defaultElectionTimeout + 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 = server.DefaultActiveSize - c.Cluster.RemoveDelay = server.DefaultRemoveDelay - c.Cluster.SyncInterval = server.DefaultSyncInterval + c.RetryInterval = float64(50+rand.Int()%50) * DefaultHeartbeatInterval / 1000 + c.Cluster.ActiveSize = DefaultActiveSize + c.Cluster.RemoveDelay = DefaultRemoveDelay + c.Cluster.SyncInterval = DefaultSyncInterval return c } @@ -278,38 +254,10 @@ func (c *Config) LoadFlags(arguments []string) error { f.StringVar(&path, "config", "", "") // BEGIN IGNORED FLAGS - // BEGIN DEPRECATED FLAGS - f.StringVar(&peers, "C", "", "(deprecated)") - f.StringVar(&c.PeersFile, "CF", c.PeersFile, "(deprecated)") - f.StringVar(&c.Name, "n", c.Name, "(deprecated)") - f.StringVar(&c.Addr, "c", c.Addr, "(deprecated)") - f.StringVar(&c.BindAddr, "cl", c.BindAddr, "(deprecated)") - f.StringVar(&c.Peer.Addr, "s", c.Peer.Addr, "(deprecated)") - f.StringVar(&c.Peer.BindAddr, "sl", c.Peer.BindAddr, "(deprecated)") - f.StringVar(&c.Peer.CAFile, "serverCAFile", c.Peer.CAFile, "(deprecated)") - f.StringVar(&c.Peer.CertFile, "serverCert", c.Peer.CertFile, "(deprecated)") - f.StringVar(&c.Peer.KeyFile, "serverKey", c.Peer.KeyFile, "(deprecated)") - f.StringVar(&c.CAFile, "clientCAFile", c.CAFile, "(deprecated)") - f.StringVar(&c.CertFile, "clientCert", c.CertFile, "(deprecated)") - f.StringVar(&c.KeyFile, "clientKey", c.KeyFile, "(deprecated)") - f.StringVar(&c.DataDir, "d", c.DataDir, "(deprecated)") - f.IntVar(&c.MaxResultBuffer, "m", c.MaxResultBuffer, "(deprecated)") - f.IntVar(&c.MaxRetryAttempts, "r", c.MaxRetryAttempts, "(deprecated)") - f.IntVar(&c.SnapshotCount, "snapshotCount", c.SnapshotCount, "(deprecated)") - f.IntVar(&c.Peer.HeartbeatInterval, "peer-heartbeat-timeout", c.Peer.HeartbeatInterval, "(deprecated)") - // END DEPRECATED FLAGS - if err := f.Parse(arguments); err != nil { return err } - // Print deprecation warnings on STDERR. - f.Visit(func(f *flag.Flag) { - if len(newFlagNameLookup[f.Name]) > 0 { - fmt.Fprintf(os.Stderr, "[deprecated] use -%s, not -%s\n", newFlagNameLookup[f.Name], f.Name) - } - }) - // Convert some parameters to lists. if peers != "" { c.Peers = ustrings.TrimSplit(peers, ",") @@ -406,8 +354,8 @@ func (c *Config) Sanitize() error { } // EtcdTLSInfo retrieves a TLSInfo object for the etcd server -func (c *Config) EtcdTLSInfo() *server.TLSInfo { - return &server.TLSInfo{ +func (c *Config) EtcdTLSInfo() *TLSInfo { + return &TLSInfo{ CAFile: c.CAFile, CertFile: c.CertFile, KeyFile: c.KeyFile, @@ -415,8 +363,8 @@ func (c *Config) EtcdTLSInfo() *server.TLSInfo { } // PeerRaftInfo retrieves a TLSInfo object for the peer server. -func (c *Config) PeerTLSInfo() *server.TLSInfo { - return &server.TLSInfo{ +func (c *Config) PeerTLSInfo() *TLSInfo { + return &TLSInfo{ CAFile: c.Peer.CAFile, CertFile: c.Peer.CertFile, KeyFile: c.Peer.KeyFile, @@ -434,8 +382,8 @@ func (c *Config) Trace() bool { return c.strTrace == "*" } -func (c *Config) ClusterConfig() *server.ClusterConfig { - return &server.ClusterConfig{ +func (c *Config) ClusterConfig() *ClusterConfig { + return &ClusterConfig{ ActiveSize: c.Cluster.ActiveSize, RemoveDelay: c.Cluster.RemoveDelay, SyncInterval: c.Cluster.SyncInterval, diff --git a/config/config_test.go b/config/config_test.go index b563de78aed..2888e5615f2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -607,150 +607,6 @@ func TestConfigCLIArgsOverrideEnvVar(t *testing.T) { assert.Equal(t, c.Addr, "127.0.0.1:2000", "") } -//-------------------------------------- -// DEPRECATED (v1) -//-------------------------------------- - -func TestConfigDeprecatedAddrFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-c", "127.0.0.1:4002"}) - assert.NoError(t, err) - assert.Equal(t, c.Addr, "127.0.0.1:4002") - }) - assert.Equal(t, stderr, "[deprecated] use -addr, not -c\n") -} - -func TestConfigDeprecatedBindAddrFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-cl", "127.0.0.1:4003"}) - assert.NoError(t, err) - assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "") - }) - assert.Equal(t, stderr, "[deprecated] use -bind-addr, not -cl\n", "") -} - -func TestConfigDeprecatedCAFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-clientCAFile", "/tmp/file.ca"}) - assert.NoError(t, err) - assert.Equal(t, c.CAFile, "/tmp/file.ca", "") - }) - assert.Equal(t, stderr, "[deprecated] use -ca-file, not -clientCAFile\n", "") -} - -func TestConfigDeprecatedCertFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-clientCert", "/tmp/file.cert"}) - assert.NoError(t, err) - assert.Equal(t, c.CertFile, "/tmp/file.cert", "") - }) - assert.Equal(t, stderr, "[deprecated] use -cert-file, not -clientCert\n", "") -} - -func TestConfigDeprecatedKeyFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-clientKey", "/tmp/file.key"}) - assert.NoError(t, err) - assert.Equal(t, c.KeyFile, "/tmp/file.key", "") - }) - assert.Equal(t, stderr, "[deprecated] use -key-file, not -clientKey\n", "") -} - -func TestConfigDeprecatedPeersFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-C", "coreos.com:4001,coreos.com:4002"}) - assert.NoError(t, err) - assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "") - }) - assert.Equal(t, stderr, "[deprecated] use -peers, not -C\n", "") -} - -func TestConfigDeprecatedPeersFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-CF", "/tmp/machines"}) - assert.NoError(t, err) - assert.Equal(t, c.PeersFile, "/tmp/machines", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peers-file, not -CF\n", "") -} - -func TestConfigDeprecatedMaxRetryAttemptsFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-r", "10"}) - assert.NoError(t, err) - assert.Equal(t, c.MaxRetryAttempts, 10, "") - }) - assert.Equal(t, stderr, "[deprecated] use -max-retry-attempts, not -r\n", "") -} - -func TestConfigDeprecatedNameFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-n", "test-name"}) - assert.NoError(t, err) - assert.Equal(t, c.Name, "test-name", "") - }) - assert.Equal(t, stderr, "[deprecated] use -name, not -n\n", "") -} - -func TestConfigDeprecatedPeerAddrFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-s", "localhost:7002"}) - assert.NoError(t, err) - assert.Equal(t, c.Peer.Addr, "localhost:7002", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peer-addr, not -s\n", "") -} - -func TestConfigDeprecatedPeerBindAddrFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-sl", "127.0.0.1:4003"}) - assert.NoError(t, err) - assert.Equal(t, c.Peer.BindAddr, "127.0.0.1:4003", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peer-bind-addr, not -sl\n", "") -} - -func TestConfigDeprecatedPeerCAFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-serverCAFile", "/tmp/peer/file.ca"}) - assert.NoError(t, err) - assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peer-ca-file, not -serverCAFile\n", "") -} - -func TestConfigDeprecatedPeerCertFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-serverCert", "/tmp/peer/file.cert"}) - assert.NoError(t, err) - assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peer-cert-file, not -serverCert\n", "") -} - -func TestConfigDeprecatedPeerKeyFileFlag(t *testing.T) { - _, stderr := capture(func() { - c := New() - err := c.LoadFlags([]string{"-serverKey", "/tmp/peer/file.key"}) - assert.NoError(t, err) - assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "") - }) - assert.Equal(t, stderr, "[deprecated] use -peer-key-file, not -serverKey\n", "") -} - //-------------------------------------- // Helpers //-------------------------------------- diff --git a/config/default.go b/config/default.go new file mode 100644 index 00000000000..86e5b67c80b --- /dev/null +++ b/config/default.go @@ -0,0 +1,29 @@ +package config + +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) +) diff --git a/config/timeout.go b/config/timeout.go deleted file mode 100644 index a462795a3b8..00000000000 --- a/config/timeout.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -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 -) diff --git a/config/tls_config.go b/config/tls_config.go new file mode 100644 index 00000000000..f3a73d51cac --- /dev/null +++ b/config/tls_config.go @@ -0,0 +1,105 @@ +package config + +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) + } +} diff --git a/etcd/etcd.go b/etcd/etcd.go index 1f4570ae94f..4455bd741c6 100644 --- a/etcd/etcd.go +++ b/etcd/etcd.go @@ -1,11 +1,11 @@ /* -Copyright 2013 CoreOS Inc. +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 + 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, @@ -17,400 +17,155 @@ limitations under the License. package etcd import ( + "crypto/tls" + "log" "net/http" - "os" - "path/filepath" - "runtime" - "strings" "sync" "time" - goetcd "github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd" - golog "github.com/coreos/etcd/third_party/github.com/coreos/go-log/log" - "github.com/coreos/etcd/third_party/github.com/goraft/raft" - httpclient "github.com/coreos/etcd/third_party/github.com/mreiferson/go-httpclient" - "github.com/coreos/etcd/config" - ehttp "github.com/coreos/etcd/http" - "github.com/coreos/etcd/log" - "github.com/coreos/etcd/metrics" - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/store" ) -// TODO(yichengq): constant extraTimeout is a hack. -// Current problem is that there is big lag between join command -// execution and join success. -// Fix it later. It should be removed when proper method is found and -// enough tests are provided. It is expected to be calculated from -// heartbeatInterval and electionTimeout only. -const extraTimeout = time.Duration(1000) * time.Millisecond - -type Etcd struct { - Config *config.Config // etcd config +const ( + participantMode int64 = iota + standbyMode + stopMode +) - Store store.Store // data store - Registry *server.Registry // stores URL information for nodes - Server *server.Server // http server, runs on 4001 by default - PeerServer *server.PeerServer // peer server, runs on 7001 by default - StandbyServer *server.StandbyServer +type Server struct { + config *config.Config + id int64 + pubAddr string + raftPubAddr string + tickDuration time.Duration - server *http.Server - peerServer *http.Server + mode atomicInt + nodes map[string]bool + p *participant + s *standby - mode Mode - modeMutex sync.Mutex - closeChan chan bool - readyNotify chan bool // To signal when server is ready to accept connections - onceReady sync.Once - stopNotify chan bool // To signal when server is stopped totally -} + client *v2client + peerHub *peerHub -// New returns a new Etcd instance. -func New(c *config.Config) *Etcd { - if c == nil { - c = config.New() - } - return &Etcd{ - Config: c, - closeChan: make(chan bool), - readyNotify: make(chan bool), - stopNotify: make(chan bool), - } + stopped bool + mu sync.Mutex + stopc chan struct{} } -// Run the etcd instance. -func (e *Etcd) Run() { - // Sanitize all the input fields. - if err := e.Config.Sanitize(); err != nil { +func New(c *config.Config, id int64) *Server { + if err := c.Sanitize(); err != nil { log.Fatalf("failed sanitizing configuration: %v", err) } - // Force remove server configuration if specified. - if e.Config.Force { - e.Config.Reset() - } - - // Enable options. - if e.Config.VeryVeryVerbose { - log.Verbose = true - raft.SetLogLevel(raft.Trace) - goetcd.SetLogger( - golog.New( - "go-etcd", - false, - golog.CombinedSink( - os.Stdout, - "[%s] %s %-9s | %s\n", - []string{"prefix", "time", "priority", "message"}, - ), - ), - ) - } else if e.Config.VeryVerbose { - log.Verbose = true - raft.SetLogLevel(raft.Debug) - } else if e.Config.Verbose { - log.Verbose = true - } - - if e.Config.CPUProfileFile != "" { - profile(e.Config.CPUProfileFile) - } - - if e.Config.DataDir == "" { - log.Fatal("The data dir was not set and could not be guessed from machine name") - } - - // Create data directory if it doesn't already exist. - if err := os.MkdirAll(e.Config.DataDir, 0744); err != nil { - log.Fatalf("Unable to create path: %s", err) - } - - // Warn people if they have an info file - info := filepath.Join(e.Config.DataDir, "info") - if _, err := os.Stat(info); err == nil { - log.Warnf("All cached configuration is now ignored. The file %s can be removed.", info) - } - - var mbName string - if e.Config.Trace() { - mbName = e.Config.MetricsBucketName() - runtime.SetBlockProfileRate(1) + tc := &tls.Config{ + InsecureSkipVerify: true, } - - mb := metrics.NewBucket(mbName) - - if e.Config.GraphiteHost != "" { - err := mb.Publish(e.Config.GraphiteHost) + var err error + if c.PeerTLSInfo().Scheme() == "https" { + tc, err = c.PeerTLSInfo().ClientConfig() if err != nil { - panic(err) + log.Fatal("failed to create raft transporter tls:", err) } } - // Retrieve CORS configuration - corsInfo, err := ehttp.NewCORSInfo(e.Config.CorsOrigins) - if err != nil { - log.Fatal("CORS:", err) - } + tr := new(http.Transport) + tr.TLSClientConfig = tc + client := &http.Client{Transport: tr} - // Create etcd key-value store and registry. - e.Store = store.New() - e.Registry = server.NewRegistry(e.Store) + s := &Server{ + config: c, + id: id, + pubAddr: c.Addr, + raftPubAddr: c.Peer.Addr, + tickDuration: defaultTickDuration, - // Create stats objects - followersStats := server.NewRaftFollowersStats(e.Config.Name) - serverStats := server.NewRaftServerStats(e.Config.Name) + mode: atomicInt(stopMode), + nodes: make(map[string]bool), - // Calculate all of our timeouts - heartbeatInterval := time.Duration(e.Config.Peer.HeartbeatInterval) * time.Millisecond - electionTimeout := time.Duration(e.Config.Peer.ElectionTimeout) * time.Millisecond - dialTimeout := (3 * heartbeatInterval) + electionTimeout - responseHeaderTimeout := (3 * heartbeatInterval) + electionTimeout + client: newClient(tc), + peerHub: newPeerHub(c.Peers, client), - clientTransporter := &httpclient.Transport{ - ResponseHeaderTimeout: responseHeaderTimeout + extraTimeout, - // This is a workaround for Transport.CancelRequest doesn't work on - // HTTPS connections blocked. The patch for it is in progress, - // and would be available in Go1.3 - // More: https://codereview.appspot.com/69280043/ - ConnectTimeout: dialTimeout + extraTimeout, - RequestTimeout: responseHeaderTimeout + dialTimeout + 2*extraTimeout, + stopc: make(chan struct{}), } - if e.Config.PeerTLSInfo().Scheme() == "https" { - clientTLSConfig, err := e.Config.PeerTLSInfo().ClientConfig() - if err != nil { - log.Fatal("client TLS error: ", err) - } - clientTransporter.TLSClientConfig = clientTLSConfig - clientTransporter.DisableCompression = true + for _, seed := range c.Peers { + s.nodes[seed] = true } - client := server.NewClient(clientTransporter) - // Create peer server - psConfig := server.PeerServerConfig{ - Name: e.Config.Name, - Scheme: e.Config.PeerTLSInfo().Scheme(), - URL: e.Config.Peer.Addr, - SnapshotCount: e.Config.SnapshotCount, - RetryTimes: e.Config.MaxRetryAttempts, - RetryInterval: e.Config.RetryInterval, - } - e.PeerServer = server.NewPeerServer(psConfig, client, e.Registry, e.Store, &mb, followersStats, serverStats) + return s +} - // Create raft transporter and server - raftTransporter := server.NewTransporter(followersStats, serverStats, e.Registry, heartbeatInterval, dialTimeout, responseHeaderTimeout) - if e.Config.PeerTLSInfo().Scheme() == "https" { - raftClientTLSConfig, err := e.Config.PeerTLSInfo().ClientConfig() - if err != nil { - log.Fatal("raft client TLS error: ", err) - } - raftTransporter.SetTLSConfig(*raftClientTLSConfig) - } - raftServer, err := raft.NewServer(e.Config.Name, e.Config.DataDir, raftTransporter, e.Store, e.PeerServer, "") - if err != nil { - log.Fatal(err) - } - raftServer.SetElectionTimeout(electionTimeout) - raftServer.SetHeartbeatInterval(heartbeatInterval) - e.PeerServer.SetRaftServer(raftServer, e.Config.Snapshot) +func (s *Server) SetTick(tick time.Duration) { + s.tickDuration = tick +} - // Create etcd server - e.Server = server.New(e.Config.Name, e.Config.Addr, e.PeerServer, e.Registry, e.Store, &mb) +// Stop stops the server elegently. +func (s *Server) Stop() { + if s.mode.Get() == stopMode { + return + } + s.mu.Lock() + s.stopped = true + switch s.mode.Get() { + case participantMode: + s.p.stop() + case standbyMode: + s.s.stop() + } + s.mu.Unlock() + <-s.stopc + s.client.CloseConnections() + s.peerHub.stop() +} - if e.Config.Trace() { - e.Server.EnableTracing() +func (s *Server) ServeHTTP(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) } +} - e.PeerServer.SetServer(e.Server) +func (s *Server) RaftHandler() http.Handler { + return http.HandlerFunc(s.ServeRaftHTTP) +} - // Create standby server - ssConfig := server.StandbyServerConfig{ - Name: e.Config.Name, - PeerScheme: e.Config.PeerTLSInfo().Scheme(), - PeerURL: e.Config.Peer.Addr, - ClientURL: e.Config.Addr, - DataDir: e.Config.DataDir, +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) } - e.StandbyServer = server.NewStandbyServer(ssConfig, client) - e.StandbyServer.SetRaftServer(raftServer) - - // Generating config could be slow. - // Put it here to make listen happen immediately after peer-server starting. - peerTLSConfig := server.TLSServerConfig(e.Config.PeerTLSInfo()) - etcdTLSConfig := server.TLSServerConfig(e.Config.EtcdTLSInfo()) - - if !e.StandbyServer.IsRunning() { - startPeerServer, possiblePeers, err := e.PeerServer.FindCluster(e.Config.Discovery, e.Config.Peers) - if err != nil { - log.Fatal(err) - } - if startPeerServer { - e.setMode(PeerMode) - } else { - e.StandbyServer.SyncCluster(possiblePeers) - e.setMode(StandbyMode) - } - } else { - e.setMode(StandbyMode) - } - - serverHTTPHandler := &ehttp.CORSHandler{e.Server.HTTPHandler(), corsInfo} - peerServerHTTPHandler := &ehttp.CORSHandler{e.PeerServer.HTTPHandler(), corsInfo} - standbyServerHTTPHandler := &ehttp.CORSHandler{e.StandbyServer.ClientHTTPHandler(), corsInfo} - - log.Infof("etcd server [name %s, listen on %s, advertised url %s]", e.Server.Name, e.Config.BindAddr, e.Server.URL()) - listener := server.NewListener(e.Config.EtcdTLSInfo().Scheme(), e.Config.BindAddr, etcdTLSConfig) - e.server = &http.Server{Handler: &ModeHandler{e, serverHTTPHandler, standbyServerHTTPHandler}} - log.Infof("peer server [name %s, listen on %s, advertised url %s]", e.PeerServer.Config.Name, e.Config.Peer.BindAddr, e.PeerServer.Config.URL) - peerListener := server.NewListener(e.Config.PeerTLSInfo().Scheme(), e.Config.Peer.BindAddr, peerTLSConfig) - e.peerServer = &http.Server{Handler: &ModeHandler{e, peerServerHTTPHandler, http.NotFoundHandler()}} - - wg := sync.WaitGroup{} - wg.Add(2) - go func() { - <-e.readyNotify - defer wg.Done() - if err := e.server.Serve(listener); err != nil { - if !isListenerClosing(err) { - log.Fatal(err) - } - } - }() - go func() { - <-e.readyNotify - defer wg.Done() - if err := e.peerServer.Serve(peerListener); err != nil { - if !isListenerClosing(err) { - log.Fatal(err) - } - } - }() - - e.runServer() - - listener.Close() - peerListener.Close() - wg.Wait() - log.Infof("etcd instance is stopped [name %s]", e.Config.Name) - close(e.stopNotify) } -func (e *Etcd) runServer() { - var removeNotify <-chan bool +func (s *Server) Run() { + next := participantMode for { - if e.mode == PeerMode { - log.Infof("%v starting in peer mode", e.Config.Name) - // Starting peer server should be followed close by listening on its port - // If not, it may leave many requests unaccepted, or cannot receive heartbeat from the cluster. - // One severe problem caused if failing receiving heartbeats is when the second node joins one-node cluster, - // the cluster could be out of work as long as the two nodes cannot transfer messages. - e.PeerServer.Start(e.Config.Snapshot, e.Config.ClusterConfig()) - removeNotify = e.PeerServer.RemoveNotify() - } else { - log.Infof("%v starting in standby mode", e.Config.Name) - e.StandbyServer.Start() - removeNotify = e.StandbyServer.RemoveNotify() + s.mu.Lock() + if s.stopped { + next = stopMode } - - // etcd server is ready to accept connections, notify waiters. - e.onceReady.Do(func() { close(e.readyNotify) }) - - select { - case <-e.closeChan: - e.PeerServer.Stop() - e.StandbyServer.Stop() + switch next { + case participantMode: + s.p = newParticipant(s.id, s.pubAddr, s.raftPubAddr, s.client, s.peerHub, s.tickDuration) + s.mode.Set(participantMode) + s.mu.Unlock() + next = s.p.run() + case standbyMode: + s.s = newStandby(s.id, s.pubAddr, s.raftPubAddr, s.nodes, s.client, s.peerHub) + s.mode.Set(standbyMode) + s.mu.Unlock() + next = s.s.run() + case stopMode: + s.mode.Set(stopMode) + s.mu.Unlock() + s.stopc <- struct{}{} return - case <-removeNotify: - } - - if e.mode == PeerMode { - peerURLs := e.Registry.PeerURLs(e.PeerServer.RaftServer().Leader(), e.Config.Name) - e.StandbyServer.SyncCluster(peerURLs) - e.setMode(StandbyMode) - } else { - // Create etcd key-value store and registry. - e.Store = store.New() - e.Registry = server.NewRegistry(e.Store) - e.PeerServer.SetStore(e.Store) - e.PeerServer.SetRegistry(e.Registry) - e.Server.SetStore(e.Store) - e.Server.SetRegistry(e.Registry) - - // Generate new peer server here. - // TODO(yichengq): raft server cannot be started after stopped. - // It should be removed when raft restart is implemented. - heartbeatInterval := time.Duration(e.Config.Peer.HeartbeatInterval) * time.Millisecond - electionTimeout := time.Duration(e.Config.Peer.ElectionTimeout) * time.Millisecond - raftServer, err := raft.NewServer(e.Config.Name, e.Config.DataDir, e.PeerServer.RaftServer().Transporter(), e.Store, e.PeerServer, "") - if err != nil { - log.Fatal(err) - } - raftServer.SetElectionTimeout(electionTimeout) - raftServer.SetHeartbeatInterval(heartbeatInterval) - e.PeerServer.SetRaftServer(raftServer, e.Config.Snapshot) - e.StandbyServer.SetRaftServer(raftServer) - - e.PeerServer.SetJoinIndex(e.StandbyServer.JoinIndex()) - e.setMode(PeerMode) + default: + panic("unsupport mode") } } } - -// Stop the etcd instance. -func (e *Etcd) Stop() { - close(e.closeChan) - <-e.stopNotify -} - -// ReadyNotify returns a channel that is going to be closed -// when the etcd instance is ready to accept connections. -func (e *Etcd) ReadyNotify() <-chan bool { - return e.readyNotify -} - -func (e *Etcd) Mode() Mode { - e.modeMutex.Lock() - defer e.modeMutex.Unlock() - return e.mode -} - -func (e *Etcd) setMode(m Mode) { - e.modeMutex.Lock() - defer e.modeMutex.Unlock() - e.mode = m -} - -func isListenerClosing(err error) bool { - // An error string equivalent to net.errClosing for using with - // http.Serve() during server shutdown. Need to re-declare - // here because it is not exported by "net" package. - const errClosing = "use of closed network connection" - - return strings.Contains(err.Error(), errClosing) -} - -type ModeGetter interface { - Mode() Mode -} - -type ModeHandler struct { - ModeGetter - PeerModeHandler http.Handler - StandbyModeHandler http.Handler -} - -func (h *ModeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - switch h.Mode() { - case PeerMode: - h.PeerModeHandler.ServeHTTP(w, r) - case StandbyMode: - h.StandbyModeHandler.ServeHTTP(w, r) - } -} - -type Mode int - -const ( - PeerMode Mode = iota - StandbyMode -) diff --git a/etcd/etcd_functional_test.go b/etcd/etcd_functional_test.go new file mode 100644 index 00000000000..c6226b6325c --- /dev/null +++ b/etcd/etcd_functional_test.go @@ -0,0 +1,169 @@ +/* +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 etcd + +import ( + "math/rand" + "net/http/httptest" + "testing" + "time" + + "github.com/coreos/etcd/config" +) + +func TestKillLeader(t *testing.T) { + tests := []int{3, 5, 9, 11} + + for i, tt := range tests { + es, hs := buildCluster(tt, false) + waitCluster(t, es) + waitLeader(es) + + lead := es[0].p.node.Leader() + es[lead].Stop() + + time.Sleep(es[0].tickDuration * defaultElection * 2) + + waitLeader(es) + if es[1].p.node.Leader() == 0 { + t.Errorf("#%d: lead = %d, want not 0", i, es[1].p.node.Leader()) + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func TestRandomKill(t *testing.T) { + tests := []int{3, 5, 9, 11} + + for _, tt := range tests { + es, hs := buildCluster(tt, false) + waitCluster(t, es) + waitLeader(es) + + toKill := make(map[int64]struct{}) + for len(toKill) != tt/2-1 { + toKill[rand.Int63n(int64(tt))] = struct{}{} + } + for k := range toKill { + es[k].Stop() + } + + time.Sleep(es[0].tickDuration * defaultElection * 2) + + waitLeader(es) + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func TestJoinThroughFollower(t *testing.T) { + tests := []int{3, 4, 5, 6} + + for _, tt := range tests { + es := make([]*Server, tt) + hs := make([]*httptest.Server, tt) + for i := 0; i < tt; i++ { + c := config.New() + if i > 0 { + c.Peers = []string{hs[i-1].URL} + } + es[i], hs[i] = initTestServer(c, int64(i), false) + } + + go es[0].Run() + + for i := 1; i < tt; i++ { + go es[i].Run() + waitLeader(es[:i]) + } + waitCluster(t, es) + + for i := range hs { + es[len(hs)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +type leadterm struct { + lead int64 + term int64 +} + +func waitActiveLeader(es []*Server) (lead, term int64) { + for { + if l, t := waitLeader(es); l >= 0 && es[l].mode.Get() == participantMode { + return l, t + } + } +} + +// waitLeader waits until all alive servers are checked to have the same leader. +// WARNING: The lead returned is not guaranteed to be actual leader. +func waitLeader(es []*Server) (lead, term int64) { + for { + ls := make([]leadterm, 0, len(es)) + for i := range es { + switch es[i].mode.Get() { + case participantMode: + ls = append(ls, getLead(es[i])) + case standbyMode: + //TODO(xiangli) add standby support + case stopMode: + } + } + if isSameLead(ls) { + return ls[0].lead, ls[0].term + } + time.Sleep(es[0].tickDuration * defaultElection) + } +} + +func getLead(s *Server) leadterm { + return leadterm{s.p.node.Leader(), s.p.node.Term()} +} + +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 +} diff --git a/etcd/etcd_test.go b/etcd/etcd_test.go index 4d5b9257952..cae36c07fce 100644 --- a/etcd/etcd_test.go +++ b/etcd/etcd_test.go @@ -1,11 +1,11 @@ /* -Copyright 2013 CoreOS Inc. +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 + 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, @@ -17,25 +17,433 @@ limitations under the License. package etcd import ( - "io/ioutil" - "os" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "net/url" "testing" + "time" "github.com/coreos/etcd/config" + "github.com/coreos/etcd/store" ) -func TestRunStop(t *testing.T) { - path, _ := ioutil.TempDir("", "etcd-") - defer os.RemoveAll(path) +func TestMultipleNodes(t *testing.T) { + tests := []int{1, 3, 5, 9, 11} - config := config.New() - config.Name = "ETCDTEST" - config.DataDir = path - config.Addr = "localhost:0" - config.Peer.Addr = "localhost:0" + for _, tt := range tests { + es, hs := buildCluster(tt, false) + waitCluster(t, es) + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func TestMultipleTLSNodes(t *testing.T) { + tests := []int{1, 3, 5} + + for _, tt := range tests { + es, hs := buildCluster(tt, true) + waitCluster(t, es) + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func TestV2Redirect(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + u := hs[1].URL + ru := fmt.Sprintf("%s%s", hs[0].URL, "/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() + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +func TestAdd(t *testing.T) { + tests := []int{3, 4, 5, 6} + + for _, tt := range tests { + es := make([]*Server, tt) + hs := make([]*httptest.Server, tt) + for i := 0; i < tt; i++ { + c := config.New() + if i > 0 { + c.Peers = []string{hs[0].URL} + } + es[i], hs[i] = initTestServer(c, int64(i), false) + } + + go es[0].Run() + waitMode(participantMode, es[0]) + + for i := 1; i < tt; i++ { + id := int64(i) + for { + lead := es[0].p.node.Leader() + if lead == -1 { + time.Sleep(defaultElection * es[0].tickDuration) + continue + } + + err := es[lead].p.add(id, es[id].raftPubAddr, es[id].pubAddr) + if err == nil { + break + } + switch err { + case tmpErr: + time.Sleep(defaultElection * es[0].tickDuration) + case raftStopErr, stopErr: + t.Fatalf("#%d on %d: unexpected stop", i, lead) + default: + t.Fatal(err) + } + } + go es[i].Run() + waitMode(participantMode, es[i]) + + for j := 0; j <= i; j++ { + p := fmt.Sprintf("%s/%d", v2machineKVPrefix, id) + w, err := es[j].p.Watch(p, false, false, 1) + if err != nil { + t.Errorf("#%d on %d: %v", i, j, err) + break + } + <-w.EventChan + } + } + + for i := range hs { + es[len(hs)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func TestRemove(t *testing.T) { + tests := []int{3, 4, 5, 6} + + for k, tt := range tests { + es, hs := buildCluster(tt, false) + waitCluster(t, es) + + lead, _ := waitLeader(es) + config := config.NewClusterConfig() + config.ActiveSize = 0 + if err := es[lead].p.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 := int64(i) + send := id + for { + send++ + if send > int64(tt-1) { + send = id + } + + lead := es[send].p.node.Leader() + if lead == -1 { + time.Sleep(defaultElection * 5 * time.Millisecond) + continue + } + + err := es[lead].p.remove(id) + if err == nil { + break + } + switch err { + case tmpErr: + time.Sleep(defaultElection * 5 * time.Millisecond) + case raftStopErr, stopErr: + if lead == id { + break + } + default: + t.Fatal(err) + } + + } + + waitMode(standbyMode, es[i]) + } + + for i := range es { + es[len(hs)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) + // ensure that no goroutines are running + TestGoroutinesRunning(t) +} + +func TestBecomeStandby(t *testing.T) { + size := 5 + round := 1 + + for j := 0; j < round; j++ { + es, hs := buildCluster(size, false) + waitCluster(t, es) + + lead, _ := waitActiveLeader(es) + i := rand.Intn(size) + // cluster only demotes follower + if int64(i) == lead { + i = (i + 1) % size + } + id := int64(i) + + config := config.NewClusterConfig() + config.SyncInterval = 1000 + + config.ActiveSize = size - 1 + if err := es[lead].p.setClusterConfig(config); err != nil { + t.Fatalf("#%d: setClusterConfig err = %v", i, err) + } + for { + err := es[lead].p.remove(id) + if err == nil { + break + } + switch err { + case tmpErr: + time.Sleep(defaultElection * 5 * time.Millisecond) + default: + t.Fatalf("#%d: remove err = %v", i, err) + } + } + + waitMode(standbyMode, es[i]) + + var leader int64 + for k := 0; k < 4; k++ { + leader, _ = es[i].s.leaderInfo() + if leader != noneId { + break + } + time.Sleep(20 * time.Millisecond) + } + if g := leader; g != lead { + t.Errorf("#%d: lead = %d, want %d", i, g, lead) + } + + for i := range hs { + es[len(hs)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +// TODO(yichengq): cannot handle previous msgDenial correctly now +func TestModeSwitch(t *testing.T) { + t.Skip("not passed") + size := 5 + round := 3 + + for i := 0; i < size; i++ { + es, hs := buildCluster(size, false) + waitCluster(t, es) + + config := config.NewClusterConfig() + config.SyncInterval = 0 + id := int64(i) + for j := 0; j < round; j++ { + lead, _ := waitActiveLeader(es) + // cluster only demotes follower + if lead == id { + continue + } + + config.ActiveSize = size - 1 + if err := es[lead].p.setClusterConfig(config); err != nil { + t.Fatalf("#%d: setClusterConfig err = %v", i, err) + } + if err := es[lead].p.remove(id); err != nil { + t.Fatalf("#%d: remove err = %v", i, err) + } + + waitMode(standbyMode, es[i]) + + for k := 0; k < 4; k++ { + if es[i].s.leader != noneId { + break + } + time.Sleep(20 * time.Millisecond) + } + if g := es[i].s.leader; g != lead { + t.Errorf("#%d: lead = %d, want %d", i, g, lead) + } + + config.ActiveSize = size + if err := es[lead].p.setClusterConfig(config); err != nil { + t.Fatalf("#%d: setClusterConfig err = %v", i, err) + } + + waitMode(participantMode, es[i]) + + if err := checkParticipant(i, es); err != nil { + t.Errorf("#%d: check alive err = %v", i, err) + } + } + + for i := range hs { + es[len(hs)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + } + afterTest(t) +} + +func buildCluster(number int, tls bool) ([]*Server, []*httptest.Server) { + bootstrapper := 0 + es := make([]*Server, number) + hs := make([]*httptest.Server, number) + var seed string + + for i := range es { + c := config.New() + if seed != "" { + c.Peers = []string{seed} + } + es[i], hs[i] = initTestServer(c, int64(i), tls) + + if i == bootstrapper { + seed = hs[i].URL + } else { + // wait for the previous configuration change to be committed + // or this configuration request might be dropped + w, err := es[0].p.Watch(v2machineKVPrefix, true, false, uint64(i)) + if err != nil { + panic(err) + } + <-w.EventChan + } + go es[i].Run() + waitMode(participantMode, es[i]) + } + return es, hs +} + +func initTestServer(c *config.Config, id int64, tls bool) (e *Server, h *httptest.Server) { + e = New(c, id) + e.SetTick(time.Millisecond * 5) + m := http.NewServeMux() + m.Handle("/", e) + m.Handle("/raft", e.RaftHandler()) + m.Handle("/raft/", e.RaftHandler()) + + if tls { + h = httptest.NewTLSServer(m) + } else { + h = httptest.NewServer(m) + } + + e.raftPubAddr = h.URL + e.pubAddr = h.URL + return +} + +func waitCluster(t *testing.T, es []*Server) { + n := len(es) + for i, e := range es { + var index uint64 + for k := 0; k < n; k++ { + index++ + w, err := e.p.Watch(v2machineKVPrefix, true, false, index) + if err != nil { + panic(err) + } + v := <-w.EventChan + // join command may appear several times due to retry + // when timeout + if k > 0 { + pw := fmt.Sprintf("%s/%d", v2machineKVPrefix, k-1) + if v.Node.Key == pw { + continue + } + } + ww := fmt.Sprintf("%s/%d", v2machineKVPrefix, k) + if v.Node.Key != ww { + t.Errorf("#%d path = %v, want %v", i, v.Node.Key, ww) + } + } + } +} + +func waitMode(mode int64, e *Server) { + for { + if e.mode.Get() == mode { + return + } + time.Sleep(10 * time.Millisecond) + } +} + +// checkParticipant checks the i-th server works well as participant. +func checkParticipant(i int, es []*Server) error { + lead, _ := waitActiveLeader(es) + key := fmt.Sprintf("/%d", rand.Int31()) + ev, err := es[lead].p.Set(key, false, "bar", store.Permanent) + if err != nil { + return err + } - etcd := New(config) - go etcd.Run() - <-etcd.ReadyNotify() - etcd.Stop() + w, err := es[i].p.Watch(key, false, false, ev.Index()) + if err != nil { + return err + } + select { + case <-w.EventChan: + case <-time.After(8 * defaultHeartbeat * es[i].tickDuration): + return fmt.Errorf("watch timeout") + } + return nil } diff --git a/etcd/participant.go b/etcd/participant.go new file mode 100644 index 00000000000..f1d2c7f490e --- /dev/null +++ b/etcd/participant.go @@ -0,0 +1,345 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "path" + "sync" + "time" + + etcdErr "github.com/coreos/etcd/error" + "github.com/coreos/etcd/raft" + "github.com/coreos/etcd/store" +) + +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" + v2StoreStatsPrefix = "/v2/stats/store" + v2adminConfigPrefix = "/v2/admin/config" + v2adminMachinesPrefix = "/v2/admin/machines/" +) + +var ( + tmpErr = fmt.Errorf("try again") + stopErr = fmt.Errorf("server is stopped") + raftStopErr = fmt.Errorf("raft is stopped") +) + +type participant struct { + id int64 + pubAddr string + raftPubAddr string + seeds map[string]bool + tickDuration time.Duration + + client *v2client + peerHub *peerHub + + proposal chan v2Proposal + addNodeC chan raft.Config + removeNodeC chan raft.Config + node *v2Raft + store.Store + rh *raftHandler + + stopped bool + mu sync.Mutex + stopc chan struct{} + + *http.ServeMux +} + +func newParticipant(id int64, pubAddr string, raftPubAddr string, client *v2client, peerHub *peerHub, tickDuration time.Duration) *participant { + p := &participant{ + id: id, + pubAddr: pubAddr, + raftPubAddr: raftPubAddr, + 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{ + Node: raft.New(id, defaultHeartbeat, defaultElection), + result: make(map[wait]chan interface{}), + }, + Store: store.New(), + rh: newRaftHandler(peerHub), + + stopc: make(chan struct{}), + + ServeMux: http.NewServeMux(), + } + + 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(v2StoreStatsPrefix, handlerErr(p.serveStoreStats)) + p.Handle(v2adminConfigPrefix, handlerErr(p.serveAdminConfig)) + p.Handle(v2adminMachinesPrefix, handlerErr(p.serveAdminMachines)) + return p +} + +func (p *participant) run() int64 { + seeds := p.peerHub.getSeeds() + if len(seeds) == 0 { + log.Println("starting a bootstrap node") + p.node.Campaign() + p.node.Add(p.id, p.raftPubAddr, []byte(p.pubAddr)) + p.apply(p.node.Next()) + } else { + log.Println("joining cluster via peers", seeds) + p.join() + } + + p.rh.start() + defer p.rh.stop() + + node := p.node + defer node.StopProposalWaiters() + + recv := p.rh.recv + ticker := time.NewTicker(p.tickDuration) + v2SyncTicker := time.NewTicker(time.Millisecond * 500) + + 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 <-p.stopc: + log.Printf("Participant %d stopped\n", p.id) + return stopMode + } + p.apply(node.Next()) + p.send(node.Msgs()) + if node.IsRemoved() { + log.Printf("Participant %d return\n", p.id) + p.stop() + return standbyMode + } + } +} + +func (p *participant) stop() { + p.mu.Lock() + defer p.mu.Unlock() + if p.stopped { + return + } + p.stopped = true + close(p.stopc) +} + +func (p *participant) raftHandler() http.Handler { + return p.rh +} + +func (p *participant) add(id int64, raftPubAddr string, pubAddr string) error { + pp := path.Join(v2machineKVPrefix, fmt.Sprint(id)) + + _, err := p.Get(pp, false, false) + if err == nil { + return nil + } + if v, ok := err.(*etcdErr.Error); !ok || v.ErrorCode != etcdErr.EcodeKeyNotFound { + return err + } + + w, err := p.Watch(pp, true, false, 0) + if err != nil { + log.Println("add error:", err) + return tmpErr + } + + select { + case p.addNodeC <- raft.Config{NodeId: id, Addr: raftPubAddr, Context: []byte(pubAddr)}: + default: + w.Remove() + log.Println("unable to send out addNode proposal") + return tmpErr + } + + select { + case v := <-w.EventChan: + if v.Action == store.Set { + return nil + } + log.Println("add error: action =", v.Action) + return tmpErr + case <-time.After(6 * defaultHeartbeat * p.tickDuration): + w.Remove() + log.Println("add error: wait timeout") + return tmpErr + case <-p.stopc: + return stopErr + } +} + +func (p *participant) remove(id int64) error { + pp := path.Join(v2machineKVPrefix, fmt.Sprint(id)) + + v, err := p.Get(pp, false, false) + if err != nil { + return nil + } + + select { + case p.removeNodeC <- raft.Config{NodeId: id}: + default: + log.Println("unable to send out removeNode proposal") + 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.Println("remove error:", err) + return tmpErr + } + + select { + case v := <-w.EventChan: + if v.Action == store.Delete { + return nil + } + log.Println("remove error: action =", v.Action) + return tmpErr + case <-time.After(6 * defaultHeartbeat * p.tickDuration): + w.Remove() + log.Println("remove error: wait timeout") + return tmpErr + case <-p.stopc: + 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.AddNode: + cfg := new(raft.Config) + if err := json.Unmarshal(ent.Data, cfg); err != nil { + log.Println(err) + break + } + peer, err := p.peerHub.add(cfg.NodeId, cfg.Addr) + if err != nil { + log.Println(err) + break + } + peer.participate() + log.Printf("Add Node %x %v %v\n", cfg.NodeId, cfg.Addr, string(cfg.Context)) + pp := path.Join(v2machineKVPrefix, fmt.Sprint(cfg.NodeId)) + p.Store.Set(pp, false, fmt.Sprintf("raft=%v&etcd=%v", cfg.Addr, string(cfg.Context)), store.Permanent) + case raft.RemoveNode: + cfg := new(raft.Config) + if err := json.Unmarshal(ent.Data, cfg); err != nil { + log.Println(err) + break + } + log.Printf("Remove Node %x\n", cfg.NodeId) + peer, err := p.peerHub.peer(cfg.NodeId) + if err != nil { + log.Fatal("cannot get the added peer:", err) + } + peer.idle() + pp := path.Join(v2machineKVPrefix, fmt.Sprint(cfg.NodeId)) + p.Store.Delete(pp, false, false) + default: + panic("unimplemented") + } + } +} + +func (p *participant) send(msgs []raft.Message) { + for i := range msgs { + if err := p.peerHub.send(msgs[i]); err != nil { + log.Println("send:", err) + } + } +} + +func (p *participant) join() { + info := &context{ + MinVersion: store.MinVersion(), + MaxVersion: store.MaxVersion(), + ClientURL: p.pubAddr, + PeerURL: p.raftPubAddr, + } + + for { + for seed := range p.peerHub.getSeeds() { + if err := p.client.AddMachine(seed, fmt.Sprint(p.id), info); err == nil { + return + } else { + log.Println(err) + } + } + time.Sleep(100 * time.Millisecond) + } + log.Println("fail to join the cluster") +} diff --git a/etcd/peer.go b/etcd/peer.go new file mode 100644 index 00000000000..b007843c168 --- /dev/null +++ b/etcd/peer.go @@ -0,0 +1,144 @@ +/* +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 etcd + +import ( + "bytes" + "fmt" + "log" + "net/http" + "sync" + "sync/atomic" +) + +const ( + maxInflight = 4 +) + +const ( + participantPeer = iota + idlePeer + stoppedPeer +) + +type peer struct { + url string + queue chan []byte + status int + inflight atomicInt + c *http.Client + mu sync.RWMutex + wg sync.WaitGroup +} + +func newPeer(url string, c *http.Client) *peer { + return &peer{ + url: url, + status: idlePeer, + c: c, + } +} + +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) + resp, err := p.c.Post(p.url, "application/octet-stream", buf) + if err != nil { + log.Println("post:", 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) +} diff --git a/etcd/peer_hub.go b/etcd/peer_hub.go new file mode 100644 index 00000000000..f3ed065f055 --- /dev/null +++ b/etcd/peer_hub.go @@ -0,0 +1,155 @@ +/* +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 etcd + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + "sync" + + "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 +} + +func newPeerHub(seeds []string, c *http.Client) *peerHub { + h := &peerHub{ + peers: make(map[int64]*peer), + seeds: make(map[string]bool), + c: c, + } + for _, seed := range seeds { + h.seeds[seed] = true + } + return h +} + +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() + } + 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) + 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 + } + return p.send(data) + } + 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)) +} diff --git a/etcd/profile.go b/etcd/profile.go deleted file mode 100644 index 76632573711..00000000000 --- a/etcd/profile.go +++ /dev/null @@ -1,27 +0,0 @@ -package etcd - -import ( - "os" - "os/signal" - "runtime/pprof" - - "github.com/coreos/etcd/log" -) - -// profile starts CPU profiling. -func profile(path string) { - f, err := os.Create(path) - if err != nil { - log.Fatal(err) - } - pprof.StartCPUProfile(f) - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - sig := <-c - log.Infof("captured %v, stopping profiler and exiting..", sig) - pprof.StopCPUProfile() - os.Exit(1) - }() -} diff --git a/etcd/raft_handler.go b/etcd/raft_handler.go new file mode 100644 index 00000000000..9cf57e1f5ad --- /dev/null +++ b/etcd/raft_handler.go @@ -0,0 +1,112 @@ +/* +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 etcd + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + "sync" + + "github.com/coreos/etcd/raft" +) + +const ( + raftPrefix = "/raft" +) + +type raftHandler struct { + mu sync.RWMutex + serving bool + + peerGetter peerGetter + + recv chan *raft.Message + *http.ServeMux +} + +func newRaftHandler(p peerGetter) *raftHandler { + h := &raftHandler{ + recv: make(chan *raft.Message, 512), + peerGetter: p, + } + h.ServeMux = http.NewServeMux() + h.ServeMux.HandleFunc(raftPrefix+"/cfg/", h.serveCfg) + h.ServeMux.HandleFunc(raftPrefix, h.serveRaft) + 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.Println(err) + return + } + + select { + case h.recv <- msg: + default: + log.Println("drop") + // 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) +} diff --git a/etcd/standby.go b/etcd/standby.go new file mode 100644 index 00000000000..21d877f4ae1 --- /dev/null +++ b/etcd/standby.go @@ -0,0 +1,171 @@ +/* +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 etcd + +import ( + "fmt" + "log" + "net/http" + "strconv" + "sync" + "time" + + "github.com/coreos/etcd/config" + "github.com/coreos/etcd/store" +) + +var ( + noneId int64 = -1 +) + +type standby struct { + id int64 + pubAddr string + raftPubAddr string + + client *v2client + peerHub *peerHub + + nodes map[string]bool + + leader int64 + leaderAddr string + mu sync.RWMutex + clusterConf *config.ClusterConfig + + stopc chan struct{} + + *http.ServeMux +} + +func newStandby(id int64, pubAddr string, raftPubAddr string, nodes map[string]bool, client *v2client, peerHub *peerHub) *standby { + s := &standby{ + id: id, + pubAddr: pubAddr, + raftPubAddr: raftPubAddr, + + client: client, + peerHub: peerHub, + + nodes: nodes, + + leader: noneId, + leaderAddr: "", + clusterConf: config.NewClusterConfig(), + + stopc: make(chan struct{}), + + ServeMux: http.NewServeMux(), + } + s.Handle("/", handlerErr(s.serveRedirect)) + return s +} + +func (s *standby) run() int64 { + var syncDuration time.Duration + for { + select { + case <-time.After(syncDuration): + case <-s.stopc: + log.Printf("Standby %d stopped\n", s.id) + return stopMode + } + + if err := s.syncCluster(); err != nil { + log.Println("standby sync:", err) + continue + } + syncDuration = time.Duration(s.clusterConf.SyncInterval * float64(time.Second)) + if s.clusterConf.ActiveSize <= len(s.nodes) { + continue + } + if err := s.joinByAddr(s.leaderAddr); err != nil { + log.Println("standby join:", err) + continue + } + return participantMode + } +} + +func (s *standby) stop() { + close(s.stopc) +} + +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() error { + for node := range s.nodes { + machines, err := s.client.GetMachines(node) + if err != nil { + continue + } + config, err := s.client.GetClusterConfig(node) + if err != nil { + continue + } + s.nodes = make(map[string]bool) + for _, machine := range machines { + s.nodes[machine.PeerURL] = true + if machine.State == stateLeader { + id, err := strconv.ParseInt(machine.Name, 0, 64) + if err != nil { + return err + } + s.setLeaderInfo(id, machine.PeerURL) + } + } + s.clusterConf = config + return nil + } + return fmt.Errorf("unreachable cluster") +} + +func (s *standby) joinByAddr(addr string) error { + info := &context{ + MinVersion: store.MinVersion(), + MaxVersion: store.MaxVersion(), + ClientURL: s.pubAddr, + PeerURL: s.raftPubAddr, + } + if err := s.client.AddMachine(s.leaderAddr, fmt.Sprint(s.id), info); err != nil { + return err + } + return nil +} diff --git a/etcd/v2_admin.go b/etcd/v2_admin.go new file mode 100644 index 00000000000..7ab50de1fba --- /dev/null +++ b/etcd/v2_admin.go @@ -0,0 +1,182 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" + + "github.com/coreos/etcd/config" + "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.someMachineMessage(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() *config.ClusterConfig { + c := config.NewClusterConfig() + // This is used for backward compatibility because it doesn't + // set cluster config in older version. + if e, err := p.Get(v2configKVPrefix, false, false); err == nil { + json.Unmarshal([]byte(*e.Node.Value), c) + } + return c +} + +func (p *participant) setClusterConfig(c *config.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 +} + +// someMachineMessage return machine message of specified name. +func (p *participant) someMachineMessage(name string) (*machineMessage, error) { + pp := filepath.Join(v2machineKVPrefix, name) + e, err := p.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.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 +} diff --git a/etcd/v2_apply.go b/etcd/v2_apply.go new file mode 100644 index 00000000000..e614d61c590 --- /dev/null +++ b/etcd/v2_apply.go @@ -0,0 +1,81 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "log" + + "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 + + cmd := new(cmd) + if err := json.Unmarshal(ent.Data, cmd); err != nil { + log.Println("v2apply.decode:", err) + return + } + + switch cmd.Type { + case "set": + e, err = p.Store.Set(cmd.Key, cmd.Dir, cmd.Value, cmd.Time) + case "update": + e, err = p.Store.Update(cmd.Key, cmd.Value, cmd.Time) + case "create", "unique": + e, err = p.Store.Create(cmd.Key, cmd.Dir, cmd.Value, cmd.Unique, cmd.Time) + case "delete": + e, err = p.Store.Delete(cmd.Key, cmd.Dir, cmd.Recursive) + case "cad": + e, err = p.Store.CompareAndDelete(cmd.Key, cmd.PrevValue, cmd.PrevIndex) + case "cas": + e, err = p.Store.CompareAndSwap(cmd.Key, cmd.PrevValue, cmd.PrevIndex, cmd.Value, cmd.Time) + case "sync": + p.Store.DeleteExpiredKeys(cmd.Time) + return + default: + log.Println("unexpected command type:", 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) +} diff --git a/etcd/v2_client.go b/etcd/v2_client.go new file mode 100644 index 00000000000..583ffefb861 --- /dev/null +++ b/etcd/v2_client.go @@ -0,0 +1,257 @@ +/* +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 etcd + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/coreos/etcd/config" + 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) (*config.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) + } + + config := new(config.ClusterConfig) + if uerr := c.readJSONBody(resp.Body, config); uerr != nil { + return nil, uerr + } + return config, 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 + + log.Printf("Send Join Request to %s", url) + 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 { + log.Printf("Error parsing join response: %v", err) + 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) +} diff --git a/etcd/v2_http.go b/etcd/v2_http.go new file mode 100644 index 00000000000..f83d11b957c --- /dev/null +++ b/etcd/v2_http.go @@ -0,0 +1,150 @@ +/* +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 etcd + +import ( + "fmt" + "log" + "net/http" + "net/url" + "strings" + + 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) 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.Println("http error", 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 { + log.Println("redirect cannot find node", id) + 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 { + log.Println("redirect cannot build new url:", err) + 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("redirect cannot parse url: %v", err) + } + + redirectURL.Path = originalURL.Path + redirectURL.RawQuery = originalURL.RawQuery + redirectURL.Fragment = originalURL.Fragment + return redirectURL.String(), nil +} diff --git a/etcd/v2_http_delete.go b/etcd/v2_http_delete.go new file mode 100644 index 00000000000..703d9c333ac --- /dev/null +++ b/etcd/v2_http_delete.go @@ -0,0 +1,85 @@ +/* +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 etcd + +import ( + "log" + "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 + } + log.Println("delete:", err) + 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 + } + log.Println("cad:", err) + return err +} diff --git a/etcd/v2_http_endpoint_test.go b/etcd/v2_http_endpoint_test.go new file mode 100644 index 00000000000..e13ca523509 --- /dev/null +++ b/etcd/v2_http_endpoint_test.go @@ -0,0 +1,341 @@ +/* +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 etcd + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "sort" + "strings" + "testing" + + "github.com/coreos/etcd/config" + "github.com/coreos/etcd/store" +) + +func TestMachinesEndPoint(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + + w := make([]string, len(hs)) + for i := range hs { + w[i] = hs[i].URL + } + + for i := range hs { + r, err := http.Get(hs[i].URL + 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) + } + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +func TestLeaderEndPoint(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + + us := make([]string, len(hs)) + for i := range hs { + us[i] = hs[i].URL + } + // todo(xiangli) change this to raft port... + w := hs[0].URL + "/raft" + + for i := range hs { + r, err := http.Get(hs[i].URL + 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) + } + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +func TestStoreStatsEndPoint(t *testing.T) { + es, hs := buildCluster(1, false) + waitCluster(t, es) + + resp, err := http.Get(hs[0].URL + 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) + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +func TestGetAdminConfigEndPoint(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + + for i := range hs { + r, err := http.Get(hs[i].URL + v2adminConfigPrefix) + if err != nil { + t.Errorf("%v", err) + continue + } + if g := r.StatusCode; g != 200 { + t.Errorf("#%d: status = %d, want %d", i, g, 200) + } + if g := r.Header.Get("Content-Type"); g != "application/json" { + t.Errorf("#%d: ContentType = %d, want application/json", i, g) + } + + conf := new(config.ClusterConfig) + err = json.NewDecoder(r.Body).Decode(conf) + r.Body.Close() + if err != nil { + t.Errorf("%v", err) + continue + } + w := config.NewClusterConfig() + if !reflect.DeepEqual(conf, w) { + t.Errorf("#%d: config = %+v, want %+v", i, conf, w) + } + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +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 { + es, hs := buildCluster(3, false) + waitCluster(t, es) + index := es[0].p.Index() + + r, err := NewTestClient().Put(hs[0].URL+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) + } + + for j := range es { + w, err := es[j].p.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.%d: %s = %s, want %s", i, j, v2configKVPrefix, g, tt.wc) + } + } + + for j := range es { + es[len(es)-j-1].Stop() + } + for j := range hs { + hs[len(hs)-j-1].Close() + } + } + afterTest(t) +} + +func TestGetAdminMachineEndPoint(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + + for i := range es { + for j := range hs { + name := fmt.Sprint(es[i].id) + r, err := http.Get(hs[j].URL + 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 = %d, 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: hs[i].URL, + PeerURL: hs[i].URL, + } + if i == 0 { + wm.State = stateLeader + } + if !reflect.DeepEqual(m, wm) { + t.Errorf("#%d on %d: body = %+v, want %+v", i, j, m, wm) + } + } + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +func TestGetAdminMachinesEndPoint(t *testing.T) { + es, hs := buildCluster(3, false) + waitCluster(t, es) + + w := make([]*machineMessage, len(hs)) + for i := range hs { + w[i] = &machineMessage{ + Name: fmt.Sprint(es[i].id), + State: stateFollower, + ClientURL: hs[i].URL, + PeerURL: hs[i].URL, + } + } + w[0].State = stateLeader + + for i := range hs { + r, err := http.Get(hs[i].URL + 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) + } + } + + for i := range es { + es[len(es)-i-1].Stop() + } + for i := range hs { + hs[len(hs)-i-1].Close() + } + afterTest(t) +} + +// 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] } diff --git a/etcd/v2_http_get.go b/etcd/v2_http_get.go new file mode 100644 index 00000000000..e7c79a16e3e --- /dev/null +++ b/etcd/v2_http_get.go @@ -0,0 +1,127 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + etcdErr "github.com/coreos/etcd/error" +) + +func (p *participant) GetHandler(w http.ResponseWriter, req *http.Request) error { + key := req.URL.Path[len("/v2/keys"):] + // TODO(xiangli): handle consistent get + recursive := (req.FormValue("recursive") == "true") + sort := (req.FormValue("sorted") == "true") + waitIndex := req.FormValue("waitIndex") + stream := (req.FormValue("stream") == "true") + 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) + + 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) 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) +} diff --git a/etcd/v2_http_kv_test.go b/etcd/v2_http_kv_test.go new file mode 100644 index 00000000000..67c8f6d4240 --- /dev/null +++ b/etcd/v2_http_kv_test.go @@ -0,0 +1,963 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + "time" +) + +func TestV2Set(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + v := url.Values{} + v.Set("value", "bar") + + tests := []struct { + relativeURL string + value url.Values + wStatus int + w string + }{ + { + "/v2/keys/foo/bar", + v, + http.StatusCreated, + `{"action":"set","node":{"key":"/foo/bar","value":"bar","modifiedIndex":2,"createdIndex":2}}`, + }, + { + "/v2/keys/foodir?dir=true", + url.Values{}, + http.StatusCreated, + `{"action":"set","node":{"key":"/foodir","dir":true,"modifiedIndex":3,"createdIndex":3}}`, + }, + { + "/v2/keys/fooempty", + url.Values(map[string][]string{"value": {""}}), + http.StatusCreated, + `{"action":"set","node":{"key":"/fooempty","value":"","modifiedIndex":4,"createdIndex":4}}`, + }, + } + + for i, tt := range tests { + resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) + if err != nil { + t.Errorf("#%d: err = %v, want nil", i, err) + } + g := string(tc.ReadBody(resp)) + if g != tt.w { + t.Errorf("#%d: body = %v, want %v", i, g, tt.w) + } + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2CreateUpdate(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + tests := []struct { + relativeURL string + value url.Values + wStatus int + w map[string]interface{} + }{ + // key with ttl + { + "/v2/keys/ttl/foo", + url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"20"}}), + http.StatusCreated, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "XXX", + "ttl": float64(20), + }, + }, + }, + // key with bad ttl + { + "/v2/keys/ttl/foo", + url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"bad_ttl"}}), + http.StatusBadRequest, + map[string]interface{}{ + "errorCode": float64(202), + "message": "The given TTL in POST form is not a number", + "cause": "Update", + }, + }, + // create key + { + "/v2/keys/create/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}), + http.StatusCreated, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "XXX", + }, + }, + }, + // created key failed + { + "/v2/keys/create/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(105), + "message": "Key already exists", + "cause": "/create/foo", + }, + }, + // update the newly created key with ttl + { + "/v2/keys/create/foo", + url.Values(map[string][]string{"value": {"YYY"}, "prevExist": {"true"}, "ttl": {"20"}}), + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "YYY", + "ttl": float64(20), + }, + "action": "update", + }, + }, + // update the ttl to none + { + "/v2/keys/create/foo", + url.Values(map[string][]string{"value": {"ZZZ"}, "prevExist": {"true"}}), + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "ZZZ", + }, + "action": "update", + }, + }, + // update on a non-existing key + { + "/v2/keys/nonexist", + url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}}), + http.StatusNotFound, + map[string]interface{}{ + "errorCode": float64(100), + "message": "Key not found", + "cause": "/nonexist", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2CAS(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + tests := []struct { + relativeURL string + value url.Values + wStatus int + w map[string]interface{} + }{ + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}}), + http.StatusCreated, + nil, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"2"}}), + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "YYY", + "modifiedIndex": float64(3), + }, + "action": "compareAndSwap", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[10 != 3]", + "index": float64(3), + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"bad_index"}}), + http.StatusBadRequest, + map[string]interface{}{ + "errorCode": float64(203), + "message": "The given index in POST form is not a number", + "cause": "CompareAndSwap", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"ZZZ"}, "prevValue": {"YYY"}}), + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "value": "ZZZ", + }, + "action": "compareAndSwap", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[bad_value != ZZZ]", + }, + }, + // prevValue is required + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {""}}), + http.StatusBadRequest, + map[string]interface{}{ + "errorCode": float64(201), + "message": "PrevValue is Required in POST form", + "cause": "CompareAndSwap", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"100"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[bad_value != ZZZ] [100 != 4]", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"ZZZ"}, "prevIndex": {"100"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[100 != 4]", + }, + }, + { + "/v2/keys/cas/foo", + url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"4"}}), + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[bad_value != ZZZ]", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2Delete(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + v := url.Values{} + v.Set("value", "XXX") + resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/emptydir?dir=true"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foodir/bar?dir=true"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + + tests := []struct { + relativeURL string + wStatus int + w map[string]interface{} + }{ + { + "/v2/keys/foo", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo", + }, + "prevNode": map[string]interface{}{ + "key": "/foo", + "value": "XXX", + }, + "action": "delete", + }, + }, + { + "/v2/keys/emptydir", + http.StatusForbidden, + map[string]interface{}{ + "errorCode": float64(102), + "message": "Not a file", + "cause": "/emptydir", + }, + }, + { + "/v2/keys/emptydir?dir=true", + http.StatusOK, + nil, + }, + { + "/v2/keys/foodir?dir=true", + http.StatusForbidden, + map[string]interface{}{ + "errorCode": float64(108), + "message": "Directory not empty", + "cause": "/foodir", + }, + }, + { + "/v2/keys/foodir?recursive=true", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foodir", + "dir": true, + }, + "prevNode": map[string]interface{}{ + "key": "/foodir", + "dir": true, + }, + "action": "delete", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2CAD(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + v := url.Values{} + v.Set("value", "XXX") + resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + + resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foovalue"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + + tests := []struct { + relativeURL string + wStatus int + w map[string]interface{} + }{ + { + "/v2/keys/foo?prevIndex=100", + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[100 != 2]", + }, + }, + { + "/v2/keys/foo?prevIndex=bad_index", + http.StatusBadRequest, + map[string]interface{}{ + "errorCode": float64(203), + "message": "The given index in POST form is not a number", + "cause": "CompareAndDelete", + }, + }, + { + "/v2/keys/foo?prevIndex=2", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo", + "modifiedIndex": float64(4), + }, + "action": "compareAndDelete", + }, + }, + { + "/v2/keys/foovalue?prevValue=YYY", + http.StatusPreconditionFailed, + map[string]interface{}{ + "errorCode": float64(101), + "message": "Compare failed", + "cause": "[YYY != XXX]", + }, + }, + { + "/v2/keys/foovalue?prevValue=", + http.StatusBadRequest, + map[string]interface{}{ + "errorCode": float64(201), + "message": "PrevValue is Required in POST form", + "cause": "CompareAndDelete", + }, + }, + { + "/v2/keys/foovalue?prevValue=XXX", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foovalue", + "modifiedIndex": float64(5), + }, + "action": "compareAndDelete", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2Unique(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + tests := []struct { + relativeURL string + value url.Values + wStatus int + w map[string]interface{} + }{ + { + "/v2/keys/foo", + url.Values(map[string][]string{"value": {"XXX"}}), + http.StatusCreated, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo/2", + "value": "XXX", + }, + "action": "create", + }, + }, + { + "/v2/keys/foo", + url.Values(map[string][]string{"value": {"XXX"}}), + http.StatusCreated, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo/3", + "value": "XXX", + }, + "action": "create", + }, + }, + { + "/v2/keys/bar", + url.Values(map[string][]string{"value": {"XXX"}}), + http.StatusCreated, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/bar/4", + "value": "XXX", + }, + "action": "create", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.PostForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2Get(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + v := url.Values{} + v.Set("value", "XXX") + resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar/zar"), v) + if err != nil { + t.Error(err) + } + resp.Body.Close() + + tests := []struct { + relativeURL string + wStatus int + w map[string]interface{} + }{ + { + "/v2/keys/foo/bar/zar", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo/bar/zar", + "value": "XXX", + }, + "action": "get", + }, + }, + { + "/v2/keys/foo", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo", + "dir": true, + "nodes": []interface{}{ + map[string]interface{}{ + "key": "/foo/bar", + "dir": true, + "createdIndex": float64(2), + "modifiedIndex": float64(2), + }, + }, + }, + "action": "get", + }, + }, + { + "/v2/keys/foo?recursive=true", + http.StatusOK, + map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo", + "dir": true, + "nodes": []interface{}{ + map[string]interface{}{ + "key": "/foo/bar", + "dir": true, + "createdIndex": float64(2), + "modifiedIndex": float64(2), + "nodes": []interface{}{ + map[string]interface{}{ + "key": "/foo/bar/zar", + "value": "XXX", + "createdIndex": float64(2), + "modifiedIndex": float64(2), + }, + }, + }, + }, + }, + "action": "get", + }, + }, + } + + for i, tt := range tests { + resp, _ := tc.Get(fmt.Sprintf("%s%s", u, tt.relativeURL)) + if resp.StatusCode != tt.wStatus { + t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) + } + if resp.Header.Get("Content-Type") != "application/json" { + t.Errorf("#%d: header = %v, want %v", resp.Header.Get("Content-Type"), "application/json") + } + if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { + t.Errorf("#%d: %v", i, err) + } + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2Watch(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + var watchResp *http.Response + c := make(chan bool) + go func() { + watchResp, _ = tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true")) + c <- true + }() + + // Make sure response didn't fire early. + time.Sleep(1 * time.Millisecond) + + // Set a value. + v := url.Values{} + v.Set("value", "XXX") + resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) + resp.Body.Close() + + select { + case <-c: + case <-time.After(time.Millisecond): + t.Fatal("cannot get watch result") + } + + body := tc.ReadBodyJSON(watchResp) + w := map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo/bar", + "value": "XXX", + "modifiedIndex": float64(2), + }, + "action": "set", + } + + if err := checkBody(body, w); err != nil { + t.Error(err) + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2WatchWithIndex(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + var body map[string]interface{} + c := make(chan bool, 1) + go func() { + resp, _ := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true&waitIndex=3")) + body = tc.ReadBodyJSON(resp) + c <- true + }() + + select { + case <-c: + t.Fatal("should not get the watch result") + case <-time.After(time.Millisecond): + } + + // Set a value (before given index). + v := url.Values{} + v.Set("value", "XXX") + resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) + resp.Body.Close() + + select { + case <-c: + t.Fatal("should not get the watch result") + case <-time.After(time.Millisecond): + } + + // Set a value (before given index). + resp, _ = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) + resp.Body.Close() + + select { + case <-c: + case <-time.After(time.Millisecond): + t.Fatal("cannot get watch result") + } + + w := map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/foo/bar", + "value": "XXX", + "modifiedIndex": float64(3), + }, + "action": "set", + } + if err := checkBody(body, w); err != nil { + t.Error(err) + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2WatchKeyInDir(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + var body map[string]interface{} + c := make(chan bool) + + // Set a value (before given index). + v := url.Values{} + v.Set("dir", "true") + v.Set("ttl", "1") + resp, _ := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir"), v) + resp.Body.Close() + + // Set a value (before given index). + v = url.Values{} + v.Set("value", "XXX") + resp, _ = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar"), v) + resp.Body.Close() + + go func() { + resp, _ := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar?wait=true")) + body = tc.ReadBodyJSON(resp) + c <- true + }() + + select { + case <-c: + case <-time.After(time.Millisecond * 1500): + t.Fatal("cannot get watch result") + } + + w := map[string]interface{}{ + "node": map[string]interface{}{ + "key": "/keyindir", + }, + "action": "expire", + } + if err := checkBody(body, w); err != nil { + t.Error(err) + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func TestV2Head(t *testing.T) { + es, hs := buildCluster(1, false) + u := hs[0].URL + tc := NewTestClient() + + v := url.Values{} + v.Set("value", "XXX") + fullURL := fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar") + resp, _ := tc.Head(fullURL) + resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) + } + if resp.ContentLength != -1 { + t.Errorf("ContentLength = %d, want -1", resp.ContentLength) + } + + resp, _ = tc.PutForm(fullURL, v) + resp.Body.Close() + + resp, _ = tc.Head(fullURL) + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + if resp.ContentLength != -1 { + t.Errorf("ContentLength = %d, want -1", resp.ContentLength) + } + + es[0].Stop() + hs[0].Close() + afterTest(t) +} + +func checkBody(body map[string]interface{}, w map[string]interface{}) error { + if body["node"] != nil { + if w["node"] != nil { + wn := w["node"].(map[string]interface{}) + n := body["node"].(map[string]interface{}) + for k := range n { + if wn[k] == nil { + delete(n, k) + } + } + body["node"] = n + } + if w["prevNode"] != nil { + wn := w["prevNode"].(map[string]interface{}) + n := body["prevNode"].(map[string]interface{}) + for k := range n { + if wn[k] == nil { + delete(n, k) + } + } + body["prevNode"] = n + } + } + for k, v := range w { + g := body[k] + if !reflect.DeepEqual(g, v) { + return fmt.Errorf("%v = %+v, want %+v", k, g, v) + } + } + return nil +} + +type testHttpClient struct { + *http.Client +} + +// Creates a new HTTP client with KeepAlive disabled. +func NewTestClient() *testHttpClient { + return &testHttpClient{&http.Client{Transport: &http.Transport{DisableKeepAlives: true}}} +} + +// Reads the body from the response and closes it. +func (t *testHttpClient) 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 (t *testHttpClient) ReadBodyJSON(resp *http.Response) map[string]interface{} { + m := make(map[string]interface{}) + b := t.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 (t *testHttpClient) Head(url string) (*http.Response, error) { + return t.send("HEAD", url, "application/json", nil) +} + +func (t *testHttpClient) Get(url string) (*http.Response, error) { + return t.send("GET", url, "application/json", nil) +} + +func (t *testHttpClient) Post(url string, bodyType string, body io.Reader) (*http.Response, error) { + return t.send("POST", url, bodyType, body) +} + +func (t *testHttpClient) PostForm(url string, data url.Values) (*http.Response, error) { + return t.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +func (t *testHttpClient) Put(url string, bodyType string, body io.Reader) (*http.Response, error) { + return t.send("PUT", url, bodyType, body) +} + +func (t *testHttpClient) PutForm(url string, data url.Values) (*http.Response, error) { + return t.Put(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +func (t *testHttpClient) Delete(url string, bodyType string, body io.Reader) (*http.Response, error) { + return t.send("DELETE", url, bodyType, body) +} + +func (t *testHttpClient) DeleteForm(url string, data url.Values) (*http.Response, error) { + return t.Delete(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +func (t *testHttpClient) send(method string, url string, bodyType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", bodyType) + return t.Do(req) +} diff --git a/etcd/v2_http_post.go b/etcd/v2_http_post.go new file mode 100644 index 00000000000..30c1e514b03 --- /dev/null +++ b/etcd/v2_http_post.go @@ -0,0 +1,48 @@ +/* +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 etcd + +import ( + "log" + "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 + } + log.Println("unique:", err) + return err +} diff --git a/etcd/v2_http_put.go b/etcd/v2_http_put.go new file mode 100644 index 00000000000..cb57096c8d6 --- /dev/null +++ b/etcd/v2_http_put.go @@ -0,0 +1,162 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "log" + "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 + } + log.Println("set:", err) + 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 + } + log.Println("create:", err) + 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 + } + log.Println("update:", err) + 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 + } + log.Println("update:", err) + return err +} + +func firstValue(f url.Values, key string) (string, bool) { + l, ok := f[key] + if !ok { + return "", false + } + return l[0], true +} diff --git a/etcd/v2_raft.go b/etcd/v2_raft.go new file mode 100644 index 00000000000..47707d547e0 --- /dev/null +++ b/etcd/v2_raft.go @@ -0,0 +1,70 @@ +/* +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 etcd + +import ( + "encoding/json" + "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 + } + sync := &cmd{Type: "sync", Time: time.Now()} + data, err := json.Marshal(sync) + if err != nil { + panic(err) + } + r.Node.Propose(data) +} + +func (r *v2Raft) StopProposalWaiters() { + for k, ch := range r.result { + ch <- raftStopErr + delete(r.result, k) + } +} diff --git a/etcd/v2_store.go b/etcd/v2_store.go new file mode 100644 index 00000000000..6ba56da049c --- /dev/null +++ b/etcd/v2_store.go @@ -0,0 +1,101 @@ +/* +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 etcd + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/coreos/etcd/store" +) + +type cmd struct { + Type string + Key string + Value string + PrevValue string + PrevIndex uint64 + Dir bool + Recursive bool + Unique bool + Time time.Time +} + +func (p *participant) Set(key string, dir bool, value string, expireTime time.Time) (*store.Event, error) { + set := &cmd{Type: "set", Key: key, Dir: dir, Value: value, Time: 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: "create", Key: key, Dir: dir, Value: value, Time: expireTime, Unique: unique} + return p.do(create) +} + +func (p *participant) Update(key string, value string, expireTime time.Time) (*store.Event, error) { + update := &cmd{Type: "update", Key: key, Value: value, Time: expireTime} + return p.do(update) +} + +func (p *participant) CAS(key, value, prevValue string, prevIndex uint64, expireTime time.Time) (*store.Event, error) { + cas := &cmd{Type: "cas", Key: key, Value: value, PrevValue: prevValue, PrevIndex: prevIndex, Time: expireTime} + return p.do(cas) +} + +func (p *participant) Delete(key string, dir, recursive bool) (*store.Event, error) { + d := &cmd{Type: "delete", 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: "cad", Key: key, PrevValue: prevValue, PrevIndex: prevIndex} + return p.do(cad) +} + +func (p *participant) do(c *cmd) (*store.Event, error) { + data, err := json.Marshal(c) + 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.stopc: + 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") + } +} diff --git a/server/usage.go b/etcd/v2_usage.go similarity index 82% rename from server/usage.go rename to etcd/v2_usage.go index ad3b707ab1e..a916b4ec2a1 100644 --- a/server/usage.go +++ b/etcd/v2_usage.go @@ -1,4 +1,20 @@ -package server +/* +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 etcd import ( "strings" diff --git a/etcd/z_last_test.go b/etcd/z_last_test.go new file mode 100644 index 00000000000..b3addcda892 --- /dev/null +++ b/etcd/z_last_test.go @@ -0,0 +1,94 @@ +// 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 etcd + +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 < 4; 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) +} diff --git a/main.go b/main.go index e2f370d1fc7..56807bcb8ec 100644 --- a/main.go +++ b/main.go @@ -1,44 +1,74 @@ -/* -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 main import ( + "crypto/tls" "fmt" + "log" + "math/rand" + "net" + "net/http" "os" + "time" "github.com/coreos/etcd/config" "github.com/coreos/etcd/etcd" - "github.com/coreos/etcd/server" + ehttp "github.com/coreos/etcd/http" ) func main() { var config = config.New() if err := config.Load(os.Args[1:]); err != nil { - fmt.Println(server.Usage() + "\n") - fmt.Println(err.Error() + "\n") + fmt.Println(etcd.Usage() + "\n") + fmt.Println(err.Error(), "\n") os.Exit(1) } else if config.ShowVersion { - fmt.Println("etcd version", server.ReleaseVersion) + fmt.Println("0.5") os.Exit(0) } else if config.ShowHelp { - fmt.Println(server.Usage() + "\n") os.Exit(0) } - var etcd = etcd.New(config) - etcd.Run() + e := etcd.New(config, genId()) + go e.Run() + + corsInfo, err := ehttp.NewCORSInfo(config.CorsOrigins) + if err != nil { + log.Fatal("cors:", err) + } + + go func() { + serve("raft", config.Peer.BindAddr, config.PeerTLSInfo(), corsInfo, e.RaftHandler()) + }() + serve("etcd", config.BindAddr, config.EtcdTLSInfo(), corsInfo, e) +} + +func genId() int64 { + r := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) + return r.Int63() +} + +func serve(who string, addr string, tinfo *config.TLSInfo, cinfo *ehttp.CORSInfo, handler http.Handler) { + t, terr := tinfo.ServerConfig() + l, err := net.Listen("tcp", addr) + if err != nil { + log.Fatal(err) + } + log.Printf("%v server starts listening on %v\n", who, addr) + + switch tinfo.Scheme() { + case "http": + log.Printf("%v server starts serving HTTP\n", who) + + case "https": + if t == nil { + log.Fatalf("failed to create %v tls: %v\n", who, terr) + } + l = tls.NewListener(l, t) + log.Printf("%v server starts serving HTTPS\n", who) + default: + log.Fatal("unsupported http scheme", tinfo.Scheme()) + } + + h := &ehttp.CORSHandler{handler, cinfo} + log.Fatal(http.Serve(l, h)) } diff --git a/raft/cluster_test.go b/raft/cluster_test.go index 8173b72f210..d3c8c51a680 100644 --- a/raft/cluster_test.go +++ b/raft/cluster_test.go @@ -39,7 +39,7 @@ func TestBuildCluster(t *testing.T) { if tt.ids != nil { w = tt.ids[0] } - if g := n.sm.lead; g != w { + if g := n.sm.lead.Get(); g != w { t.Errorf("#%d.%d: lead = %d, want %d", i, j, g, w) } @@ -124,7 +124,7 @@ func buildCluster(size int, ids []int64) (nt *network, nodes []*Node) { lead := dictate(nodes[0]) lead.Next() for i := 1; i < size; i++ { - lead.Add(ids[i], "") + lead.Add(ids[i], "", nil) nt.send(lead.Msgs()...) for j := 0; j < i; j++ { nodes[j].Next() diff --git a/raft/log.go b/raft/log.go index bab261947f5..9d3fd86bc75 100644 --- a/raft/log.go +++ b/raft/log.go @@ -3,7 +3,7 @@ package raft import "fmt" const ( - Normal int = iota + Normal int64 = iota AddNode RemoveNode @@ -14,8 +14,8 @@ const ( ) type Entry struct { - Type int - Term int + Type int64 + Term int64 Data []byte } @@ -25,13 +25,13 @@ func (e *Entry) isConfig() bool { type log struct { ents []Entry - committed int - applied int - offset int + committed int64 + applied int64 + offset int64 // want a compact after the number of entries exceeds the threshold // TODO(xiangli) size might be a better criteria - compactThreshold int + compactThreshold int64 } func newLog() *log { @@ -43,32 +43,51 @@ func newLog() *log { } } -func (l *log) maybeAppend(index, logTerm, committed int, ents ...Entry) bool { +func (l *log) maybeAppend(index, logTerm, committed int64, ents ...Entry) bool { if l.matchTerm(index, logTerm) { - l.append(index, ents...) - l.committed = committed + from := index + 1 + ci := l.findConflict(from, ents) + switch { + case ci == -1: + case ci <= l.committed: + panic("conflict with committed entry") + default: + l.append(ci-1, ents[ci-from:]...) + } + if l.committed < committed { + l.committed = min(committed, l.lastIndex()) + } return true } return false } -func (l *log) append(after int, ents ...Entry) int { +func (l *log) append(after int64, ents ...Entry) int64 { l.ents = append(l.slice(l.offset, after+1), ents...) return l.lastIndex() } -func (l *log) lastIndex() int { - return len(l.ents) - 1 + l.offset +func (l *log) findConflict(from int64, ents []Entry) int64 { + for i, ne := range ents { + if oe := l.at(from + int64(i)); oe == nil || oe.Term != ne.Term { + return from + int64(i) + } + } + return -1 +} + +func (l *log) lastIndex() int64 { + return int64(len(l.ents)) - 1 + l.offset } -func (l *log) term(i int) int { +func (l *log) term(i int64) int64 { if e := l.at(i); e != nil { return e.Term } return -1 } -func (l *log) entries(i int) []Entry { +func (l *log) entries(i int64) []Entry { // never send out the first entry // first entry is only used for matching // prevLogTerm @@ -78,19 +97,19 @@ func (l *log) entries(i int) []Entry { return l.slice(i, l.lastIndex()+1) } -func (l *log) isUpToDate(i, term int) bool { +func (l *log) isUpToDate(i, term int64) bool { e := l.at(l.lastIndex()) return term > e.Term || (term == e.Term && i >= l.lastIndex()) } -func (l *log) matchTerm(i, term int) bool { +func (l *log) matchTerm(i, term int64) bool { if e := l.at(i); e != nil { return e.Term == term } return false } -func (l *log) maybeCommit(maxIndex, term int) bool { +func (l *log) maybeCommit(maxIndex, term int64) bool { if maxIndex > l.committed && l.term(maxIndex) == term { l.committed = maxIndex return true @@ -112,27 +131,27 @@ func (l *log) nextEnts() (ents []Entry) { // i must be not smaller than the index of the first entry // and not greater than the index of the last entry. // the number of entries after compaction will be returned. -func (l *log) compact(i int) int { +func (l *log) compact(i int64) int64 { if l.isOutOfBounds(i) { panic(fmt.Sprintf("compact %d out of bounds [%d:%d]", i, l.offset, l.lastIndex())) } l.ents = l.slice(i, l.lastIndex()+1) l.offset = i - return len(l.ents) + return int64(len(l.ents)) } func (l *log) shouldCompact() bool { return (l.applied - l.offset) > l.compactThreshold } -func (l *log) restore(index, term int) { +func (l *log) restore(index, term int64) { l.ents = []Entry{{Term: term}} l.committed = index l.applied = index l.offset = index } -func (l *log) at(i int) *Entry { +func (l *log) at(i int64) *Entry { if l.isOutOfBounds(i) { return nil } @@ -140,7 +159,7 @@ func (l *log) at(i int) *Entry { } // slice get a slice of log entries from lo through hi-1, inclusive. -func (l *log) slice(lo int, hi int) []Entry { +func (l *log) slice(lo int64, hi int64) []Entry { if lo >= hi { return nil } @@ -150,9 +169,16 @@ func (l *log) slice(lo int, hi int) []Entry { return l.ents[lo-l.offset : hi-l.offset] } -func (l *log) isOutOfBounds(i int) bool { +func (l *log) isOutOfBounds(i int64) bool { if i < l.offset || i > l.lastIndex() { return true } return false } + +func min(a, b int64) int64 { + if a > b { + return b + } + return a +} diff --git a/raft/log_test.go b/raft/log_test.go index ed46f16c597..fe507124edf 100644 --- a/raft/log_test.go +++ b/raft/log_test.go @@ -5,14 +5,69 @@ import ( "testing" ) +// TestAppend ensures: +// 1. If an existing entry conflicts with a new one (same index +// but different terms), delete the existing entry and all that +// follow it +// 2.Append any new entries not already in the log +func TestAppend(t *testing.T) { + previousEnts := []Entry{{Term: 1}, {Term: 2}} + tests := []struct { + after int64 + ents []Entry + windex int64 + wents []Entry + }{ + { + 2, + []Entry{}, + 2, + []Entry{{Term: 1}, {Term: 2}}, + }, + { + 2, + []Entry{{Term: 2}}, + 3, + []Entry{{Term: 1}, {Term: 2}, {Term: 2}}, + }, + // conflicts with index 1 + { + 0, + []Entry{{Term: 2}}, + 1, + []Entry{{Term: 2}}, + }, + // conflicts with index 2 + { + 1, + []Entry{{Term: 3}, {Term: 3}}, + 3, + []Entry{{Term: 1}, {Term: 3}, {Term: 3}}, + }, + } + + for i, tt := range tests { + log := newLog() + log.ents = append(log.ents, previousEnts...) + index := log.append(tt.after, tt.ents...) + if index != tt.windex { + t.Errorf("#%d: lastIndex = %d, want %d", i, index, tt.windex) + } + if g := log.entries(1); !reflect.DeepEqual(g, tt.wents) { + t.Errorf("#%d: logEnts = %+v, want %+v", i, g, tt.wents) + } + } +} + // TestCompactionSideEffects ensures that all the log related funcationality works correctly after // a compaction. func TestCompactionSideEffects(t *testing.T) { - lastIndex := 1000 + var i int64 + lastIndex := int64(1000) log := newLog() - for i := 0; i < lastIndex; i++ { - log.append(i, Entry{Term: i + 1}) + for i = 0; i < lastIndex; i++ { + log.append(int64(i), Entry{Term: int64(i + 1)}) } log.compact(500) @@ -49,15 +104,15 @@ func TestCompactionSideEffects(t *testing.T) { func TestCompaction(t *testing.T) { tests := []struct { app int - compact []int + compact []int64 wleft []int wallow bool }{ // out of upper bound - {1000, []int{1001}, []int{-1}, false}, - {1000, []int{300, 500, 800, 900}, []int{701, 501, 201, 101}, true}, + {1000, []int64{1001}, []int{-1}, false}, + {1000, []int64{300, 500, 800, 900}, []int{701, 501, 201, 101}, true}, // out of lower bound - {1000, []int{300, 299}, []int{701, -1}, false}, + {1000, []int64{300, 299}, []int{701, -1}, false}, } for i, tt := range tests { @@ -72,7 +127,7 @@ func TestCompaction(t *testing.T) { log := newLog() for i := 0; i < tt.app; i++ { - log.append(i, Entry{}) + log.append(int64(i), Entry{}) } for j := 0; j < len(tt.compact); j++ { @@ -86,13 +141,14 @@ func TestCompaction(t *testing.T) { } func TestLogRestore(t *testing.T) { + var i int64 log := newLog() - for i := 0; i < 100; i++ { + for i = 0; i < 100; i++ { log.append(i, Entry{Term: i + 1}) } - index := 1000 - term := 1000 + index := int64(1000) + term := int64(1000) log.restore(index, term) // only has the guard entry @@ -114,12 +170,12 @@ func TestLogRestore(t *testing.T) { } func TestIsOutOfBounds(t *testing.T) { - offset := 100 - num := 100 + offset := int64(100) + num := int64(100) l := &log{offset: offset, ents: make([]Entry, num)} tests := []struct { - index int + index int64 w bool }{ {offset - 1, true}, @@ -138,16 +194,17 @@ func TestIsOutOfBounds(t *testing.T) { } func TestAt(t *testing.T) { - offset := 100 - num := 100 + var i int64 + offset := int64(100) + num := int64(100) l := &log{offset: offset} - for i := 0; i < num; i++ { + for i = 0; i < num; i++ { l.ents = append(l.ents, Entry{Term: i}) } tests := []struct { - index int + index int64 w *Entry }{ {offset - 1, nil}, @@ -166,17 +223,18 @@ func TestAt(t *testing.T) { } func TestSlice(t *testing.T) { - offset := 100 - num := 100 + var i int64 + offset := int64(100) + num := int64(100) l := &log{offset: offset} - for i := 0; i < num; i++ { + for i = 0; i < num; i++ { l.ents = append(l.ents, Entry{Term: i}) } tests := []struct { - from int - to int + from int64 + to int64 w []Entry }{ {offset - 1, offset + 1, nil}, diff --git a/raft/node.go b/raft/node.go index f063d78a179..b762a94a8c5 100644 --- a/raft/node.go +++ b/raft/node.go @@ -3,6 +3,9 @@ package raft import ( "encoding/json" golog "log" + "math/rand" + "sync/atomic" + "time" ) type Interface interface { @@ -10,19 +13,25 @@ type Interface interface { Msgs() []Message } -type tick int +type tick int64 type Config struct { - NodeId int64 - Addr string + NodeId int64 + Addr string + Context []byte } type Node struct { sm *stateMachine - elapsed tick - election tick - heartbeat tick + elapsed tick + electionRand tick + election tick + heartbeat tick + + // TODO: it needs garbage collection later + rmNodes map[int64]struct{} + removed bool } func New(id int64, heartbeat, election tick) *Node { @@ -30,35 +39,65 @@ func New(id int64, heartbeat, election tick) *Node { panic("election is least three times as heartbeat [election: %d, heartbeat: %d]") } + rand.Seed(time.Now().UnixNano()) n := &Node{ - heartbeat: heartbeat, - election: election, - sm: newStateMachine(id, []int64{id}), + heartbeat: heartbeat, + election: election, + electionRand: election + tick(rand.Int31())%election, + sm: newStateMachine(id, []int64{id}), + rmNodes: make(map[int64]struct{}), } return n } -func (n *Node) Id() int64 { return n.sm.id } +func (n *Node) Id() int64 { + return atomic.LoadInt64(&n.sm.id) +} + +func (n *Node) Index() int64 { return n.sm.index.Get() } + +func (n *Node) Term() int64 { return n.sm.term.Get() } + +func (n *Node) Applied() int64 { return n.sm.log.applied } + +func (n *Node) HasLeader() bool { return n.Leader() != none } + +func (n *Node) IsLeader() bool { return n.Leader() == n.Id() } + +func (n *Node) Leader() int64 { return n.sm.lead.Get() } -func (n *Node) HasLeader() bool { return n.sm.lead != none } +func (n *Node) IsRemoved() bool { return n.removed } // Propose asynchronously proposes data be applied to the underlying state machine. func (n *Node) Propose(data []byte) { n.propose(Normal, data) } -func (n *Node) propose(t int, data []byte) { +func (n *Node) propose(t int64, data []byte) { n.Step(Message{Type: msgProp, Entries: []Entry{{Type: t, Data: data}}}) } func (n *Node) Campaign() { n.Step(Message{Type: msgHup}) } -func (n *Node) Add(id int64, addr string) { n.updateConf(AddNode, &Config{NodeId: id, Addr: addr}) } +func (n *Node) Add(id int64, addr string, context []byte) { + n.UpdateConf(AddNode, &Config{NodeId: id, Addr: addr, Context: context}) +} -func (n *Node) Remove(id int64) { n.updateConf(RemoveNode, &Config{NodeId: id}) } +func (n *Node) Remove(id int64) { n.UpdateConf(RemoveNode, &Config{NodeId: id}) } func (n *Node) Msgs() []Message { return n.sm.Msgs() } func (n *Node) Step(m Message) bool { + if m.Type == msgDenied { + n.removed = true + return false + } + if m.Term != 0 { + if _, ok := n.rmNodes[m.From]; ok { + n.sm.send(Message{To: m.From, Type: msgDenied}) + return true + } + } + l := len(n.sm.msgs) if !n.sm.Step(m) { return false @@ -91,6 +130,7 @@ func (n *Node) Next() []Entry { continue } n.sm.addNode(c.NodeId) + delete(n.rmNodes, c.NodeId) case RemoveNode: c := new(Config) if err := json.Unmarshal(ents[i].Data, c); err != nil { @@ -98,6 +138,10 @@ func (n *Node) Next() []Entry { continue } n.sm.removeNode(c.NodeId) + n.rmNodes[c.NodeId] = struct{}{} + if c.NodeId == n.sm.id { + n.removed = true + } default: panic("unexpected entry type") } @@ -113,19 +157,22 @@ func (n *Node) Tick() { return } - timeout, msgType := n.election, msgHup + timeout, msgType := n.electionRand, msgHup if n.sm.state == stateLeader { timeout, msgType = n.heartbeat, msgBeat } if n.elapsed >= timeout { n.Step(Message{Type: msgType}) n.elapsed = 0 + if n.sm.state != stateLeader { + n.electionRand = n.election + tick(rand.Int31())%n.election + } } else { n.elapsed++ } } -func (n *Node) updateConf(t int, c *Config) { +func (n *Node) UpdateConf(t int64, c *Config) { data, err := json.Marshal(c) if err != nil { panic(err) diff --git a/raft/node_test.go b/raft/node_test.go index f3f7fe21240..1992ff2a389 100644 --- a/raft/node_test.go +++ b/raft/node_test.go @@ -1,6 +1,7 @@ package raft import ( + "reflect" "testing" ) @@ -15,7 +16,7 @@ func TestTickMsgHup(t *testing.T) { // simulate to patch the join log n.Step(Message{Type: msgApp, Commit: 1, Entries: []Entry{Entry{}}}) - for i := 0; i < defaultElection+1; i++ { + for i := 0; i < defaultElection*2; i++ { n.Tick() } @@ -36,10 +37,10 @@ func TestTickMsgBeat(t *testing.T) { n := dictate(New(0, defaultHeartbeat, defaultElection)) n.Next() for i := 1; i < k; i++ { - n.Add(int64(i), "") + n.Add(int64(i), "", nil) for _, m := range n.Msgs() { if m.Type == msgApp { - n.Step(Message{From: m.To, Type: msgAppResp, Index: m.Index + len(m.Entries)}) + n.Step(Message{From: m.To, Type: msgAppResp, Index: m.Index + int64(len(m.Entries))}) } } // ignore commit index update messages @@ -72,13 +73,14 @@ func TestResetElapse(t *testing.T) { }{ {Message{From: 0, To: 1, Type: msgApp, Term: 2, Entries: []Entry{{Term: 1}}}, 0}, {Message{From: 0, To: 1, Type: msgApp, Term: 1, Entries: []Entry{{Term: 1}}}, 1}, - {Message{From: 0, To: 1, Type: msgVote, Term: 2}, 0}, + {Message{From: 0, To: 1, Type: msgVote, Term: 2, Index: 1, LogTerm: 1}, 0}, {Message{From: 0, To: 1, Type: msgVote, Term: 1}, 1}, } for i, tt := range tests { n := New(0, defaultHeartbeat, defaultElection) n.sm = newStateMachine(0, []int64{0, 1, 2}) + n.sm.log.append(0, Entry{Type: Normal, Term: 1}) n.sm.term = 2 n.sm.log.committed = 1 @@ -112,7 +114,7 @@ func TestStartCluster(t *testing.T) { func TestAdd(t *testing.T) { n := dictate(New(0, defaultHeartbeat, defaultElection)) n.Next() - n.Add(1, "") + n.Add(1, "", nil) n.Next() if len(n.sm.ins) != 2 { @@ -126,7 +128,7 @@ func TestAdd(t *testing.T) { func TestRemove(t *testing.T) { n := dictate(New(0, defaultHeartbeat, defaultElection)) n.Next() - n.Add(1, "") + n.Add(1, "", nil) n.Next() n.Remove(0) n.Step(Message{Type: msgAppResp, From: 1, Term: 1, Index: 4}) @@ -140,8 +142,54 @@ func TestRemove(t *testing.T) { } } +func TestDenial(t *testing.T) { + logents := []Entry{ + {Type: AddNode, Term: 1, Data: []byte(`{"NodeId":1}`)}, + {Type: AddNode, Term: 1, Data: []byte(`{"NodeId":2}`)}, + {Type: RemoveNode, Term: 1, Data: []byte(`{"NodeId":2}`)}, + } + + tests := []struct { + ent Entry + wdenied map[int64]bool + }{ + { + Entry{Type: AddNode, Term: 1, Data: []byte(`{"NodeId":2}`)}, + map[int64]bool{0: false, 1: false, 2: false}, + }, + { + Entry{Type: RemoveNode, Term: 1, Data: []byte(`{"NodeId":1}`)}, + map[int64]bool{0: false, 1: true, 2: true}, + }, + { + Entry{Type: RemoveNode, Term: 1, Data: []byte(`{"NodeId":0}`)}, + map[int64]bool{0: true, 1: false, 2: true}, + }, + } + + for i, tt := range tests { + n := dictate(New(0, defaultHeartbeat, defaultElection)) + n.Next() + n.Msgs() + n.sm.log.append(n.sm.log.committed, append(logents, tt.ent)...) + n.sm.log.committed += int64(len(logents) + 1) + n.Next() + + for id, denied := range tt.wdenied { + n.Step(Message{From: id, To: 0, Type: msgApp, Term: 1}) + w := []Message{} + if denied { + w = []Message{{From: 0, To: id, Term: 1, Type: msgDenied}} + } + if g := n.Msgs(); !reflect.DeepEqual(g, w) { + t.Errorf("#%d: msgs for %d = %+v, want %+v", i, id, g, w) + } + } + } +} + func dictate(n *Node) *Node { n.Step(Message{Type: msgHup}) - n.Add(n.Id(), "") + n.Add(n.Id(), "", nil) return n } diff --git a/raft/raft.go b/raft/raft.go index cc8c45b1839..1c9944b5115 100644 --- a/raft/raft.go +++ b/raft/raft.go @@ -3,11 +3,12 @@ package raft import ( "errors" "sort" + "sync/atomic" ) const none = -1 -type messageType int +type messageType int64 const ( msgHup messageType = iota @@ -18,6 +19,7 @@ const ( msgVote msgVoteResp msgSnap + msgDenied ) var mtmap = [...]string{ @@ -29,10 +31,11 @@ var mtmap = [...]string{ msgVote: "msgVote", msgVoteResp: "msgVoteResp", msgSnap: "msgSnap", + msgDenied: "msgDenied", } func (mt messageType) String() string { - return mtmap[int(mt)] + return mtmap[int64(mt)] } var errNoLeader = errors.New("no leader") @@ -43,7 +46,7 @@ const ( stateLeader ) -type stateType int +type stateType int64 var stmap = [...]string{ stateFollower: "stateFollower", @@ -58,27 +61,27 @@ var stepmap = [...]stepFunc{ } func (st stateType) String() string { - return stmap[int(st)] + return stmap[int64(st)] } type Message struct { Type messageType To int64 From int64 - Term int - LogTerm int - Index int - PrevTerm int + Term int64 + LogTerm int64 + Index int64 + PrevTerm int64 Entries []Entry - Commit int + Commit int64 Snapshot Snapshot } type index struct { - match, next int + match, next int64 } -func (in *index) update(n int) { +func (in *index) update(n int64) { in.match = n in.next = n + 1 } @@ -89,11 +92,30 @@ func (in *index) decr() { } } +// An AtomicInt is an int64 to be accessed atomically. +type atomicInt int64 + +func (i *atomicInt) Set(n int64) { + atomic.StoreInt64((*int64)(i), n) +} + +func (i *atomicInt) Get() int64 { + return atomic.LoadInt64((*int64)(i)) +} + +// int64Slice implements sort interface +type int64Slice []int64 + +func (p int64Slice) Len() int { return len(p) } +func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] } +func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + type stateMachine struct { id int64 // the term we are participating in at any time - term int + term atomicInt + index atomicInt // who we voted for in term vote int64 @@ -110,7 +132,7 @@ type stateMachine struct { msgs []Message // the leader id - lead int64 + lead atomicInt // pending reconfiguration pendingConf bool @@ -119,7 +141,7 @@ type stateMachine struct { } func newStateMachine(id int64, peers []int64) *stateMachine { - sm := &stateMachine{id: id, log: newLog(), ins: make(map[int64]*index)} + sm := &stateMachine{id: id, lead: none, log: newLog(), ins: make(map[int64]*index)} for _, p := range peers { sm.ins[p] = &index{} } @@ -146,7 +168,7 @@ func (sm *stateMachine) poll(id int64, v bool) (granted int) { // send persists state to stable storage and then sends to its mailbox. func (sm *stateMachine) send(m Message) { m.From = sm.id - m.Term = sm.term + m.Term = sm.term.Get() sm.msgs = append(sm.msgs, m) } @@ -180,14 +202,14 @@ func (sm *stateMachine) bcastAppend() { func (sm *stateMachine) maybeCommit() bool { // TODO(bmizerany): optimize.. Currently naive - mis := make([]int, 0, len(sm.ins)) + mis := make(int64Slice, 0, len(sm.ins)) for i := range sm.ins { mis = append(mis, sm.ins[i].match) } - sort.Sort(sort.Reverse(sort.IntSlice(mis))) + sort.Sort(sort.Reverse(mis)) mci := mis[sm.q()-1] - return sm.log.maybeCommit(mci, sm.term) + return sm.log.maybeCommit(mci, sm.term.Get()) } // nextEnts returns the appliable entries and updates the applied index @@ -195,9 +217,9 @@ func (sm *stateMachine) nextEnts() (ents []Entry) { return sm.log.nextEnts() } -func (sm *stateMachine) reset(term int) { - sm.term = term - sm.lead = none +func (sm *stateMachine) reset(term int64) { + sm.term.Set(term) + sm.lead.Set(none) sm.vote = none sm.votes = make(map[int64]bool) for i := range sm.ins { @@ -213,8 +235,8 @@ func (sm *stateMachine) q() int { } func (sm *stateMachine) appendEntry(e Entry) { - e.Term = sm.term - sm.log.append(sm.log.lastIndex(), e) + e.Term = sm.term.Get() + sm.index.Set(sm.log.append(sm.log.lastIndex(), e)) sm.ins[sm.id].update(sm.log.lastIndex()) sm.maybeCommit() } @@ -226,9 +248,9 @@ func (sm *stateMachine) promotable() bool { return sm.log.committed != 0 } -func (sm *stateMachine) becomeFollower(term int, lead int64) { +func (sm *stateMachine) becomeFollower(term int64, lead int64) { sm.reset(term) - sm.lead = lead + sm.lead.Set(lead) sm.state = stateFollower sm.pendingConf = false } @@ -238,7 +260,7 @@ func (sm *stateMachine) becomeCandidate() { if sm.state == stateLeader { panic("invalid transition [leader -> candidate]") } - sm.reset(sm.term + 1) + sm.reset(sm.term.Get() + 1) sm.vote = sm.id sm.state = stateCandidate } @@ -248,8 +270,8 @@ func (sm *stateMachine) becomeLeader() { if sm.state == stateFollower { panic("invalid transition [follower -> leader]") } - sm.reset(sm.term) - sm.lead = sm.id + sm.reset(sm.term.Get()) + sm.lead.Set(sm.id) sm.state = stateLeader for _, e := range sm.log.entries(sm.log.committed + 1) { @@ -288,9 +310,13 @@ func (sm *stateMachine) Step(m Message) (ok bool) { switch { case m.Term == 0: // local message - case m.Term > sm.term: - sm.becomeFollower(m.Term, m.From) - case m.Term < sm.term: + case m.Term > sm.term.Get(): + lead := m.From + if m.Type == msgVote { + lead = none + } + sm.becomeFollower(m.Term, lead) + case m.Term < sm.term.Get(): // ignore return true } @@ -300,6 +326,7 @@ func (sm *stateMachine) Step(m Message) (ok bool) { func (sm *stateMachine) handleAppendEntries(m Message) { if sm.log.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...) { + sm.index.Set(sm.log.lastIndex()) sm.send(Message{To: m.From, Type: msgAppResp, Index: sm.log.lastIndex()}) } else { sm.send(Message{To: m.From, Type: msgAppResp, Index: -1}) @@ -361,7 +388,7 @@ func stepCandidate(sm *stateMachine, m Message) bool { case msgProp: return false case msgApp: - sm.becomeFollower(sm.term, m.From) + sm.becomeFollower(sm.term.Get(), m.From) sm.handleAppendEntries(m) case msgSnap: sm.becomeFollower(m.Term, m.From) @@ -375,7 +402,7 @@ func stepCandidate(sm *stateMachine, m Message) bool { sm.becomeLeader() sm.bcastAppend() case len(sm.votes) - gr: - sm.becomeFollower(sm.term, none) + sm.becomeFollower(sm.term.Get(), none) } } return true @@ -384,12 +411,13 @@ func stepCandidate(sm *stateMachine, m Message) bool { func stepFollower(sm *stateMachine, m Message) bool { switch m.Type { case msgProp: - if sm.lead == none { + if sm.lead.Get() == none { return false } - m.To = sm.lead + m.To = sm.lead.Get() sm.send(m) case msgApp: + sm.lead.Set(m.From) sm.handleAppendEntries(m) case msgSnap: sm.handleSnapshot(m) @@ -424,6 +452,7 @@ func (sm *stateMachine) restore(s Snapshot) { } sm.log.restore(s.Index, s.Term) + sm.index.Set(sm.log.lastIndex()) sm.ins = make(map[int64]*index) for _, n := range s.Nodes { sm.ins[n] = &index{next: sm.log.lastIndex() + 1} @@ -435,7 +464,7 @@ func (sm *stateMachine) restore(s Snapshot) { sm.snapshoter.Restore(s) } -func (sm *stateMachine) needSnapshot(i int) bool { +func (sm *stateMachine) needSnapshot(i int64) bool { if i < sm.log.offset { if sm.snapshoter == nil { panic("need snapshot but snapshoter is nil") diff --git a/raft/raft_test.go b/raft/raft_test.go index aeaa4b629f9..248501ff8d2 100644 --- a/raft/raft_test.go +++ b/raft/raft_test.go @@ -33,7 +33,7 @@ func TestLeaderElection(t *testing.T) { if sm.state != tt.state { t.Errorf("#%d: state = %s, want %s", i, sm.state, tt.state) } - if g := sm.term; g != 1 { + if g := sm.term.Get(); g != 1 { t.Errorf("#%d: term = %d, want %d", i, g, 1) } } @@ -43,7 +43,7 @@ func TestLogReplication(t *testing.T) { tests := []struct { *network msgs []Message - wcommitted int + wcommitted int64 }{ { newNetwork(nil, nil, nil), @@ -214,7 +214,7 @@ func TestDuelingCandidates(t *testing.T) { tests := []struct { sm *stateMachine state stateType - term int + term int64 log *log }{ {a, stateFollower, 2, wlog}, @@ -226,7 +226,7 @@ func TestDuelingCandidates(t *testing.T) { if g := tt.sm.state; g != tt.state { t.Errorf("#%d: state = %s, want %s", i, g, tt.state) } - if g := tt.sm.term; g != tt.term { + if g := tt.sm.term.Get(); g != tt.term { t.Errorf("#%d: term = %d, want %d", i, g, tt.term) } base := ltoa(tt.log) @@ -365,7 +365,7 @@ func TestProposal(t *testing.T) { } } sm := tt.network.peers[0].(*stateMachine) - if g := sm.term; g != 1 { + if g := sm.term.Get(); g != 1 { t.Errorf("#%d: term = %d, want %d", i, g, 1) } } @@ -398,7 +398,7 @@ func TestProposalByProxy(t *testing.T) { } } sm := tt.peers[0].(*stateMachine) - if g := sm.term; g != 1 { + if g := sm.term.Get(); g != 1 { t.Errorf("#%d: term = %d, want %d", i, g, 1) } } @@ -406,30 +406,30 @@ func TestProposalByProxy(t *testing.T) { func TestCommit(t *testing.T) { tests := []struct { - matches []int + matches []int64 logs []Entry - smTerm int - w int + smTerm int64 + w int64 }{ // single - {[]int{1}, []Entry{{}, {Term: 1}}, 1, 1}, - {[]int{1}, []Entry{{}, {Term: 1}}, 2, 0}, - {[]int{2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, - {[]int{1}, []Entry{{}, {Term: 2}}, 2, 1}, + {[]int64{1}, []Entry{{}, {Term: 1}}, 1, 1}, + {[]int64{1}, []Entry{{}, {Term: 1}}, 2, 0}, + {[]int64{2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, + {[]int64{1}, []Entry{{}, {Term: 2}}, 2, 1}, // odd - {[]int{2, 1, 1}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, - {[]int{2, 1, 1}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, - {[]int{2, 1, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, - {[]int{2, 1, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, + {[]int64{2, 1, 1}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, + {[]int64{2, 1, 1}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, + {[]int64{2, 1, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, + {[]int64{2, 1, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, // even - {[]int{2, 1, 1, 1}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, - {[]int{2, 1, 1, 1}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, - {[]int{2, 1, 1, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, - {[]int{2, 1, 1, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, - {[]int{2, 1, 2, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, - {[]int{2, 1, 2, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, + {[]int64{2, 1, 1, 1}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, + {[]int64{2, 1, 1, 1}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, + {[]int64{2, 1, 1, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 1, 1}, + {[]int64{2, 1, 1, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, + {[]int64{2, 1, 2, 2}, []Entry{{}, {Term: 1}, {Term: 2}}, 2, 2}, + {[]int64{2, 1, 2, 2}, []Entry{{}, {Term: 1}, {Term: 1}}, 2, 0}, } for i, tt := range tests { @@ -437,7 +437,7 @@ func TestCommit(t *testing.T) { for j := 0; j < len(tt.matches); j++ { ins[int64(j)] = &index{tt.matches[j], tt.matches[j] + 1} } - sm := &stateMachine{log: &log{ents: tt.logs}, ins: ins, term: tt.smTerm} + sm := &stateMachine{log: &log{ents: tt.logs}, ins: ins, term: atomicInt(tt.smTerm)} sm.maybeCommit() if g := sm.log.committed; g != tt.w { t.Errorf("#%d: committed = %d, want %d", i, g, tt.w) @@ -445,12 +445,68 @@ func TestCommit(t *testing.T) { } } +// TestHandleMsgApp ensures: +// 1. Reply false if log doesn’t contain an entry at prevLogIndex whose term matches prevLogTerm. +// 2. If an existing entry conflicts with a new one (same index but different terms), +// delete the existing entry and all that follow it; append any new entries not already in the log. +// 3. If leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry). +func TestHandleMsgApp(t *testing.T) { + tests := []struct { + m Message + wIndex int64 + wCommit int64 + wAccept bool + }{ + // Ensure 1 + {Message{Type: msgApp, Term: 2, LogTerm: 3, Index: 2, Commit: 3}, 2, 0, false}, // previous log mismatch + {Message{Type: msgApp, Term: 2, LogTerm: 3, Index: 3, Commit: 3}, 2, 0, false}, // previous log non-exist + + // Ensure 2 + {Message{Type: msgApp, Term: 2, LogTerm: 1, Index: 1, Commit: 1}, 2, 1, true}, + {Message{Type: msgApp, Term: 2, LogTerm: 0, Index: 0, Commit: 1, Entries: []Entry{{Term: 2}}}, 1, 1, true}, + {Message{Type: msgApp, Term: 2, LogTerm: 2, Index: 2, Commit: 3, Entries: []Entry{{Term: 2}, {Term: 2}}}, 4, 3, true}, + {Message{Type: msgApp, Term: 2, LogTerm: 2, Index: 2, Commit: 4, Entries: []Entry{{Term: 2}}}, 3, 3, true}, + {Message{Type: msgApp, Term: 2, LogTerm: 1, Index: 1, Commit: 4, Entries: []Entry{{Term: 2}}}, 2, 2, true}, + + // Ensure 3 + {Message{Type: msgApp, Term: 2, LogTerm: 2, Index: 2, Commit: 2}, 2, 2, true}, + {Message{Type: msgApp, Term: 2, LogTerm: 2, Index: 2, Commit: 4}, 2, 2, true}, // commit upto min(commit, last) + } + + for i, tt := range tests { + sm := &stateMachine{ + state: stateFollower, + term: 2, + log: &log{committed: 0, ents: []Entry{{}, {Term: 1}, {Term: 2}}}, + } + + sm.handleAppendEntries(tt.m) + if sm.log.lastIndex() != tt.wIndex { + t.Errorf("#%d: lastIndex = %d, want %d", i, sm.log.lastIndex(), tt.wIndex) + } + if sm.log.committed != tt.wCommit { + t.Errorf("#%d: committed = %d, want %d", i, sm.log.committed, tt.wCommit) + } + m := sm.Msgs() + if len(m) != 1 { + t.Errorf("#%d: msg = nil, want 1") + } + gaccept := true + if m[0].Index == -1 { + gaccept = false + } + if gaccept != tt.wAccept { + t.Errorf("#%d: accept = %v, want %v", gaccept, tt.wAccept) + } + } +} + func TestRecvMsgVote(t *testing.T) { tests := []struct { state stateType - i, term int + i, term int64 voteFor int64 - w int + w int64 }{ {stateFollower, 0, 0, none, -1}, {stateFollower, 0, 1, none, -1}, @@ -504,7 +560,7 @@ func TestStateTransition(t *testing.T) { from stateType to stateType wallow bool - wterm int + wterm int64 wlead int64 }{ {stateFollower, stateFollower, true, 1, none}, @@ -542,10 +598,10 @@ func TestStateTransition(t *testing.T) { sm.becomeLeader() } - if sm.term != tt.wterm { - t.Errorf("%d: term = %d, want %d", i, sm.term, tt.wterm) + if sm.term.Get() != tt.wterm { + t.Errorf("%d: term = %d, want %d", i, sm.term.Get(), tt.wterm) } - if sm.lead != tt.wlead { + if sm.lead.Get() != tt.wlead { t.Errorf("%d: lead = %d, want %d", i, sm.lead, tt.wlead) } }() @@ -579,7 +635,7 @@ func TestConf(t *testing.T) { // the uncommitted log entries func TestConfChangeLeader(t *testing.T) { tests := []struct { - et int + et int64 wPending bool }{ {Normal, false}, @@ -605,8 +661,8 @@ func TestAllServerStepdown(t *testing.T) { state stateType wstate stateType - wterm int - windex int + wterm int64 + windex int64 }{ {stateFollower, stateFollower, 3, 1}, {stateCandidate, stateFollower, 3, 1}, @@ -614,7 +670,7 @@ func TestAllServerStepdown(t *testing.T) { } tmsgTypes := [...]messageType{msgVote, msgApp} - tterm := 3 + tterm := int64(3) for i, tt := range tests { sm := newStateMachine(0, []int64{0, 1, 2}) @@ -629,27 +685,34 @@ func TestAllServerStepdown(t *testing.T) { } for j, msgType := range tmsgTypes { - sm.Step(Message{Type: msgType, Term: tterm, LogTerm: tterm}) + sm.Step(Message{From: 1, Type: msgType, Term: tterm, LogTerm: tterm}) if sm.state != tt.wstate { t.Errorf("#%d.%d state = %v , want %v", i, j, sm.state, tt.wstate) } - if sm.term != tt.wterm { - t.Errorf("#%d.%d term = %v , want %v", i, j, sm.term, tt.wterm) + if sm.term.Get() != tt.wterm { + t.Errorf("#%d.%d term = %v , want %v", i, j, sm.term.Get(), tt.wterm) } - if len(sm.log.ents) != tt.windex { + if int64(len(sm.log.ents)) != tt.windex { t.Errorf("#%d.%d index = %v , want %v", i, j, len(sm.log.ents), tt.windex) } + wlead := int64(1) + if msgType == msgVote { + wlead = none + } + if sm.lead.Get() != wlead { + t.Errorf("#%d, sm.lead = %d, want %d", i, sm.lead.Get(), none) + } } } } func TestLeaderAppResp(t *testing.T) { tests := []struct { - index int + index int64 wmsgNum int - windex int - wcommitted int + windex int64 + wcommitted int64 }{ {-1, 1, 1, 0}, // bad resp; leader does not commit; reply with log entries {2, 2, 2, 2}, // good resp; leader commits; broadcast with commit index @@ -663,7 +726,7 @@ func TestLeaderAppResp(t *testing.T) { sm.becomeCandidate() sm.becomeLeader() sm.Msgs() - sm.Step(Message{From: 1, Type: msgAppResp, Index: tt.index, Term: sm.term}) + sm.Step(Message{From: 1, Type: msgAppResp, Index: tt.index, Term: sm.term.Get()}) msgs := sm.Msgs() if len(msgs) != tt.wmsgNum { @@ -695,7 +758,7 @@ func TestRecvMsgBeat(t *testing.T) { for i, tt := range tests { sm := newStateMachine(0, []int64{0, 1, 2}) sm.log = &log{ents: []Entry{{}, {Term: 0}, {Term: 1}}} - sm.term = 1 + sm.term.Set(1) sm.state = tt.state sm.Step(Message{Type: msgBeat}) @@ -714,7 +777,7 @@ func TestRecvMsgBeat(t *testing.T) { func TestMaybeCompact(t *testing.T) { tests := []struct { snapshoter Snapshoter - applied int + applied int64 wCompact bool }{ {nil, defaultCompactThreshold + 1, false}, @@ -726,7 +789,7 @@ func TestMaybeCompact(t *testing.T) { sm := newStateMachine(0, []int64{0, 1, 2}) sm.setSnapshoter(tt.snapshoter) for i := 0; i < defaultCompactThreshold*2; i++ { - sm.log.append(i, Entry{Term: i + 1}) + sm.log.append(int64(i), Entry{Term: int64(i + 1)}) } sm.log.applied = tt.applied sm.log.committed = tt.applied @@ -891,7 +954,7 @@ func TestSlowNodeRestore(t *testing.T) { } } -func ents(terms ...int) *stateMachine { +func ents(terms ...int64) *stateMachine { ents := []Entry{{}} for _, term := range terms { ents = append(ents, Entry{Term: term}) @@ -1022,7 +1085,7 @@ type logSnapshoter struct { snapshot Snapshot } -func (s *logSnapshoter) Snap(index, term int, nodes []int64) { +func (s *logSnapshoter) Snap(index, term int64, nodes []int64) { s.snapshot = Snapshot{ Index: index, Term: term, @@ -1036,10 +1099,3 @@ func (s *logSnapshoter) Restore(ss Snapshot) { func (s *logSnapshoter) GetSnap() Snapshot { return s.snapshot } - -// int64Slice implements sort interface -type int64Slice []int64 - -func (p int64Slice) Len() int { return len(p) } -func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] } -func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/raft/snapshot.go b/raft/snapshot.go index ab2ca65eb0a..4ba3c0f9287 100644 --- a/raft/snapshot.go +++ b/raft/snapshot.go @@ -6,15 +6,15 @@ type Snapshot struct { // the configuration Nodes []int64 // the index at which the snapshot was taken. - Index int + Index int64 // the log term of the index - Term int + Term int64 } // A snapshoter can make a snapshot of its current state atomically. // It can restore from a snapshot and get the latest snapshot it took. type Snapshoter interface { - Snap(index, term int, nodes []int64) + Snap(index, term int64, nodes []int64) Restore(snap Snapshot) GetSnap() Snapshot } diff --git a/server/server.go b/server/server.go index a7aebc31f37..134b656ec61 100644 --- a/server/server.go +++ b/server/server.go @@ -17,7 +17,6 @@ import ( "github.com/coreos/etcd/metrics" "github.com/coreos/etcd/mod" uhttp "github.com/coreos/etcd/pkg/http" - "github.com/coreos/etcd/server/v1" "github.com/coreos/etcd/server/v2" "github.com/coreos/etcd/store" _ "github.com/coreos/etcd/store/v2" @@ -107,19 +106,6 @@ func (s *Server) SetStore(store store.Store) { s.store = store } -func (s *Server) installV1(r *mux.Router) { - s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.GetKeyHandler).Methods("GET", "HEAD") - s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.SetKeyHandler).Methods("POST", "PUT") - s.handleFuncV1(r, "/v1/keys/{key:.*}", v1.DeleteKeyHandler).Methods("DELETE") - s.handleFuncV1(r, "/v1/watch/{key:.*}", v1.WatchKeyHandler).Methods("GET", "HEAD", "POST") - s.handleFunc(r, "/v1/leader", s.GetLeaderHandler).Methods("GET", "HEAD") - s.handleFunc(r, "/v1/machines", s.GetPeersHandler).Methods("GET", "HEAD") - s.handleFunc(r, "/v1/peers", s.GetPeersHandler).Methods("GET", "HEAD") - s.handleFunc(r, "/v1/stats/self", s.GetStatsHandler).Methods("GET", "HEAD") - s.handleFunc(r, "/v1/stats/leader", s.GetLeaderStatsHandler).Methods("GET", "HEAD") - s.handleFunc(r, "/v1/stats/store", s.GetStoreStatsHandler).Methods("GET", "HEAD") -} - func (s *Server) installV2(r *mux.Router) { r2 := mux.NewRouter() r.PathPrefix("/v2").Handler(ehttp.NewLowerQueryParamsHandler(r2)) @@ -150,13 +136,6 @@ func (s *Server) installDebug(r *mux.Router) { r.HandleFunc("/debug/pprof/{name}", pprof.Index) } -// Adds a v1 server handler to the router. -func (s *Server) handleFuncV1(r *mux.Router, path string, f func(http.ResponseWriter, *http.Request, v1.Server) error) *mux.Route { - return s.handleFunc(r, path, func(w http.ResponseWriter, req *http.Request) error { - return f(w, req, s) - }) -} - // Adds a v2 server handler to the router. func (s *Server) handleFuncV2(r *mux.Router, path string, f func(http.ResponseWriter, *http.Request, v2.Server) error) *mux.Route { return s.handleFunc(r, path, func(w http.ResponseWriter, req *http.Request) error { @@ -202,7 +181,6 @@ func (s *Server) HTTPHandler() http.Handler { // Install the routes. s.handleFunc(router, "/version", s.GetVersionHandler).Methods("GET") - s.installV1(router) s.installV2(router) // Mod is deprecated temporariy due to its unstable state. // It would be added back later. @@ -235,26 +213,20 @@ func (s *Server) Dispatch(c raft.Command, w http.ResponseWriter, req *http.Reque return nil } - var b []byte - if strings.HasPrefix(req.URL.Path, "/v1") { - b, _ = json.Marshal(result.(*store.Event).Response(0)) - w.WriteHeader(http.StatusOK) + e, _ := result.(*store.Event) + b, _ := json.Marshal(e) + + 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(e.Index())) + w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex())) + w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term())) + + if e.IsCreated() { + w.WriteHeader(http.StatusCreated) } else { - e, _ := result.(*store.Event) - b, _ = json.Marshal(e) - - 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(e.Index())) - w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex())) - w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term())) - - if e.IsCreated() { - w.WriteHeader(http.StatusCreated) - } else { - w.WriteHeader(http.StatusOK) - } + w.WriteHeader(http.StatusOK) } w.Write(b) diff --git a/server/transporter_test.go b/server/transporter_test.go deleted file mode 100644 index 394841729f0..00000000000 --- a/server/transporter_test.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -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 server - -/* -import ( - "crypto/tls" - "fmt" - "io/ioutil" - "net/http" - "testing" - "time" -) - -func TestTransporterTimeout(t *testing.T) { - - http.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "timeout") - w.(http.Flusher).Flush() // send headers and some body - time.Sleep(time.Second * 100) - }) - - go http.ListenAndServe(":8080", nil) - - conf := tls.Config{} - - ts := newTransporter("http", conf, nil) - - ts.Get("http://google.com") - _, _, err := ts.Get("http://google.com:9999") - if err == nil { - t.Fatal("timeout error") - } - - res, req, err := ts.Get("http://localhost:8080/timeout") - - if err != nil { - t.Fatal("should not timeout") - } - - ts.CancelWhenTimeout(req) - - body, err := ioutil.ReadAll(res.Body) - if err == nil { - fmt.Println(string(body)) - t.Fatal("expected an error reading the body") - } - - _, _, err = ts.Post("http://google.com:9999", nil) - if err == nil { - t.Fatal("timeout error") - } - - _, _, err = ts.Get("http://www.google.com") - if err != nil { - t.Fatal("get error: ", err.Error()) - } - - _, _, err = ts.Post("http://www.google.com", nil) - if err != nil { - t.Fatal("post error") - } - -} -*/ diff --git a/server/v1/delete_key_handler.go b/server/v1/delete_key_handler.go deleted file mode 100644 index fd147601d4c..00000000000 --- a/server/v1/delete_key_handler.go +++ /dev/null @@ -1,15 +0,0 @@ -package v1 - -import ( - "net/http" - - "github.com/coreos/etcd/third_party/github.com/gorilla/mux" -) - -// Removes a key from the store. -func DeleteKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error { - vars := mux.Vars(req) - key := "/" + vars["key"] - c := s.Store().CommandFactory().CreateDeleteCommand(key, false, false) - return s.Dispatch(c, w, req) -} diff --git a/server/v1/get_key_handler.go b/server/v1/get_key_handler.go deleted file mode 100644 index 541480fe51d..00000000000 --- a/server/v1/get_key_handler.go +++ /dev/null @@ -1,31 +0,0 @@ -package v1 - -import ( - "encoding/json" - "net/http" - - "github.com/coreos/etcd/third_party/github.com/gorilla/mux" -) - -// Retrieves the value for a given key. -func GetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error { - vars := mux.Vars(req) - key := "/" + vars["key"] - - // Retrieve the key from the store. - event, err := s.Store().Get(key, false, false) - if err != nil { - return err - } - - w.WriteHeader(http.StatusOK) - - if req.Method == "HEAD" { - return nil - } - - // Convert event to a response and write to client. - b, _ := json.Marshal(event.Response(s.Store().Index())) - w.Write(b) - return nil -} diff --git a/server/v1/set_key_handler.go b/server/v1/set_key_handler.go deleted file mode 100644 index fa27db2a112..00000000000 --- a/server/v1/set_key_handler.go +++ /dev/null @@ -1,47 +0,0 @@ -package v1 - -import ( - "net/http" - - etcdErr "github.com/coreos/etcd/error" - "github.com/coreos/etcd/store" - "github.com/coreos/etcd/third_party/github.com/goraft/raft" - "github.com/coreos/etcd/third_party/github.com/gorilla/mux" -) - -// Sets the value for a given key. -func SetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error { - vars := mux.Vars(req) - key := "/" + vars["key"] - - req.ParseForm() - - // Parse non-blank value. - value := req.Form.Get("value") - if len(value) == 0 { - return etcdErr.NewError(200, "Set", s.Store().Index()) - } - - // Convert time-to-live to an expiration time. - expireTime, err := store.TTL(req.Form.Get("ttl")) - if err != nil { - return etcdErr.NewError(202, "Set", s.Store().Index()) - } - - // If the "prevValue" is specified then test-and-set. Otherwise create a new key. - var c raft.Command - if prevValueArr, ok := req.Form["prevValue"]; ok { - if len(prevValueArr[0]) > 0 { - // test against previous value - c = s.Store().CommandFactory().CreateCompareAndSwapCommand(key, value, prevValueArr[0], 0, expireTime) - } else { - // test against existence - c = s.Store().CommandFactory().CreateCreateCommand(key, false, value, expireTime, false) - } - - } else { - c = s.Store().CommandFactory().CreateSetCommand(key, false, value, expireTime) - } - - return s.Dispatch(c, w, req) -} diff --git a/server/v1/tests/delete_handler_test.go b/server/v1/tests/delete_handler_test.go deleted file mode 100644 index 437e40e9377..00000000000 --- a/server/v1/tests/delete_handler_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package v1 - -import ( - "fmt" - "net/http" - "net/url" - "testing" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a key is deleted. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v1/keys/foo/bar -// -func TestV1DeleteKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"delete","key":"/foo/bar","prevValue":"XXX","index":4}`, "") - }) -} diff --git a/server/v1/tests/get_handler_test.go b/server/v1/tests/get_handler_test.go deleted file mode 100644 index 6e045f1a51f..00000000000 --- a/server/v1/tests/get_handler_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package v1 - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a value can be retrieve for a given key. -// -// $ curl localhost:4001/v1/keys/foo/bar -> fail -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl localhost:4001/v1/keys/foo/bar -// -func TestV1GetKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar") - resp, _ := tests.Get(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - - resp, _ = tests.PutForm(fullURL, v) - tests.ReadBody(resp) - - resp, _ = tests.Get(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "get", "") - assert.Equal(t, body["key"], "/foo/bar", "") - assert.Equal(t, body["value"], "XXX", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that a directory of values can be retrieved for a given key. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/x -d value=XXX -// $ curl -X PUT localhost:4001/v1/keys/foo/y/z -d value=YYY -// $ curl localhost:4001/v1/keys/foo -// -func TestV1GetKeyDir(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/x"), v) - tests.ReadBody(resp) - - v.Set("value", "YYY") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/y/z"), v) - tests.ReadBody(resp) - - resp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo")) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - nodes := make([]interface{}, 0) - if err := json.Unmarshal(body, &nodes); err != nil { - panic(fmt.Sprintf("HTTP body JSON parse error: %v", err)) - } - assert.Equal(t, len(nodes), 2, "") - - node0 := nodes[0].(map[string]interface{}) - assert.Equal(t, node0["action"], "get", "") - assert.Equal(t, node0["key"], "/foo/x", "") - assert.Equal(t, node0["value"], "XXX", "") - - node1 := nodes[1].(map[string]interface{}) - assert.Equal(t, node1["action"], "get", "") - assert.Equal(t, node1["key"], "/foo/y", "") - assert.Equal(t, node1["dir"], true, "") - }) -} - -// Ensures that a watcher can wait for a value to be set and return it to the client. -// -// $ curl localhost:4001/v1/watch/foo/bar -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// -func TestV1WatchKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - // There exists a little gap between etcd ready to serve and - // it actually serves the first request, which means the response - // delay could be a little bigger. - // This test is time sensitive, so it does one request to ensure - // that the server is working. - tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar")) - - var watchResp *http.Response - c := make(chan bool) - go func() { - watchResp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v1/watch/foo/bar")) - c <- true - }() - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - - // Set a value. - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - tests.ReadBody(resp) - - // A response should follow from the GET above. - time.Sleep(1 * time.Millisecond) - - select { - case <-c: - - default: - t.Fatal("cannot get watch result") - } - - body := tests.ReadBodyJSON(watchResp) - assert.NotNil(t, body, "") - assert.Equal(t, body["action"], "set", "") - - assert.Equal(t, body["key"], "/foo/bar", "") - assert.Equal(t, body["value"], "XXX", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that a watcher can wait for a value to be set after a given index. -// -// $ curl -X POST localhost:4001/v1/watch/foo/bar -d index=4 -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY -// -func TestV1WatchKeyWithIndex(t *testing.T) { - tests.RunServer(func(s *server.Server) { - var body map[string]interface{} - c := make(chan bool) - go func() { - v := url.Values{} - v.Set("index", "4") - resp, _ := tests.PostForm(fmt.Sprintf("%s%s", s.URL(), "/v1/watch/foo/bar"), v) - body = tests.ReadBodyJSON(resp) - c <- true - }() - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - assert.Nil(t, body, "") - - // Set a value (before given index). - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - tests.ReadBody(resp) - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - assert.Nil(t, body, "") - - // Set a value (before given index). - v.Set("value", "YYY") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - tests.ReadBody(resp) - - // A response should follow from the GET above. - time.Sleep(1 * time.Millisecond) - - select { - case <-c: - - default: - t.Fatal("cannot get watch result") - } - - assert.NotNil(t, body, "") - assert.Equal(t, body["action"], "set", "") - - assert.Equal(t, body["key"], "/foo/bar", "") - assert.Equal(t, body["value"], "YYY", "") - assert.Equal(t, body["index"], 4, "") - }) -} - -// Ensures that HEAD works. -// -// $ curl -I localhost:4001/v1/keys/foo/bar -> fail -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl -I localhost:4001/v1/keys/foo/bar -// -func TestV1HeadKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar") - resp, _ := tests.Get(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - assert.Equal(t, resp.ContentLength, -1) - - resp, _ = tests.PutForm(fullURL, v) - tests.ReadBody(resp) - - resp, _ = tests.Get(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusOK) - assert.Equal(t, resp.ContentLength, -1) - }) -} diff --git a/server/v1/tests/put_handler_test.go b/server/v1/tests/put_handler_test.go deleted file mode 100644 index f7aeb2e6cd0..00000000000 --- a/server/v1/tests/put_handler_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package v1 - -import ( - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a key is set to a given value. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// -func TestV1SetKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - - assert.Equal(t, string(body), `{"action":"set","key":"/foo/bar","value":"XXX","newKey":true,"index":3}`, "") - }) -} - -// Ensures that a time-to-live is added to a key. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d ttl=20 -// -func TestV1SetKeyWithTTL(t *testing.T) { - tests.RunServer(func(s *server.Server) { - t0 := time.Now() - v := url.Values{} - v.Set("value", "XXX") - v.Set("ttl", "20") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["ttl"], 20, "") - - // Make sure the expiration date is correct. - expiration, _ := time.Parse(time.RFC3339Nano, body["expiration"].(string)) - assert.Equal(t, expiration.Sub(t0)/time.Second, 20, "") - }) -} - -// Ensures that an invalid time-to-live is returned as an error. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d ttl=bad_ttl -// -func TestV1SetKeyWithBadTTL(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("ttl", "bad_ttl") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 202, "") - assert.Equal(t, body["message"], "The given TTL in POST form is not a number", "") - assert.Equal(t, body["cause"], "Set", "") - }) -} - -// Ensures that a key is conditionally set if it previously did not exist. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue= -// -func TestV1CreateKeySuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("prevValue", "") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["value"], "XXX", "") - }) -} - -// Ensures that a key is not conditionally set because it previously existed. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue= -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -d prevValue= -> fail -// -func TestV1CreateKeyFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("prevValue", "") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - tests.ReadBody(resp) - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 105, "") - assert.Equal(t, body["message"], "Key already exists", "") - assert.Equal(t, body["cause"], "/foo/bar", "") - }) -} - -// Ensures that a key is set only if the previous value matches. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY -d prevValue=XXX -// -func TestV1SetKeyCASOnValueSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "XXX") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "testAndSet", "") - assert.Equal(t, body["value"], "YYY", "") - assert.Equal(t, body["index"], 4, "") - }) -} - -// Ensures that a key is not set if the previous value does not match. -// -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v1/keys/foo/bar -d value=YYY -d prevValue=AAA -// -func TestV1SetKeyCASOnValueFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v1/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "AAA") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[AAA != XXX]", "") - assert.Equal(t, body["index"], 3, "") - }) -} diff --git a/server/v1/v1.go b/server/v1/v1.go deleted file mode 100644 index e0c7dc5ff14..00000000000 --- a/server/v1/v1.go +++ /dev/null @@ -1,16 +0,0 @@ -package v1 - -import ( - "net/http" - - "github.com/coreos/etcd/store" - "github.com/coreos/etcd/third_party/github.com/goraft/raft" -) - -// The Server interface provides all the methods required for the v1 API. -type Server interface { - CommitIndex() uint64 - Term() uint64 - Store() store.Store - Dispatch(raft.Command, http.ResponseWriter, *http.Request) error -} diff --git a/server/v1/watch_key_handler.go b/server/v1/watch_key_handler.go deleted file mode 100644 index 7d4d7ada00f..00000000000 --- a/server/v1/watch_key_handler.go +++ /dev/null @@ -1,42 +0,0 @@ -package v1 - -import ( - "encoding/json" - "net/http" - "strconv" - - etcdErr "github.com/coreos/etcd/error" - "github.com/coreos/etcd/third_party/github.com/gorilla/mux" -) - -// Watches a given key prefix for changes. -func WatchKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error { - var err error - vars := mux.Vars(req) - key := "/" + vars["key"] - - // Create a command to watch from a given index (default 0). - var sinceIndex uint64 = 0 - if req.Method == "POST" { - sinceIndex, err = strconv.ParseUint(string(req.FormValue("index")), 10, 64) - if err != nil { - return etcdErr.NewError(203, "Watch From Index", s.Store().Index()) - } - } - - // Start the watcher on the store. - watcher, err := s.Store().Watch(key, false, false, sinceIndex) - if err != nil { - return etcdErr.NewError(500, key, s.Store().Index()) - } - event := <-watcher.EventChan - - // Convert event to a response and write to client. - w.WriteHeader(http.StatusOK) - if req.Method == "HEAD" { - return nil - } - b, _ := json.Marshal(event.Response(s.Store().Index())) - w.Write(b) - return nil -} diff --git a/server/v2/tests/delete_handler_test.go b/server/v2/tests/delete_handler_test.go deleted file mode 100644 index d34daf70aff..00000000000 --- a/server/v2/tests/delete_handler_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package v2 - -import ( - "fmt" - "net/http" - "net/url" - "testing" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a key is deleted. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo/bar -// -func TestV2DeleteKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"delete","node":{"key":"/foo/bar","modifiedIndex":4,"createdIndex":3},"prevNode":{"key":"/foo/bar","value":"XXX","modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that an empty directory is deleted when dir is set. -// -// $ curl -X PUT localhost:4001/v2/keys/foo?dir=true -// $ curl -X DELETE localhost:4001/v2/keys/foo ->fail -// $ curl -X DELETE localhost:4001/v2/keys/foo?dir=true -// -func TestV2DeleteEmptyDirectory(t *testing.T) { - tests.RunServer(func(s *server.Server) { - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), url.Values{}) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusForbidden) - bodyJson := tests.ReadBodyJSON(resp) - assert.Equal(t, bodyJson["errorCode"], 102, "") - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"delete","node":{"key":"/foo","dir":true,"modifiedIndex":4,"createdIndex":3},"prevNode":{"key":"/foo","dir":true,"modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that a not-empty directory is deleted when dir is set. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar?dir=true -// $ curl -X DELETE localhost:4001/v2/keys/foo?dir=true ->fail -// $ curl -X DELETE localhost:4001/v2/keys/foo?dir=true&recursive=true -// -func TestV2DeleteNonEmptyDirectory(t *testing.T) { - tests.RunServer(func(s *server.Server) { - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?dir=true"), url.Values{}) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusForbidden) - bodyJson := tests.ReadBodyJSON(resp) - assert.Equal(t, bodyJson["errorCode"], 108, "") - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true&recursive=true"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"delete","node":{"key":"/foo","dir":true,"modifiedIndex":4,"createdIndex":3},"prevNode":{"key":"/foo","dir":true,"modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that a directory is deleted when recursive is set. -// -// $ curl -X PUT localhost:4001/v2/keys/foo?dir=true -// $ curl -X DELETE localhost:4001/v2/keys/foo?recursive=true -// -func TestV2DeleteDirectoryRecursiveImpliesDir(t *testing.T) { - tests.RunServer(func(s *server.Server) { - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), url.Values{}) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?recursive=true"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"delete","node":{"key":"/foo","dir":true,"modifiedIndex":4,"createdIndex":3},"prevNode":{"key":"/foo","dir":true,"modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that a key is deleted if the previous index matches -// -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo?prevIndex=3 -// -func TestV2DeleteKeyCADOnIndexSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?prevIndex=3"), url.Values{}) - assert.Nil(t, err, "") - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "compareAndDelete", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo", "") - assert.Equal(t, node["modifiedIndex"], 4, "") - }) -} - -// Ensures that a key is not deleted if the previous index does not match -// -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo?prevIndex=100 -// -func TestV2DeleteKeyCADOnIndexFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - tests.ReadBody(resp) - resp, err = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?prevIndex=100"), url.Values{}) - assert.Nil(t, err, "") - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101) - }) -} - -// Ensures that an error is thrown if an invalid previous index is provided. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo/bar?prevIndex=bad_index -// -func TestV2DeleteKeyCADWithInvalidIndex(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, _ = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?prevIndex=bad_index"), v) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 203) - }) -} - -// Ensures that a key is deleted only if the previous value matches. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo/bar?prevValue=XXX -// -func TestV2DeleteKeyCADOnValueSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, _ = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?prevValue=XXX"), v) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "compareAndDelete", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["modifiedIndex"], 4, "") - }) -} - -// Ensures that a key is not deleted if the previous value does not match. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo/bar?prevValue=YYY -// -func TestV2DeleteKeyCADOnValueFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, _ = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?prevValue=YYY"), v) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101) - }) -} - -// Ensures that an error is thrown if an invalid previous value is provided. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X DELETE localhost:4001/v2/keys/foo/bar?prevIndex= -// -func TestV2DeleteKeyCADWithInvalidValue(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - resp, _ = tests.DeleteForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?prevValue="), v) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 201) - }) -} diff --git a/server/v2/tests/get_handler_test.go b/server/v2/tests/get_handler_test.go deleted file mode 100644 index 23a582245aa..00000000000 --- a/server/v2/tests/get_handler_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package v2 - -import ( - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a value can be retrieve for a given key. -// -// $ curl localhost:4001/v2/keys/foo/bar -> fail -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl localhost:4001/v2/keys/foo/bar -// -func TestV2GetKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.Get(fullURL) - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - tests.ReadBody(resp) - - resp, _ = tests.Get(fullURL) - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "get", "") - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/bar", "") - assert.Equal(t, node["value"], "XXX", "") - assert.Equal(t, node["modifiedIndex"], 3, "") - }) -} - -// Ensures that a directory of values can be recursively retrieved for a given key. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/x -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/y/z -d value=YYY -// $ curl localhost:4001/v2/keys/foo -d recursive=true -// -func TestV2GetKeyRecursively(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("ttl", "10") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/x"), v) - tests.ReadBody(resp) - - v.Set("value", "YYY") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/y/z"), v) - tests.ReadBody(resp) - - resp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?recursive=true")) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "get", "") - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo", "") - assert.Equal(t, node["dir"], true, "") - assert.Equal(t, node["modifiedIndex"], 3, "") - assert.Equal(t, len(node["nodes"].([]interface{})), 2, "") - - node0 := node["nodes"].([]interface{})[0].(map[string]interface{}) - assert.Equal(t, node0["key"], "/foo/x", "") - assert.Equal(t, node0["value"], "XXX", "") - assert.Equal(t, node0["ttl"], 10, "") - - node1 := node["nodes"].([]interface{})[1].(map[string]interface{}) - assert.Equal(t, node1["key"], "/foo/y", "") - assert.Equal(t, node1["dir"], true, "") - - node2 := node1["nodes"].([]interface{})[0].(map[string]interface{}) - assert.Equal(t, node2["key"], "/foo/y/z", "") - assert.Equal(t, node2["value"], "YYY", "") - }) -} - -// Ensures that a watcher can wait for a value to be set and return it to the client. -// -// $ curl localhost:4001/v2/keys/foo/bar?wait=true -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// -func TestV2WatchKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - // There exists a little gap between etcd ready to serve and - // it actually serves the first request, which means the response - // delay could be a little bigger. - // This test is time sensitive, so it does one request to ensure - // that the server is working. - tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar")) - - var watchResp *http.Response - c := make(chan bool) - go func() { - watchResp, _ = tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?wait=true")) - c <- true - }() - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - - // Set a value. - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - - // A response should follow from the GET above. - time.Sleep(1 * time.Millisecond) - - select { - case <-c: - - default: - t.Fatal("cannot get watch result") - } - - body := tests.ReadBodyJSON(watchResp) - assert.NotNil(t, body, "") - assert.Equal(t, body["action"], "set", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/bar", "") - assert.Equal(t, node["value"], "XXX", "") - assert.Equal(t, node["modifiedIndex"], 3, "") - }) -} - -// Ensures that a watcher can wait for a value to be set after a given index. -// -// $ curl localhost:4001/v2/keys/foo/bar?wait=true&waitIndex=4 -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -// -func TestV2WatchKeyWithIndex(t *testing.T) { - tests.RunServer(func(s *server.Server) { - var body map[string]interface{} - c := make(chan bool) - go func() { - resp, _ := tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar?wait=true&waitIndex=4")) - body = tests.ReadBodyJSON(resp) - c <- true - }() - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - assert.Nil(t, body, "") - - // Set a value (before given index). - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - - // Make sure response didn't fire early. - time.Sleep(1 * time.Millisecond) - assert.Nil(t, body, "") - - // Set a value (before given index). - v.Set("value", "YYY") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - tests.ReadBody(resp) - - // A response should follow from the GET above. - time.Sleep(1 * time.Millisecond) - - select { - case <-c: - - default: - t.Fatal("cannot get watch result") - } - - assert.NotNil(t, body, "") - assert.Equal(t, body["action"], "set", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/bar", "") - assert.Equal(t, node["value"], "YYY", "") - assert.Equal(t, node["modifiedIndex"], 4, "") - }) -} - -// Ensures that a watcher can wait for a value to be set after a given index. -// -// $ curl localhost:4001/v2/keys/keyindir/bar?wait=true -// $ curl -X PUT localhost:4001/v2/keys/keyindir -d dir=true -d ttl=1 -// $ curl -X PUT localhost:4001/v2/keys/keyindir/bar -d value=YYY -// -func TestV2WatchKeyInDir(t *testing.T) { - tests.RunServer(func(s *server.Server) { - var body map[string]interface{} - c := make(chan bool) - - // Set a value (before given index). - v := url.Values{} - v.Set("dir", "true") - v.Set("ttl", "1") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/keyindir"), v) - tests.ReadBody(resp) - - // Set a value (before given index). - v = url.Values{} - v.Set("value", "XXX") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/keyindir/bar"), v) - tests.ReadBody(resp) - - go func() { - resp, _ := tests.Get(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/keyindir/bar?wait=true")) - body = tests.ReadBodyJSON(resp) - c <- true - }() - - // wait for expiration, we do have a up to 500 millisecond delay - time.Sleep(2000 * time.Millisecond) - - select { - case <-c: - - default: - t.Fatal("cannot get watch result") - } - - assert.NotNil(t, body, "") - assert.Equal(t, body["action"], "expire", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/keyindir", "") - }) -} - -// Ensures that HEAD could work. -// -// $ curl -I localhost:4001/v2/keys/foo/bar -> fail -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -I localhost:4001/v2/keys/foo/bar -// -func TestV2HeadKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.Head(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - assert.Equal(t, resp.ContentLength, -1) - - resp, _ = tests.PutForm(fullURL, v) - tests.ReadBody(resp) - - resp, _ = tests.Head(fullURL) - assert.Equal(t, resp.StatusCode, http.StatusOK) - assert.Equal(t, resp.ContentLength, -1) - }) -} diff --git a/server/v2/tests/post_handler_test.go b/server/v2/tests/post_handler_test.go deleted file mode 100644 index cb143e17b5a..00000000000 --- a/server/v2/tests/post_handler_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package v2 - -import ( - "fmt" - "net/http" - "testing" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures a unique value is added to the key's children. -// -// $ curl -X POST localhost:4001/v2/keys/foo/bar -// $ curl -X POST localhost:4001/v2/keys/foo/bar -// $ curl -X POST localhost:4001/v2/keys/foo/baz -// -func TestV2CreateUnique(t *testing.T) { - tests.RunServer(func(s *server.Server) { - // POST should add index to list. - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PostForm(fullURL, nil) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "create", "") - - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/bar/3", "") - assert.Nil(t, node["dir"], "") - assert.Equal(t, node["modifiedIndex"], 3, "") - - // Second POST should add next index to list. - resp, _ = tests.PostForm(fullURL, nil) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body = tests.ReadBodyJSON(resp) - - node = body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/bar/4", "") - - // POST to a different key should add index to that list. - resp, _ = tests.PostForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/baz"), nil) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body = tests.ReadBodyJSON(resp) - - node = body["node"].(map[string]interface{}) - assert.Equal(t, node["key"], "/foo/baz/5", "") - }) -} diff --git a/server/v2/tests/put_handler_test.go b/server/v2/tests/put_handler_test.go deleted file mode 100644 index 318e71a2d88..00000000000 --- a/server/v2/tests/put_handler_test.go +++ /dev/null @@ -1,460 +0,0 @@ -package v2 - -import ( - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/coreos/etcd/server" - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensures that a key is set to a given value. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// -func TestV2SetKey(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"set","node":{"key":"/foo/bar","value":"XXX","modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that a directory is created -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar?dir=true -// -func TestV2SetDirectory(t *testing.T) { - tests.RunServer(func(s *server.Server) { - resp, err := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), url.Values{}) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, string(body), `{"action":"set","node":{"key":"/foo","dir":true,"modifiedIndex":3,"createdIndex":3}}`, "") - }) -} - -// Ensures that a time-to-live is added to a key. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d ttl=20 -// -func TestV2SetKeyWithTTL(t *testing.T) { - tests.RunServer(func(s *server.Server) { - t0 := time.Now() - v := url.Values{} - v.Set("value", "XXX") - v.Set("ttl", "20") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBodyJSON(resp) - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["ttl"], 20, "") - - // Make sure the expiration date is correct. - expiration, _ := time.Parse(time.RFC3339Nano, node["expiration"].(string)) - assert.Equal(t, expiration.Sub(t0)/time.Second, 20, "") - }) -} - -// Ensures that an invalid time-to-live is returned as an error. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d ttl=bad_ttl -// -func TestV2SetKeyWithBadTTL(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("ttl", "bad_ttl") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 202, "") - assert.Equal(t, body["message"], "The given TTL in POST form is not a number", "") - assert.Equal(t, body["cause"], "Update", "") - }) -} - -// Ensures that a key is conditionally set if it previously did not exist. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=false -// -func TestV2CreateKeySuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("prevExist", "false") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBodyJSON(resp) - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["value"], "XXX", "") - }) -} - -// Ensures that a key is not conditionally set because it previously existed. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=false -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=false -> fail -// -func TestV2CreateKeyFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("prevExist", "false") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 105, "") - assert.Equal(t, body["message"], "Key already exists", "") - assert.Equal(t, body["cause"], "/foo/bar", "") - }) -} - -// Ensures that a key is conditionally set only if it previously did exist. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevExist=true -// -func TestV2UpdateKeySuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - - v.Set("value", "YYY") - v.Set("prevExist", "true") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "update", "") - }) -} - -// Ensures that a key is not conditionally set if it previously did not exist. -// -// $ curl -X PUT localhost:4001/v2/keys/foo?dir=true -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=true -// -func TestV2UpdateKeyFailOnValue(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo?dir=true"), v) - - assert.Equal(t, resp.StatusCode, http.StatusCreated) - v.Set("value", "YYY") - v.Set("prevExist", "true") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 100, "") - assert.Equal(t, body["message"], "Key not found", "") - assert.Equal(t, body["cause"], "/foo/bar", "") - }) -} - -// Ensures that a key is not conditionally set if it previously did not exist. -// -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=YYY -d prevExist=true -> fail -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevExist=true -> fail -// -func TestV2UpdateKeyFailOnMissingDirectory(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "YYY") - v.Set("prevExist", "true") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 100, "") - assert.Equal(t, body["message"], "Key not found", "") - assert.Equal(t, body["cause"], "/foo", "") - - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - body = tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 100, "") - assert.Equal(t, body["message"], "Key not found", "") - assert.Equal(t, body["cause"], "/foo", "") - }) -} - -// Ensures that a key could update TTL. -// -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -d ttl=1000 -d prevExist=true -// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -d ttl= -d prevExist=true -// -func TestV2UpdateKeySuccessWithTTL(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - node := (tests.ReadBodyJSON(resp)["node"]).(map[string]interface{}) - createdIndex := node["createdIndex"] - - v.Set("ttl", "1000") - v.Set("prevExist", "true") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - node = (tests.ReadBodyJSON(resp)["node"]).(map[string]interface{}) - assert.Equal(t, node["value"], "XXX", "") - assert.Equal(t, node["ttl"], 1000, "") - assert.NotEqual(t, node["expiration"], "", "") - assert.Equal(t, node["createdIndex"], createdIndex, "") - - v.Del("ttl") - resp, _ = tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo"), v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - node = (tests.ReadBodyJSON(resp)["node"]).(map[string]interface{}) - assert.Equal(t, node["value"], "XXX", "") - assert.Equal(t, node["ttl"], nil, "") - assert.Equal(t, node["expiration"], nil, "") - assert.Equal(t, node["createdIndex"], createdIndex, "") - }) -} - -// Ensures that a key is set only if the previous index matches. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=1 -// -func TestV2SetKeyCASOnIndexSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevIndex", "3") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "compareAndSwap", "") - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["value"], "YYY", "") - assert.Equal(t, node["modifiedIndex"], 4, "") - }) -} - -// Ensures that a key is not set if the previous index does not match. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=10 -// -func TestV2SetKeyCASOnIndexFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevIndex", "10") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[10 != 3]", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that an error is thrown if an invalid previous index is provided. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=bad_index -// -func TestV2SetKeyCASWithInvalidIndex(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "YYY") - v.Set("prevIndex", "bad_index") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 203, "") - assert.Equal(t, body["message"], "The given index in POST form is not a number", "") - assert.Equal(t, body["cause"], "CompareAndSwap", "") - }) -} - -// Ensures that a key is set only if the previous value matches. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=XXX -// -func TestV2SetKeyCASOnValueSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "XXX") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusOK) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["action"], "compareAndSwap", "") - node := body["node"].(map[string]interface{}) - assert.Equal(t, node["value"], "YYY", "") - assert.Equal(t, node["modifiedIndex"], 4, "") - }) -} - -// Ensures that a key is not set if the previous value does not match. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=AAA -// -func TestV2SetKeyCASOnValueFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "AAA") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[AAA != XXX]", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that an error is returned if a blank prevValue is set. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevValue= -// -func TestV2SetKeyCASWithMissingValueFails(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - v.Set("prevValue", "") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 201, "") - assert.Equal(t, body["message"], "PrevValue is Required in POST form", "") - assert.Equal(t, body["cause"], "CompareAndSwap", "") - }) -} - -// Ensures that a key is not set if both previous value and index do not match. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=AAA -d prevIndex=4 -// -func TestV2SetKeyCASOnValueAndIndexFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "AAA") - v.Set("prevIndex", "4") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[AAA != XXX] [4 != 3]", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that a key is not set if previous value match but index does not. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=XXX -d prevIndex=4 -// -func TestV2SetKeyCASOnValueMatchAndIndexFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "XXX") - v.Set("prevIndex", "4") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[4 != 3]", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensures that a key is not set if previous index matches but value does not. -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=AAA -d prevIndex=3 -// -func TestV2SetKeyCASOnIndexMatchAndValueFail(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "XXX") - fullURL := fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar") - resp, _ := tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - tests.ReadBody(resp) - v.Set("value", "YYY") - v.Set("prevValue", "AAA") - v.Set("prevIndex", "3") - resp, _ = tests.PutForm(fullURL, v) - assert.Equal(t, resp.StatusCode, http.StatusPreconditionFailed) - body := tests.ReadBodyJSON(resp) - assert.Equal(t, body["errorCode"], 101, "") - assert.Equal(t, body["message"], "Compare failed", "") - assert.Equal(t, body["cause"], "[AAA != XXX]", "") - assert.Equal(t, body["index"], 3, "") - }) -} - -// Ensure that we can set an empty value -// -// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value= -// -func TestV2SetKeyCASWithEmptyValueSuccess(t *testing.T) { - tests.RunServer(func(s *server.Server) { - v := url.Values{} - v.Set("value", "") - resp, _ := tests.PutForm(fmt.Sprintf("%s%s", s.URL(), "/v2/keys/foo/bar"), v) - assert.Equal(t, resp.StatusCode, http.StatusCreated) - body := tests.ReadBody(resp) - assert.Equal(t, string(body), `{"action":"set","node":{"key":"/foo/bar","value":"","modifiedIndex":3,"createdIndex":3}}`) - }) -} diff --git a/store/event.go b/store/event.go index 84ebf3ce360..5d702ec3be1 100644 --- a/store/event.go +++ b/store/event.go @@ -45,54 +45,3 @@ func (e *Event) IsCreated() bool { func (e *Event) Index() uint64 { return e.Node.ModifiedIndex } - -// Converts an event object into a response object. -func (event *Event) Response(currentIndex uint64) interface{} { - if !event.Node.Dir { - response := &Response{ - Action: event.Action, - Key: event.Node.Key, - Value: event.Node.Value, - Index: event.Node.ModifiedIndex, - TTL: event.Node.TTL, - Expiration: event.Node.Expiration, - } - - if event.PrevNode != nil { - response.PrevValue = event.PrevNode.Value - } - - if currentIndex != 0 { - response.Index = currentIndex - } - - if response.Action == Set { - if response.PrevValue == nil { - response.NewKey = true - } - } - - if response.Action == CompareAndSwap || response.Action == Create { - response.Action = "testAndSet" - } - - return response - } else { - responses := make([]*Response, len(event.Node.Nodes)) - - for i, node := range event.Node.Nodes { - responses[i] = &Response{ - Action: event.Action, - Key: node.Key, - Value: node.Value, - Dir: node.Dir, - Index: node.ModifiedIndex, - } - - if currentIndex != 0 { - responses[i].Index = currentIndex - } - } - return responses - } -} diff --git a/store/node.go b/store/node.go index f11619a600e..8ea253019ba 100644 --- a/store/node.go +++ b/store/node.go @@ -101,7 +101,7 @@ func (n *node) IsDir() bool { // If the receiver node is not a key-value pair, a "Not A File" error will be returned. func (n *node) Read() (string, *etcdErr.Error) { if n.IsDir() { - return "", etcdErr.NewError(etcdErr.EcodeNotFile, "", n.store.Index()) + return "", etcdErr.NewError(etcdErr.EcodeNotFile, "", n.store.CurrentIndex) } return n.Value, nil @@ -111,7 +111,7 @@ func (n *node) Read() (string, *etcdErr.Error) { // If the receiver node is a directory, a "Not A File" error will be returned. func (n *node) Write(value string, index uint64) *etcdErr.Error { if n.IsDir() { - return etcdErr.NewError(etcdErr.EcodeNotFile, "", n.store.Index()) + return etcdErr.NewError(etcdErr.EcodeNotFile, "", n.store.CurrentIndex) } n.Value = value @@ -143,7 +143,7 @@ func (n *node) ExpirationAndTTL() (*time.Time, int64) { // If the receiver node is not a directory, a "Not A Directory" error will be returned. func (n *node) List() ([]*node, *etcdErr.Error) { if !n.IsDir() { - return nil, etcdErr.NewError(etcdErr.EcodeNotDir, "", n.store.Index()) + return nil, etcdErr.NewError(etcdErr.EcodeNotDir, "", n.store.CurrentIndex) } nodes := make([]*node, len(n.Children)) @@ -161,7 +161,7 @@ func (n *node) List() ([]*node, *etcdErr.Error) { // On success, it returns the file node func (n *node) GetChild(name string) (*node, *etcdErr.Error) { if !n.IsDir() { - return nil, etcdErr.NewError(etcdErr.EcodeNotDir, n.Path, n.store.Index()) + return nil, etcdErr.NewError(etcdErr.EcodeNotDir, n.Path, n.store.CurrentIndex) } child, ok := n.Children[name] @@ -179,7 +179,7 @@ func (n *node) GetChild(name string) (*node, *etcdErr.Error) { // error will be returned func (n *node) Add(child *node) *etcdErr.Error { if !n.IsDir() { - return etcdErr.NewError(etcdErr.EcodeNotDir, "", n.store.Index()) + return etcdErr.NewError(etcdErr.EcodeNotDir, "", n.store.CurrentIndex) } _, name := path.Split(child.Path) @@ -187,7 +187,7 @@ func (n *node) Add(child *node) *etcdErr.Error { _, ok := n.Children[name] if ok { - return etcdErr.NewError(etcdErr.EcodeNodeExist, "", n.store.Index()) + return etcdErr.NewError(etcdErr.EcodeNodeExist, "", n.store.CurrentIndex) } n.Children[name] = child @@ -201,13 +201,13 @@ func (n *node) Remove(dir, recursive bool, callback func(path string)) *etcdErr. if n.IsDir() { if !dir { // cannot delete a directory without recursive set to true - return etcdErr.NewError(etcdErr.EcodeNotFile, n.Path, n.store.Index()) + return etcdErr.NewError(etcdErr.EcodeNotFile, n.Path, n.store.CurrentIndex) } if len(n.Children) != 0 && !recursive { // cannot delete a directory if it is not empty and the operation // is not recursive - return etcdErr.NewError(etcdErr.EcodeDirNotEmpty, n.Path, n.store.Index()) + return etcdErr.NewError(etcdErr.EcodeDirNotEmpty, n.Path, n.store.CurrentIndex) } } diff --git a/store/response_v1.go b/store/response_v1.go deleted file mode 100644 index 5b9244bf9a0..00000000000 --- a/store/response_v1.go +++ /dev/null @@ -1,26 +0,0 @@ -package store - -import ( - "time" -) - -// The response from the store to the user who issue a command -type Response struct { - Action string `json:"action"` - Key string `json:"key"` - Dir bool `json:"dir,omitempty"` - PrevValue *string `json:"prevValue,omitempty"` - Value *string `json:"value,omitempty"` - - // If the key did not exist before the action, - // this field should be set to true - NewKey bool `json:"newKey,omitempty"` - - Expiration *time.Time `json:"expiration,omitempty"` - - // Time to live in second - TTL int64 `json:"ttl,omitempty"` - - // The command index of the raft machine when the command is executed - Index uint64 `json:"index"` -} diff --git a/store/store.go b/store/store.go index 359c359542d..3c17289572e 100644 --- a/store/store.go +++ b/store/store.go @@ -94,6 +94,8 @@ func (s *store) Version() int { // Retrieves current of the store func (s *store) Index() uint64 { + s.worldLock.RLock() + defer s.worldLock.RUnlock() return s.CurrentIndex } diff --git a/store/watcher.go b/store/watcher.go index 6d583b6a729..3c201e0d0e3 100644 --- a/store/watcher.go +++ b/store/watcher.go @@ -68,5 +68,7 @@ func (w *Watcher) Remove() { defer w.hub.mutex.Unlock() close(w.EventChan) - w.remove() + if w.remove != nil { + w.remove() + } } diff --git a/test.sh b/test.sh index 9a7af6fde8b..5cabb52a1f3 100755 --- a/test.sh +++ b/test.sh @@ -17,9 +17,6 @@ go test -v ./server -race go test -i ./config go test -v ./config -race -go test -i ./server/v1/tests -go test -v ./server/v1/tests -race - go test -i ./server/v2/tests go test -v ./server/v2/tests -race diff --git a/tests/fixtures/v1.cluster/README b/tests/fixtures/v1.cluster/README deleted file mode 100644 index 8e144444a3e..00000000000 --- a/tests/fixtures/v1.cluster/README +++ /dev/null @@ -1,15 +0,0 @@ -README - -The scripts in this directory should be run from the project root: - -$ cd $GOPATH/src/github.com/coreos/etcd -$ tests/fixtures/v1/run.1.sh - -Scripts with numbers should be run in separate terminal windows (in order): - -$ tests/fixtures/v1/run.1.sh -$ tests/fixtures/v1/run.2.sh -$ tests/fixtures/v1/run.3.sh -$ tests/fixtures/v1/run.4.sh - -The resulting server state data can be found in tmp/node*. diff --git a/tests/fixtures/v1.cluster/node0/conf b/tests/fixtures/v1.cluster/node0/conf deleted file mode 100644 index 8f401dbe448..00000000000 --- a/tests/fixtures/v1.cluster/node0/conf +++ /dev/null @@ -1 +0,0 @@ -{"commitIndex":15,"peers":[{"name":"node2","connectionString":""}]} \ No newline at end of file diff --git a/tests/fixtures/v1.cluster/node0/info b/tests/fixtures/v1.cluster/node0/info deleted file mode 100644 index 398c8e1e80a..00000000000 --- a/tests/fixtures/v1.cluster/node0/info +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "node0", - "raftURL": "http://127.0.0.1:7001", - "etcdURL": "http://127.0.0.1:4001", - "webURL": "", - "raftListenHost": "127.0.0.1:7001", - "etcdListenHost": "127.0.0.1:4001", - "raftTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - }, - "etcdTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - } -} diff --git a/tests/fixtures/v1.cluster/node0/log b/tests/fixtures/v1.cluster/node0/log deleted file mode 100644 index de3e7075e93..00000000000 Binary files a/tests/fixtures/v1.cluster/node0/log and /dev/null differ diff --git a/tests/fixtures/v1.cluster/node2/conf b/tests/fixtures/v1.cluster/node2/conf deleted file mode 100644 index 19d6a9c9228..00000000000 --- a/tests/fixtures/v1.cluster/node2/conf +++ /dev/null @@ -1 +0,0 @@ -{"commitIndex":15,"peers":[{"name":"node0","connectionString":""}]} \ No newline at end of file diff --git a/tests/fixtures/v1.cluster/node2/info b/tests/fixtures/v1.cluster/node2/info deleted file mode 100644 index 85114a5f810..00000000000 --- a/tests/fixtures/v1.cluster/node2/info +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "node2", - "raftURL": "http://127.0.0.1:7002", - "etcdURL": "http://127.0.0.1:4002", - "webURL": "", - "raftListenHost": "127.0.0.1:7002", - "etcdListenHost": "127.0.0.1:4002", - "raftTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - }, - "etcdTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - } -} diff --git a/tests/fixtures/v1.cluster/node2/log b/tests/fixtures/v1.cluster/node2/log deleted file mode 100644 index de3e7075e93..00000000000 Binary files a/tests/fixtures/v1.cluster/node2/log and /dev/null differ diff --git a/tests/fixtures/v1.cluster/node3/conf b/tests/fixtures/v1.cluster/node3/conf deleted file mode 100644 index d8a5840de89..00000000000 --- a/tests/fixtures/v1.cluster/node3/conf +++ /dev/null @@ -1 +0,0 @@ -{"commitIndex":15,"peers":[{"name":"node0","connectionString":""},{"name":"node2","connectionString":""}]} \ No newline at end of file diff --git a/tests/fixtures/v1.cluster/node3/info b/tests/fixtures/v1.cluster/node3/info deleted file mode 100644 index 5e5cb3f3a84..00000000000 --- a/tests/fixtures/v1.cluster/node3/info +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "node3", - "raftURL": "http://127.0.0.1:7003", - "etcdURL": "http://127.0.0.1:4003", - "webURL": "", - "raftListenHost": "127.0.0.1:7003", - "etcdListenHost": "127.0.0.1:4003", - "raftTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - }, - "etcdTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - } -} diff --git a/tests/fixtures/v1.cluster/node3/log b/tests/fixtures/v1.cluster/node3/log deleted file mode 100644 index de3e7075e93..00000000000 Binary files a/tests/fixtures/v1.cluster/node3/log and /dev/null differ diff --git a/tests/fixtures/v1.cluster/run.1.sh b/tests/fixtures/v1.cluster/run.1.sh deleted file mode 100755 index ee77deaed26..00000000000 --- a/tests/fixtures/v1.cluster/run.1.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -./build -./etcd -d tmp/node0 -n node0 diff --git a/tests/fixtures/v1.cluster/run.2.sh b/tests/fixtures/v1.cluster/run.2.sh deleted file mode 100755 index 1b067eb2b83..00000000000 --- a/tests/fixtures/v1.cluster/run.2.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -./etcd -s 127.0.0.1:7002 -c 127.0.0.1:4002 -C 127.0.0.1:7001 -d tmp/node2 -n node2 diff --git a/tests/fixtures/v1.cluster/run.3.sh b/tests/fixtures/v1.cluster/run.3.sh deleted file mode 100755 index a1c9c6b3e56..00000000000 --- a/tests/fixtures/v1.cluster/run.3.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -./etcd -s 127.0.0.1:7003 -c 127.0.0.1:4003 -C 127.0.0.1:7001 -d tmp/node3 -n node3 diff --git a/tests/fixtures/v1.cluster/run.4.sh b/tests/fixtures/v1.cluster/run.4.sh deleted file mode 100755 index 15c756eb091..00000000000 --- a/tests/fixtures/v1.cluster/run.4.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello world" -curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello etcd" -curl -L http://127.0.0.1:4001/v1/keys/message -X DELETE -curl -L http://127.0.0.1:4001/v1/keys/message2 -d value="Hola" -curl -L http://127.0.0.1:4001/v1/keys/expiring -d value=bar -d ttl=5 -curl -L http://127.0.0.1:4001/v1/keys/foo -d value=one -curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=two -d value=three -curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=one -d value=two -curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=four -curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=five -curl -X DELETE http://127.0.0.1:7001/remove/node2 diff --git a/tests/fixtures/v1.solo/README b/tests/fixtures/v1.solo/README deleted file mode 100644 index 65d86d32381..00000000000 --- a/tests/fixtures/v1.solo/README +++ /dev/null @@ -1,13 +0,0 @@ -README - -The scripts in this directory should be run from the project root: - -$ cd $GOPATH/src/github.com/coreos/etcd -$ tests/fixtures/v1.solo/run.1.sh - -Scripts with numbers should be run in separate terminal windows (in order): - -$ tests/fixtures/v1/run.1.sh -$ tests/fixtures/v1/run.2.sh - -The resulting server state data can be found in tmp/node0. diff --git a/tests/fixtures/v1.solo/node0/conf b/tests/fixtures/v1.solo/node0/conf deleted file mode 100644 index 95106f8b126..00000000000 --- a/tests/fixtures/v1.solo/node0/conf +++ /dev/null @@ -1 +0,0 @@ -{"commitIndex":1,"peers":[]} \ No newline at end of file diff --git a/tests/fixtures/v1.solo/node0/info b/tests/fixtures/v1.solo/node0/info deleted file mode 100644 index 398c8e1e80a..00000000000 --- a/tests/fixtures/v1.solo/node0/info +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "node0", - "raftURL": "http://127.0.0.1:7001", - "etcdURL": "http://127.0.0.1:4001", - "webURL": "", - "raftListenHost": "127.0.0.1:7001", - "etcdListenHost": "127.0.0.1:4001", - "raftTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - }, - "etcdTLS": { - "CertFile": "", - "KeyFile": "", - "CAFile": "" - } -} diff --git a/tests/fixtures/v1.solo/node0/log b/tests/fixtures/v1.solo/node0/log deleted file mode 100644 index 661d21d0e8d..00000000000 Binary files a/tests/fixtures/v1.solo/node0/log and /dev/null differ diff --git a/tests/fixtures/v1.solo/run.1.sh b/tests/fixtures/v1.solo/run.1.sh deleted file mode 100755 index ee77deaed26..00000000000 --- a/tests/fixtures/v1.solo/run.1.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -./build -./etcd -d tmp/node0 -n node0 diff --git a/tests/fixtures/v1.solo/run.2.sh b/tests/fixtures/v1.solo/run.2.sh deleted file mode 100755 index 96bd3e862fc..00000000000 --- a/tests/fixtures/v1.solo/run.2.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello world" diff --git a/tests/functional/cluster_config_test.go b/tests/functional/cluster_config_test.go index 09c1c1054ed..e7697e9b03f 100644 --- a/tests/functional/cluster_config_test.go +++ b/tests/functional/cluster_config_test.go @@ -2,7 +2,6 @@ package test import ( "bytes" - "encoding/json" "os" "testing" "time" @@ -11,25 +10,6 @@ import ( "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" ) -// Ensure that the cluster configuration can be updated. -func TestClusterConfigSet(t *testing.T) { - _, etcds, err := CreateCluster(3, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false) - assert.NoError(t, err) - defer DestroyCluster(etcds) - - resp, _ := tests.Put("http://localhost:7001/v2/admin/config", "application/json", bytes.NewBufferString(`{"activeSize":3, "removeDelay":60}`)) - assert.Equal(t, resp.StatusCode, 200) - - time.Sleep(1 * time.Second) - - resp, _ = tests.Get("http://localhost:7002/v2/admin/config") - body := tests.ReadBodyJSON(resp) - assert.Equal(t, resp.StatusCode, 200) - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - assert.Equal(t, body["activeSize"], 3) - assert.Equal(t, body["removeDelay"], 60) -} - // Ensure that the cluster configuration can be reloaded. func TestClusterConfigReload(t *testing.T) { procAttr := &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}} @@ -65,26 +45,3 @@ func TestClusterConfigReload(t *testing.T) { assert.Equal(t, body["activeSize"], 3) assert.Equal(t, body["removeDelay"], 60) } - -// TestGetMachines tests '/v2/admin/machines' sends back messages of all machines. -func TestGetMachines(t *testing.T) { - _, etcds, err := CreateCluster(3, &os.ProcAttr{Files: []*os.File{nil, os.Stdout, os.Stderr}}, false) - assert.NoError(t, err) - defer DestroyCluster(etcds) - - time.Sleep(1 * time.Second) - - resp, err := tests.Get("http://localhost:7001/v2/admin/machines") - if !assert.Equal(t, err, nil) { - t.FailNow() - } - assert.Equal(t, resp.StatusCode, 200) - assert.Equal(t, resp.Header.Get("Content-Type"), "application/json") - machines := make([]map[string]interface{}, 0) - b := tests.ReadBody(resp) - json.Unmarshal(b, &machines) - assert.Equal(t, len(machines), 3) - if machines[0]["state"] != "leader" && machines[1]["state"] != "leader" && machines[2]["state"] != "leader" { - t.Errorf("no leader in the cluster") - } -} diff --git a/tests/functional/join_test.go b/tests/functional/join_test.go deleted file mode 100644 index 3eaff710b53..00000000000 --- a/tests/functional/join_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package test - -import ( - "os" - "testing" - "time" -) - -func TestJoinThroughFollower(t *testing.T) { - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - - _, etcds, err := CreateCluster(2, procAttr, false) - if err != nil { - t.Fatal("cannot create cluster") - } - defer DestroyCluster(etcds) - - time.Sleep(time.Second) - - newEtcd, err := os.StartProcess(EtcdBinPath, []string{"etcd", "-data-dir=/tmp/node3", "-name=node3", "-addr=127.0.0.1:4003", "-peer-addr=127.0.0.1:7003", "-peers=127.0.0.1:7002", "-f"}, procAttr) - if err != nil { - t.Fatal("failed starting node3") - } - defer func() { - newEtcd.Kill() - newEtcd.Release() - }() - - time.Sleep(time.Second) - - leader, err := getLeader("http://127.0.0.1:4003") - if err != nil { - t.Fatal("failed getting leader from node3:", err) - } - if leader != "http://127.0.0.1:7001" { - t.Fatal("expect=http://127.0.0.1:7001 got=", leader) - } -} diff --git a/tests/functional/kill_leader_test.go b/tests/functional/kill_leader_test.go index 7c18d46befa..80f2cee74c5 100644 --- a/tests/functional/kill_leader_test.go +++ b/tests/functional/kill_leader_test.go @@ -15,58 +15,6 @@ import ( "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. -func TestKillLeader(t *testing.T) { - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - - clusterSize := 3 - 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) - - var totalTime time.Duration - - leader := "http://127.0.0.1:7001" - - for i := 0; i < clusterSize; i++ { - fmt.Println("leader is ", leader) - port, _ := strconv.Atoi(strings.Split(leader, ":")[2]) - num := port - 7001 - fmt.Println("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) - - etcds[num], err = os.StartProcess(EtcdBinPath, argGroup[num], procAttr) - } - stop <- true -} - // 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. diff --git a/tests/functional/kill_random_test.go b/tests/functional/kill_random_test.go deleted file mode 100644 index f8af96e234b..00000000000 --- a/tests/functional/kill_random_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package test - -import ( - "fmt" - "math/rand" - "os" - "testing" - "time" -) - -// TestKillRandom kills random peers in the cluster and -// restart them after all other peers agree on the same leader -func TestKillRandom(t *testing.T) { - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - - clusterSize := 9 - 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(3 * time.Second) - - go Monitor(clusterSize, 4, leaderChan, all, stop) - - toKill := make(map[int]bool) - - for i := 0; i < 20; i++ { - fmt.Printf("TestKillRandom Round[%d/20]\n", i) - - j := 0 - for { - - r := rand.Int31n(9) - if _, ok := toKill[int(r)]; !ok { - j++ - toKill[int(r)] = true - } - - if j > 3 { - break - } - - } - - for num := range toKill { - err := etcds[num].Kill() - if err != nil { - panic(err) - } - etcds[num].Wait() - } - - time.Sleep(1 * time.Second) - - <-leaderChan - - for num := range toKill { - etcds[num], err = os.StartProcess(EtcdBinPath, argGroup[num], procAttr) - } - - toKill = make(map[int]bool) - <-all - } - - stop <- true -} diff --git a/tests/functional/single_node_test.go b/tests/functional/single_node_test.go deleted file mode 100644 index e8b3011e055..00000000000 --- a/tests/functional/single_node_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package test - -import ( - "os" - "testing" - "time" - - "github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd" -) - -// Create a single node and try to set value -func TestSingleNode(t *testing.T) { - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - args := []string{"etcd", "-name=node1", "-f", "-data-dir=/tmp/node1"} - - process, err := os.StartProcess(EtcdBinPath, args, procAttr) - if err != nil { - t.Fatal("start process failed:" + err.Error()) - return - } - defer process.Kill() - - time.Sleep(time.Second) - - c := etcd.NewClient(nil) - - c.SyncCluster() - // Test Set - 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("Set 1: ", err) - } - - 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) - node = result.Node - - if err != nil || node.Key != "/foo" || node.Value != "bar" || node.TTL != 100 { - if err != nil { - t.Fatal("Set 2: ", err) - } - t.Fatalf("Set 2 failed with %s %s %v", node.Key, node.Value, node.TTL) - } - - // Add a test-and-set test - - // First, we'll test we can change the value if we get it write - result, err = c.CompareAndSwap("foo", "foobar", 100, "bar", 0) - node = result.Node - - if err != nil || node.Key != "/foo" || node.Value != "foobar" || node.TTL != 100 { - if err != nil { - t.Fatal(err) - } - t.Fatalf("Set 3 failed with %s %s %v", node.Key, node.Value, node.TTL) - } - - // Next, we'll make sure we can't set it without the correct prior value - _, err = c.CompareAndSwap("foo", "foofoo", 100, "bar", 0) - - if err == nil { - t.Fatalf("Set 4 expecting error when setting key with incorrect previous value") - } -} diff --git a/tests/functional/util.go b/tests/functional/util.go index 135ee14e2cd..36a72c39a10 100644 --- a/tests/functional/util.go +++ b/tests/functional/util.go @@ -227,7 +227,7 @@ func Monitor(size int, allowDeadNum int, leaderChan chan string, all chan bool, func getLeader(addr string) (string, error) { - resp, err := client.Get(addr + "/v1/leader") + resp, err := client.Get(addr + "/v2/leader") if err != nil { return "", err diff --git a/tests/functional/v1_migration_test.go b/tests/functional/v1_migration_test.go deleted file mode 100644 index 75aaebb3fb9..00000000000 --- a/tests/functional/v1_migration_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package test - -import ( - "fmt" - "io/ioutil" - "net/http" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - "github.com/coreos/etcd/tests" - "github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert" -) - -// Ensure that we can start a v2 node from the log of a v1 node. -func TestV1SoloMigration(t *testing.T) { - path, _ := ioutil.TempDir("", "etcd-") - os.MkdirAll(path, 0777) - defer os.RemoveAll(path) - - nodepath := filepath.Join(path, "node0") - fixturepath, _ := filepath.Abs("../fixtures/v1.solo/node0") - fmt.Println("DATA_DIR =", nodepath) - - // Copy over fixture files. - c := exec.Command("cp", "-rf", fixturepath, nodepath) - if out, err := c.CombinedOutput(); err != nil { - fmt.Println(">>>>>>\n", string(out), "<<<<<<") - panic("Fixture initialization error:" + err.Error()) - } - - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - - args := []string{"etcd", fmt.Sprintf("-data-dir=%s", nodepath)} - args = append(args, "-addr", "127.0.0.1:4001") - args = append(args, "-peer-addr", "127.0.0.1:7001") - args = append(args, "-name", "node0") - process, err := os.StartProcess(EtcdBinPath, args, procAttr) - if err != nil { - t.Fatal("start process failed:" + err.Error()) - return - } - defer process.Kill() - time.Sleep(time.Second) - - // Ensure deleted message is removed. - resp, err := tests.Get("http://localhost:4001/v2/keys/message") - tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, resp.StatusCode, 200, "") -} - -// Ensure that we can start a v2 cluster from the logs of a v1 cluster. -func TestV1ClusterMigration(t *testing.T) { - path, _ := ioutil.TempDir("", "etcd-") - os.RemoveAll(path) - defer os.RemoveAll(path) - - nodes := []string{"node0", "node2"} - for i, node := range nodes { - nodepath := filepath.Join(path, node) - fixturepath, _ := filepath.Abs(filepath.Join("../fixtures/v1.cluster/", node)) - fmt.Println("FIXPATH =", fixturepath) - fmt.Println("NODEPATH =", nodepath) - os.MkdirAll(filepath.Dir(nodepath), 0777) - - // Copy over fixture files. - c := exec.Command("cp", "-rf", fixturepath, nodepath) - if out, err := c.CombinedOutput(); err != nil { - fmt.Println(">>>>>>\n", string(out), "<<<<<<") - panic("Fixture initialization error:" + err.Error()) - } - - procAttr := new(os.ProcAttr) - procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr} - - args := []string{"etcd", fmt.Sprintf("-data-dir=%s", nodepath)} - args = append(args, "-addr", fmt.Sprintf("127.0.0.1:%d", 4001+i)) - args = append(args, "-peer-addr", fmt.Sprintf("127.0.0.1:%d", 7001+i)) - args = append(args, "-name", node) - process, err := os.StartProcess(EtcdBinPath, args, procAttr) - if err != nil { - t.Fatal("start process failed:" + err.Error()) - return - } - defer process.Kill() - time.Sleep(time.Second) - } - - // Ensure deleted message is removed. - resp, err := tests.Get("http://localhost:4001/v2/keys/message") - body := tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, resp.StatusCode, http.StatusNotFound) - assert.Equal(t, string(body), `{"errorCode":100,"message":"Key not found","cause":"/message","index":11}`+"\n") - - // Ensure TTL'd message is removed. - resp, err = tests.Get("http://localhost:4001/v2/keys/foo") - body = tests.ReadBody(resp) - assert.Nil(t, err, "") - assert.Equal(t, resp.StatusCode, 200, "") - assert.Equal(t, string(body), `{"action":"get","node":{"key":"/foo","value":"one","modifiedIndex":9,"createdIndex":9}}`) -}