|
@@ -25,9 +25,11 @@ import (
|
|
|
"sync"
|
|
"sync"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
|
|
|
+ "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-semver/semver"
|
|
|
"github.com/coreos/etcd/etcdserver/stats"
|
|
"github.com/coreos/etcd/etcdserver/stats"
|
|
|
"github.com/coreos/etcd/pkg/types"
|
|
"github.com/coreos/etcd/pkg/types"
|
|
|
"github.com/coreos/etcd/raft/raftpb"
|
|
"github.com/coreos/etcd/raft/raftpb"
|
|
|
|
|
+ "github.com/coreos/etcd/version"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
const (
|
|
@@ -38,6 +40,16 @@ const (
|
|
|
streamBufSize = 4096
|
|
streamBufSize = 4096
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+var (
|
|
|
|
|
+ errUnsupportedStreamType = fmt.Errorf("unsupported stream type")
|
|
|
|
|
+
|
|
|
|
|
+ // the key is in string format "major.minor.patch"
|
|
|
|
|
+ supportedStream = map[string][]streamType{
|
|
|
|
|
+ "2.0.0": []streamType{streamTypeMsgApp},
|
|
|
|
|
+ "2.1.0": []streamType{streamTypeMsgApp, streamTypeMsgAppV2, streamTypeMessage},
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
type streamType string
|
|
type streamType string
|
|
|
|
|
|
|
|
func (t streamType) endpoint() string {
|
|
func (t streamType) endpoint() string {
|
|
@@ -256,13 +268,30 @@ func startStreamReader(tr http.RoundTripper, picker *urlPicker, t streamType, fr
|
|
|
|
|
|
|
|
func (cr *streamReader) run() {
|
|
func (cr *streamReader) run() {
|
|
|
for {
|
|
for {
|
|
|
- rc, err := cr.dial()
|
|
|
|
|
|
|
+ t := cr.t
|
|
|
|
|
+ rc, err := cr.dial(t)
|
|
|
|
|
+ // downgrade to streamTypeMsgApp if the remote doesn't support
|
|
|
|
|
+ // streamTypeMsgAppV2
|
|
|
|
|
+ if t == streamTypeMsgAppV2 && err == errUnsupportedStreamType {
|
|
|
|
|
+ t = streamTypeMsgApp
|
|
|
|
|
+ rc, err = cr.dial(t)
|
|
|
|
|
+ }
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- log.Printf("rafthttp: roundtripping error: %v", err)
|
|
|
|
|
|
|
+ if err != errUnsupportedStreamType {
|
|
|
|
|
+ log.Printf("rafthttp: roundtripping error: %v", err)
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- err := cr.decodeLoop(rc)
|
|
|
|
|
- if err != io.EOF && !isClosedConnectionError(err) {
|
|
|
|
|
- log.Printf("rafthttp: failed to read message on stream %s due to %v", cr.t, err)
|
|
|
|
|
|
|
+ err := cr.decodeLoop(rc, t)
|
|
|
|
|
+ switch {
|
|
|
|
|
+ // all data is read out
|
|
|
|
|
+ case err == io.EOF:
|
|
|
|
|
+ // connection is closed by the remote
|
|
|
|
|
+ case isClosedConnectionError(err):
|
|
|
|
|
+ // stream msgapp is only used for etcd 2.0, and etcd 2.0 doesn't
|
|
|
|
|
+ // heartbeat on the idle stream, so it is expected to time out.
|
|
|
|
|
+ case t == streamTypeMsgApp && isNetworkTimeoutError(err):
|
|
|
|
|
+ default:
|
|
|
|
|
+ log.Printf("rafthttp: failed to read message on stream %s due to %v", t, err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
select {
|
|
select {
|
|
@@ -276,10 +305,10 @@ func (cr *streamReader) run() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (cr *streamReader) decodeLoop(rc io.ReadCloser) error {
|
|
|
|
|
|
|
+func (cr *streamReader) decodeLoop(rc io.ReadCloser, t streamType) error {
|
|
|
var dec decoder
|
|
var dec decoder
|
|
|
cr.mu.Lock()
|
|
cr.mu.Lock()
|
|
|
- switch cr.t {
|
|
|
|
|
|
|
+ switch t {
|
|
|
case streamTypeMsgApp:
|
|
case streamTypeMsgApp:
|
|
|
dec = &msgAppDecoder{r: rc, local: cr.from, remote: cr.to, term: cr.msgAppTerm}
|
|
dec = &msgAppDecoder{r: rc, local: cr.from, remote: cr.to, term: cr.msgAppTerm}
|
|
|
case streamTypeMsgAppV2:
|
|
case streamTypeMsgAppV2:
|
|
@@ -287,7 +316,7 @@ func (cr *streamReader) decodeLoop(rc io.ReadCloser) error {
|
|
|
case streamTypeMessage:
|
|
case streamTypeMessage:
|
|
|
dec = &messageDecoder{r: rc}
|
|
dec = &messageDecoder{r: rc}
|
|
|
default:
|
|
default:
|
|
|
- log.Panicf("rafthttp: unhandled stream type %s", cr.t)
|
|
|
|
|
|
|
+ log.Panicf("rafthttp: unhandled stream type %s", t)
|
|
|
}
|
|
}
|
|
|
cr.closer = rc
|
|
cr.closer = rc
|
|
|
cr.mu.Unlock()
|
|
cr.mu.Unlock()
|
|
@@ -347,14 +376,14 @@ func (cr *streamReader) isWorking() bool {
|
|
|
return cr.closer != nil
|
|
return cr.closer != nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (cr *streamReader) dial() (io.ReadCloser, error) {
|
|
|
|
|
|
|
+func (cr *streamReader) dial(t streamType) (io.ReadCloser, error) {
|
|
|
u := cr.picker.pick()
|
|
u := cr.picker.pick()
|
|
|
cr.mu.Lock()
|
|
cr.mu.Lock()
|
|
|
term := cr.msgAppTerm
|
|
term := cr.msgAppTerm
|
|
|
cr.mu.Unlock()
|
|
cr.mu.Unlock()
|
|
|
|
|
|
|
|
uu := u
|
|
uu := u
|
|
|
- uu.Path = path.Join(cr.t.endpoint(), cr.from.String())
|
|
|
|
|
|
|
+ uu.Path = path.Join(t.endpoint(), cr.from.String())
|
|
|
req, err := http.NewRequest("GET", uu.String(), nil)
|
|
req, err := http.NewRequest("GET", uu.String(), nil)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
cr.picker.unreachable(u)
|
|
cr.picker.unreachable(u)
|
|
@@ -362,7 +391,7 @@ func (cr *streamReader) dial() (io.ReadCloser, error) {
|
|
|
}
|
|
}
|
|
|
req.Header.Set("X-Etcd-Cluster-ID", cr.cid.String())
|
|
req.Header.Set("X-Etcd-Cluster-ID", cr.cid.String())
|
|
|
req.Header.Set("X-Raft-To", cr.to.String())
|
|
req.Header.Set("X-Raft-To", cr.to.String())
|
|
|
- if cr.t == streamTypeMsgApp {
|
|
|
|
|
|
|
+ if t == streamTypeMsgApp {
|
|
|
req.Header.Set("X-Raft-Term", strconv.FormatUint(term, 10))
|
|
req.Header.Set("X-Raft-Term", strconv.FormatUint(term, 10))
|
|
|
}
|
|
}
|
|
|
cr.mu.Lock()
|
|
cr.mu.Lock()
|
|
@@ -373,6 +402,14 @@ func (cr *streamReader) dial() (io.ReadCloser, error) {
|
|
|
cr.picker.unreachable(u)
|
|
cr.picker.unreachable(u)
|
|
|
return nil, fmt.Errorf("error roundtripping to %s: %v", req.URL, err)
|
|
return nil, fmt.Errorf("error roundtripping to %s: %v", req.URL, err)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ rv := serverVersion(resp.Header)
|
|
|
|
|
+ lv := semver.Must(semver.NewVersion(version.Version))
|
|
|
|
|
+ if compareMajorMinorVersion(rv, lv) == -1 && !checkStreamSupport(rv, t) {
|
|
|
|
|
+ resp.Body.Close()
|
|
|
|
|
+ return nil, errUnsupportedStreamType
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
switch resp.StatusCode {
|
|
switch resp.StatusCode {
|
|
|
case http.StatusGone:
|
|
case http.StatusGone:
|
|
|
resp.Body.Close()
|
|
resp.Body.Close()
|
|
@@ -384,6 +421,9 @@ func (cr *streamReader) dial() (io.ReadCloser, error) {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
case http.StatusOK:
|
|
case http.StatusOK:
|
|
|
return resp.Body, nil
|
|
return resp.Body, nil
|
|
|
|
|
+ case http.StatusNotFound:
|
|
|
|
|
+ resp.Body.Close()
|
|
|
|
|
+ return nil, fmt.Errorf("local member has not been added to the peer list of member %s", cr.to)
|
|
|
default:
|
|
default:
|
|
|
resp.Body.Close()
|
|
resp.Body.Close()
|
|
|
return nil, fmt.Errorf("unhandled http status %d", resp.StatusCode)
|
|
return nil, fmt.Errorf("unhandled http status %d", resp.StatusCode)
|
|
@@ -411,3 +451,46 @@ func isClosedConnectionError(err error) bool {
|
|
|
operr, ok := err.(*net.OpError)
|
|
operr, ok := err.(*net.OpError)
|
|
|
return ok && operr.Err.Error() == "use of closed network connection"
|
|
return ok && operr.Err.Error() == "use of closed network connection"
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// serverVersion returns the version from the given header.
|
|
|
|
|
+func serverVersion(h http.Header) *semver.Version {
|
|
|
|
|
+ verStr := h.Get("X-Server-Version")
|
|
|
|
|
+ // backward compatibility with etcd 2.0
|
|
|
|
|
+ if verStr == "" {
|
|
|
|
|
+ verStr = "2.0.0"
|
|
|
|
|
+ }
|
|
|
|
|
+ return semver.Must(semver.NewVersion(verStr))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// compareMajorMinorVersion returns an integer comparing two versions based on
|
|
|
|
|
+// their major and minor version. The result will be 0 if a==b, -1 if a < b,
|
|
|
|
|
+// and 1 if a > b.
|
|
|
|
|
+func compareMajorMinorVersion(a, b *semver.Version) int {
|
|
|
|
|
+ na := &semver.Version{Major: a.Major, Minor: a.Minor}
|
|
|
|
|
+ nb := &semver.Version{Major: b.Major, Minor: b.Minor}
|
|
|
|
|
+ switch {
|
|
|
|
|
+ case na.LessThan(*nb):
|
|
|
|
|
+ return -1
|
|
|
|
|
+ case nb.LessThan(*na):
|
|
|
|
|
+ return 1
|
|
|
|
|
+ default:
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// checkStreamSupport checks whether the stream type is supported in the
|
|
|
|
|
+// given version.
|
|
|
|
|
+func checkStreamSupport(v *semver.Version, t streamType) bool {
|
|
|
|
|
+ nv := &semver.Version{Major: v.Major, Minor: v.Minor}
|
|
|
|
|
+ for _, s := range supportedStream[nv.String()] {
|
|
|
|
|
+ if s == t {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func isNetworkTimeoutError(err error) bool {
|
|
|
|
|
+ nerr, ok := err.(net.Error)
|
|
|
|
|
+ return ok && nerr.Timeout()
|
|
|
|
|
+}
|