func TestCheckStreamSupport(t *testing.T) { tests := []struct { v *semver.Version t streamType w bool }{ // support { semver.Must(semver.NewVersion("2.1.0")), streamTypeMsgAppV2, true, }, // ignore patch { semver.Must(semver.NewVersion("2.1.9")), streamTypeMsgAppV2, true, }, // ignore prerelease { semver.Must(semver.NewVersion("2.1.0-alpha")), streamTypeMsgAppV2, true, }, } for i, tt := range tests { if g := checkStreamSupport(tt.v, tt.t); g != tt.w { t.Errorf("#%d: check = %v, want %v", i, g, tt.w) } } }
func TestServerVersion(t *testing.T) { tests := []struct { h http.Header wv *semver.Version }{ // backward compatibility with etcd 2.0 { http.Header{}, semver.Must(semver.NewVersion("2.0.0")), }, { http.Header{"X-Server-Version": []string{"2.1.0"}}, semver.Must(semver.NewVersion("2.1.0")), }, { http.Header{"X-Server-Version": []string{"2.1.0-alpha.0+git"}}, semver.Must(semver.NewVersion("2.1.0-alpha.0+git")), }, } for i, tt := range tests { v := serverVersion(tt.h) if v.String() != tt.wv.String() { t.Errorf("#%d: version = %s, want %s", i, v, tt.wv) } } }
func TestDecideClusterVersion(t *testing.T) { tests := []struct { vers map[string]*version.Versions wdver *semver.Version }{ { map[string]*version.Versions{"a": {Server: "2.0.0"}}, semver.Must(semver.NewVersion("2.0.0")), }, // unknown { map[string]*version.Versions{"a": nil}, nil, }, { map[string]*version.Versions{"a": {Server: "2.0.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}}, semver.Must(semver.NewVersion("2.0.0")), }, { map[string]*version.Versions{"a": {Server: "2.1.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}}, semver.Must(semver.NewVersion("2.1.0")), }, { map[string]*version.Versions{"a": nil, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}}, nil, }, } for i, tt := range tests { dver := decideClusterVersion(tt.vers) if !reflect.DeepEqual(dver, tt.wdver) { t.Errorf("#%d: ver = %+v, want %+v", i, dver, tt.wdver) } } }
// isCompatibleWithCluster return true if the local member has a compatible version with // the current running cluster. // The version is considered as compatible when at least one of the other members in the cluster has a // cluster version in the range of [MinClusterVersion, Version] and no known members has a cluster version // out of the range. // We set this rule since when the local member joins, another member might be offline. func isCompatibleWithCluster(cl Cluster, local types.ID, rt http.RoundTripper) bool { vers := getVersions(cl, local, rt) minV := semver.Must(semver.NewVersion(version.MinClusterVersion)) maxV := semver.Must(semver.NewVersion(version.Version)) maxV = &semver.Version{ Major: maxV.Major, Minor: maxV.Minor, } return isCompatibleWithVers(vers, local, minV, maxV) }
// checkVersionCompability checks whether the given version is compatible // with the local version. func checkVersionCompability(name string, server, minCluster *semver.Version) error { localServer := semver.Must(semver.NewVersion(version.Version)) localMinCluster := semver.Must(semver.NewVersion(version.MinClusterVersion)) if compareMajorMinorVersion(server, localMinCluster) == -1 { return fmt.Errorf("remote version is too low: remote[%s]=%s, local=%s", name, server, localServer) } if compareMajorMinorVersion(minCluster, localServer) == 1 { return fmt.Errorf("local version is too low: remote[%s]=%s, local=%s", name, server, localServer) } return nil }
// decideClusterVersion decides the cluster version based on the versions map. // The returned version is the min server version in the map, or nil if the min // version in unknown. func decideClusterVersion(vers map[string]*version.Versions) *semver.Version { var cv *semver.Version lv := semver.Must(semver.NewVersion(version.Version)) for mid, ver := range vers { if ver == nil { return nil } v, err := semver.NewVersion(ver.Server) if err != nil { plog.Errorf("cannot understand the version of member %s (%v)", mid, err) return nil } if lv.LessThan(*v) { plog.Warningf("the local etcd version %s is not up-to-date", lv.String()) plog.Warningf("member %s has a higher version %s", mid, ver.Server) } if cv == nil { cv = v } else if v.LessThan(*cv) { cv = v } } return cv }
// applyRequest interprets r as a call to store.X and returns a Response interpreted // from store.Event func (s *EtcdServer) applyRequest(r pb.Request) Response { f := func(ev *store.Event, err error) Response { return Response{Event: ev, err: err} } expr := timeutil.UnixNanoToTime(r.Expiration) refresh, _ := pbutil.GetBool(r.Refresh) ttlOptions := store.TTLOptionSet{ExpireTime: expr, Refresh: refresh} switch r.Method { case "POST": return f(s.store.Create(r.Path, r.Dir, r.Val, true, ttlOptions)) case "PUT": exists, existsSet := pbutil.GetBool(r.PrevExist) switch { case existsSet: if exists { if r.PrevIndex == 0 && r.PrevValue == "" { return f(s.store.Update(r.Path, r.Val, ttlOptions)) } else { return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) } } return f(s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions)) case r.PrevIndex > 0 || r.PrevValue != "": return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) default: // TODO (yicheng): cluster should be the owner of cluster prefix store // we should not modify cluster store here. if storeMemberAttributeRegexp.MatchString(r.Path) { id := mustParseMemberIDFromKey(path.Dir(r.Path)) var attr Attributes if err := json.Unmarshal([]byte(r.Val), &attr); err != nil { plog.Panicf("unmarshal %s should never fail: %v", r.Val, err) } ok := s.cluster.UpdateAttributes(id, attr) if !ok { return Response{} } } if r.Path == path.Join(StoreClusterPrefix, "version") { s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val))) } return f(s.store.Set(r.Path, r.Dir, r.Val, ttlOptions)) } case "DELETE": switch { case r.PrevIndex > 0 || r.PrevValue != "": return f(s.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex)) default: return f(s.store.Delete(r.Path, r.Dir, r.Recursive)) } case "QGET": return f(s.store.Get(r.Path, r.Recursive, r.Sorted)) case "SYNC": s.store.DeleteExpiredKeys(time.Unix(0, r.Time)) return Response{} default: // This should never be reached, but just in case: return Response{err: ErrUnknownMethod} } }
func (a *applierV2store) Put(r *pb.Request) Response { ttlOptions := toTTLOptions(r) exists, existsSet := pbutil.GetBool(r.PrevExist) switch { case existsSet: if exists { if r.PrevIndex == 0 && r.PrevValue == "" { return toResponse(a.s.store.Update(r.Path, r.Val, ttlOptions)) } else { return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) } } return toResponse(a.s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions)) case r.PrevIndex > 0 || r.PrevValue != "": return toResponse(a.s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) default: if storeMemberAttributeRegexp.MatchString(r.Path) { id := membership.MustParseMemberIDFromKey(path.Dir(r.Path)) var attr membership.Attributes if err := json.Unmarshal([]byte(r.Val), &attr); err != nil { plog.Panicf("unmarshal %s should never fail: %v", r.Val, err) } a.s.cluster.UpdateAttributes(id, attr) // return an empty response since there is no consumer. return Response{} } if r.Path == membership.StoreClusterVersionKey() { a.s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val))) // return an empty response since there is no consumer. return Response{} } return toResponse(a.s.store.Set(r.Path, r.Dir, r.Val, ttlOptions)) } }
func MustDetectDowngrade(cv *semver.Version) { lv := semver.Must(semver.NewVersion(version.Version)) // only keep major.minor version for comparison against cluster version lv = &semver.Version{Major: lv.Major, Minor: lv.Minor} if cv != nil && lv.LessThan(*cv) { plog.Fatalf("cluster cannot be downgraded (current version: %s is lower than determined cluster version: %s).", version.Version, version.Cluster(cv.String())) } }
func (c *RaftCluster) Version() *semver.Version { c.Lock() defer c.Unlock() if c.version == nil { return nil } return semver.Must(semver.NewVersion(c.version.String())) }
// serverVersion returns the min cluster version from the given header. func minClusterVersion(h http.Header) *semver.Version { verStr := h.Get("X-Min-Cluster-Version") // backward compatibility with etcd 2.0 if verStr == "" { verStr = "2.0.0" } return semver.Must(semver.NewVersion(verStr)) }
func TestCheckVersionCompatibility(t *testing.T) { ls := semver.Must(semver.NewVersion(version.Version)) lmc := semver.Must(semver.NewVersion(version.MinClusterVersion)) tests := []struct { server *semver.Version minCluster *semver.Version wok bool }{ // the same version as local { ls, lmc, true, }, // one version lower { lmc, &semver.Version{}, true, }, // one version higher { &semver.Version{Major: ls.Major + 1}, ls, true, }, // too low version { &semver.Version{Major: lmc.Major - 1}, &semver.Version{}, false, }, // too high version { &semver.Version{Major: ls.Major + 1, Minor: 1}, &semver.Version{Major: ls.Major + 1}, false, }, } for i, tt := range tests { err := checkVersionCompability("", tt.server, tt.minCluster) if ok := err == nil; ok != tt.wok { t.Errorf("#%d: ok = %v, want %v", i, ok, tt.wok) } } }
func clusterVersionFromStore(st store.Store) *semver.Version { e, err := st.Get(path.Join(storePrefix, "version"), false, false) if err != nil { if isKeyNotFound(err) { return nil } plog.Panicf("unexpected error (%v) when getting cluster version from store", err) } return semver.Must(semver.NewVersion(*e.Node.Value)) }
func TestCompareMajorMinorVersion(t *testing.T) { tests := []struct { va, vb *semver.Version w int }{ // equal to { semver.Must(semver.NewVersion("2.1.0")), semver.Must(semver.NewVersion("2.1.0")), 0, }, // smaller than { semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")), -1, }, // bigger than { semver.Must(semver.NewVersion("2.2.0")), semver.Must(semver.NewVersion("2.1.0")), 1, }, // ignore patch { semver.Must(semver.NewVersion("2.1.1")), semver.Must(semver.NewVersion("2.1.0")), 0, }, // ignore prerelease { semver.Must(semver.NewVersion("2.1.0-alpha.0")), semver.Must(semver.NewVersion("2.1.0")), 0, }, } for i, tt := range tests { if g := compareMajorMinorVersion(tt.va, tt.vb); g != tt.w { t.Errorf("#%d: compare = %d, want %d", i, g, tt.w) } } }
func TestIsCompatibleWithVers(t *testing.T) { tests := []struct { vers map[string]*version.Versions local types.ID minV, maxV *semver.Version wok bool }{ // too low { map[string]*version.Versions{ "a": {Server: "2.0.0", Cluster: "not_decided"}, "b": {Server: "2.1.0", Cluster: "2.1.0"}, "c": {Server: "2.1.0", Cluster: "2.1.0"}, }, 0xa, semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.0.0")), false, }, { map[string]*version.Versions{ "a": {Server: "2.1.0", Cluster: "not_decided"}, "b": {Server: "2.1.0", Cluster: "2.1.0"}, "c": {Server: "2.1.0", Cluster: "2.1.0"}, }, 0xa, semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")), true, }, // too high { map[string]*version.Versions{ "a": {Server: "2.2.0", Cluster: "not_decided"}, "b": {Server: "2.0.0", Cluster: "2.0.0"}, "c": {Server: "2.0.0", Cluster: "2.0.0"}, }, 0xa, semver.Must(semver.NewVersion("2.1.0")), semver.Must(semver.NewVersion("2.2.0")), false, }, // cannot get b's version, expect ok { map[string]*version.Versions{ "a": {Server: "2.1.0", Cluster: "not_decided"}, "b": nil, "c": {Server: "2.1.0", Cluster: "2.1.0"}, }, 0xa, semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")), true, }, // cannot get b and c's version, expect not ok { map[string]*version.Versions{ "a": {Server: "2.1.0", Cluster: "not_decided"}, "b": nil, "c": nil, }, 0xa, semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")), false, }, } for i, tt := range tests { ok := isCompatibleWithVers(tt.vers, tt.local, tt.minV, tt.maxV) if ok != tt.wok { t.Errorf("#%d: ok = %+v, want %+v", i, ok, tt.wok) } } }
func (cr *streamReader) dial(t streamType) (io.ReadCloser, error) { u := cr.picker.pick() uu := u uu.Path = path.Join(t.endpoint(), cr.local.String()) req, err := http.NewRequest("GET", uu.String(), nil) if err != nil { cr.picker.unreachable(u) return nil, fmt.Errorf("failed to make http request to %s (%v)", u, err) } req.Header.Set("X-Server-From", cr.local.String()) req.Header.Set("X-Server-Version", version.Version) req.Header.Set("X-Min-Cluster-Version", version.MinClusterVersion) req.Header.Set("X-Etcd-Cluster-ID", cr.cid.String()) req.Header.Set("X-Raft-To", cr.remote.String()) setPeerURLsHeader(req, cr.tr.URLs) cr.mu.Lock() select { case <-cr.stopc: cr.mu.Unlock() return nil, fmt.Errorf("stream reader is stopped") default: } cr.cancel = httputil.RequestCanceler(cr.tr.streamRt, req) cr.mu.Unlock() resp, err := cr.tr.streamRt.RoundTrip(req) if err != nil { cr.picker.unreachable(u) return nil, err } rv := serverVersion(resp.Header) lv := semver.Must(semver.NewVersion(version.Version)) if compareMajorMinorVersion(rv, lv) == -1 && !checkStreamSupport(rv, t) { httputil.GracefulClose(resp) cr.picker.unreachable(u) return nil, errUnsupportedStreamType } switch resp.StatusCode { case http.StatusGone: httputil.GracefulClose(resp) cr.picker.unreachable(u) err := fmt.Errorf("the member has been permanently removed from the cluster") select { case cr.errorc <- err: default: } return nil, err case http.StatusOK: return resp.Body, nil case http.StatusNotFound: httputil.GracefulClose(resp) cr.picker.unreachable(u) return nil, fmt.Errorf("remote member %s could not recognize local member", cr.remote) case http.StatusPreconditionFailed: b, err := ioutil.ReadAll(resp.Body) if err != nil { cr.picker.unreachable(u) return nil, err } httputil.GracefulClose(resp) cr.picker.unreachable(u) switch strings.TrimSuffix(string(b), "\n") { case errIncompatibleVersion.Error(): plog.Errorf("request sent was ignored by peer %s (server version incompatible)", cr.remote) return nil, errIncompatibleVersion case errClusterIDMismatch.Error(): plog.Errorf("request sent was ignored (cluster ID mismatch: remote[%s]=%s, local=%s)", cr.remote, resp.Header.Get("X-Etcd-Cluster-ID"), cr.cid) return nil, errClusterIDMismatch default: return nil, fmt.Errorf("unhandled error %q when precondition failed", string(b)) } default: httputil.GracefulClose(resp) cr.picker.unreachable(u) return nil, fmt.Errorf("unhandled http status %d", resp.StatusCode) } }
// applyRequest interprets r as a call to store.X and returns a Response interpreted // from store.Event func (s *EtcdServer) applyRequest(r pb.Request) Response { f := func(ev *store.Event, err error) Response { return Response{Event: ev, err: err} } refresh, _ := pbutil.GetBool(r.Refresh) ttlOptions := store.TTLOptionSet{Refresh: refresh} if r.Expiration != 0 { ttlOptions.ExpireTime = time.Unix(0, r.Expiration) } switch r.Method { case "POST": return f(s.store.Create(r.Path, r.Dir, r.Val, true, ttlOptions)) case "PUT": exists, existsSet := pbutil.GetBool(r.PrevExist) switch { case existsSet: if exists { if r.PrevIndex == 0 && r.PrevValue == "" { return f(s.store.Update(r.Path, r.Val, ttlOptions)) } else { return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) } } return f(s.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions)) case r.PrevIndex > 0 || r.PrevValue != "": return f(s.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions)) default: if storeMemberAttributeRegexp.MatchString(r.Path) { id := membership.MustParseMemberIDFromKey(path.Dir(r.Path)) var attr membership.Attributes if err := json.Unmarshal([]byte(r.Val), &attr); err != nil { plog.Panicf("unmarshal %s should never fail: %v", r.Val, err) } s.cluster.UpdateAttributes(id, attr) // return an empty response since there is no consumer. return Response{} } if r.Path == membership.StoreClusterVersionKey() { s.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val))) // return an empty response since there is no consumer. return Response{} } return f(s.store.Set(r.Path, r.Dir, r.Val, ttlOptions)) } case "DELETE": switch { case r.PrevIndex > 0 || r.PrevValue != "": return f(s.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex)) default: return f(s.store.Delete(r.Path, r.Dir, r.Recursive)) } case "QGET": return f(s.store.Get(r.Path, r.Recursive, r.Sorted)) case "SYNC": s.store.DeleteExpiredKeys(time.Unix(0, r.Time)) return Response{} default: // This should never be reached, but just in case: return Response{err: ErrUnknownMethod} } }