From 0d62c997773d38f348bc05b6def59484644387cc Mon Sep 17 00:00:00 2001 From: Siyuan Zhang Date: Thu, 18 Apr 2024 18:44:15 +0000 Subject: [PATCH] Allow updateClusterVersion when downgrading from 3.5. Signed-off-by: Siyuan Zhang --- etcdserver/api/membership/cluster.go | 25 ++++++++ etcdserver/api/membership/cluster_test.go | 78 +++++++++++++++++++++++ etcdserver/server.go | 4 +- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/etcdserver/api/membership/cluster.go b/etcdserver/api/membership/cluster.go index 788217f0cbc..144da586141 100644 --- a/etcdserver/api/membership/cluster.go +++ b/etcdserver/api/membership/cluster.go @@ -789,6 +789,15 @@ func ValidateClusterAndAssignIDs(lg *zap.Logger, local *RaftCluster, existing *R return nil } +// isValidDowngrade verifies whether the cluster can be downgraded from verFrom to verTo +func isValidDowngrade(verFrom *semver.Version, verTo *semver.Version) bool { + allowedDowngradeVersion := semver.Version{ + Major: verFrom.Major, + Minor: verFrom.Minor - 1, + } + return verTo.Equal(allowedDowngradeVersion) +} + func mustDetectDowngrade(lg *zap.Logger, cv *semver.Version, nextClusterVersionCompatible bool) { err := detectDowngrade(cv, nextClusterVersionCompatible) if err != nil { @@ -825,6 +834,22 @@ func detectDowngrade(cv *semver.Version, nextClusterVersionCompatible bool) erro return nil } +// IsValidClusterVersionChange checks the two scenario when version is valid to change: +// 1. Downgrade: cluster version is 1 minor version higher than local version, +// cluster version should change. +// 2. Cluster start: when not all members version are available, cluster version +// is set to MinVersion(3.0), when all members are at higher version, cluster version +// is lower than minimal server version, cluster version should change +func IsValidClusterVersionChange(verFrom *semver.Version, verTo *semver.Version, nextClusterVersionCompatible bool) bool { + verFrom = &semver.Version{Major: verFrom.Major, Minor: verFrom.Minor} + verTo = &semver.Version{Major: verTo.Major, Minor: verTo.Minor} + + if (nextClusterVersionCompatible && isValidDowngrade(verFrom, verTo)) || (verFrom.Major == verTo.Major && verFrom.LessThan(*verTo)) { + return true + } + return false +} + // IsLocalMemberLearner returns if the local member is raft learner func (c *RaftCluster) IsLocalMemberLearner() bool { c.Lock() diff --git a/etcdserver/api/membership/cluster_test.go b/etcdserver/api/membership/cluster_test.go index 0589f74453e..73c1ce9490f 100644 --- a/etcdserver/api/membership/cluster_test.go +++ b/etcdserver/api/membership/cluster_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/coreos/go-semver/semver" + "github.com/stretchr/testify/assert" "go.etcd.io/etcd/etcdserver/api/v2store" "go.etcd.io/etcd/pkg/mock/mockstore" "go.etcd.io/etcd/pkg/testutil" @@ -948,6 +949,83 @@ func TestIsReadyToPromoteMember(t *testing.T) { } } +func TestIsVersionChangable(t *testing.T) { + tests := []struct { + name string + verFrom string + verTo string + nextClusterVersionCompatible bool + expectedResult bool + }{ + { + name: "When local version is one minor lower than cluster version in downgrade mode", + verFrom: "3.5.0", + verTo: "3.4.0", + nextClusterVersionCompatible: true, + expectedResult: true, + }, + { + name: "When local version is one minor lower than cluster version not in downgrade mode", + verFrom: "3.5.0", + verTo: "3.4.0", + expectedResult: false, + }, + { + name: "When local version is one minor and one patch lower than cluster version in downgrade mode", + verFrom: "3.5.2", + verTo: "3.4.1", + nextClusterVersionCompatible: true, + expectedResult: true, + }, + { + name: "When local version is one minor higher than cluster version", + verFrom: "3.3.0", + verTo: "3.4.0", + expectedResult: true, + }, + { + name: "When local version is two minor higher than cluster version", + verFrom: "3.2.0", + verTo: "3.4.0", + expectedResult: true, + }, + { + name: "When local version is one major higher than cluster version", + verFrom: "2.4.0", + verTo: "3.4.0", + expectedResult: false, + }, + { + name: "When local version is equal to cluster version", + verFrom: "3.4.0", + verTo: "3.4.0", + expectedResult: false, + }, + { + name: "When local version is one patch higher than cluster version", + verFrom: "3.4.0", + verTo: "3.4.1", + expectedResult: false, + }, + { + name: "When local version is two minor lower than cluster version", + verFrom: "3.6.0", + verTo: "3.4.0", + nextClusterVersionCompatible: true, + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + verFrom := semver.Must(semver.NewVersion(tt.verFrom)) + verTo := semver.Must(semver.NewVersion(tt.verTo)) + ret := IsValidClusterVersionChange(verFrom, verTo, tt.nextClusterVersionCompatible) + assert.Equal(t, tt.expectedResult, ret) + }) + } +} + func TestDetectDowngrade(t *testing.T) { tests := []struct { clusterVersion string diff --git a/etcdserver/server.go b/etcdserver/server.go index 0d4de02e0da..2622409afb0 100644 --- a/etcdserver/server.go +++ b/etcdserver/server.go @@ -2636,8 +2636,8 @@ func (s *EtcdServer) monitorVersions() { } // update cluster version only if the decided version is greater than - // the current cluster version - if v != nil && s.cluster.Version().LessThan(*v) { + // the current cluster version or it is a valid downgrade + if v != nil && membership.IsValidClusterVersionChange(s.cluster.Version(), v, s.Config().NextClusterVersionCompatible) { s.goAttach(func() { s.updateClusterVersion(v.String()) }) } }