Browse Source

etcdserver: support MemberAdd for learner

Added IsLearner field to etcdserver internal Member type. Routed
learner MemberAdd request from server API to raft. Apply learner
MemberAdd result to server after the request is passed through Raft.
Jingyi Hu 7 năm trước cách đây
mục cha
commit
604bc04f70

+ 4 - 2
etcdserver/api/membership/cluster.go

@@ -59,10 +59,12 @@ type RaftCluster struct {
 	removed map[types.ID]bool
 	removed map[types.ID]bool
 }
 }
 
 
+// NewClusterFromURLsMap creates a new raft cluster using provided urls map. Currently, it does not support creating
+// cluster with raft learner member.
 func NewClusterFromURLsMap(lg *zap.Logger, token string, urlsmap types.URLsMap) (*RaftCluster, error) {
 func NewClusterFromURLsMap(lg *zap.Logger, token string, urlsmap types.URLsMap) (*RaftCluster, error) {
 	c := NewCluster(lg, token)
 	c := NewCluster(lg, token)
 	for name, urls := range urlsmap {
 	for name, urls := range urlsmap {
-		m := NewMember(name, urls, token, nil)
+		m := NewMember(name, urls, token, nil, false)
 		if _, ok := c.members[m.ID]; ok {
 		if _, ok := c.members[m.ID]; ok {
 			return nil, fmt.Errorf("member exists with identical ID %v", m)
 			return nil, fmt.Errorf("member exists with identical ID %v", m)
 		}
 		}
@@ -259,7 +261,7 @@ func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange) error {
 		return ErrIDRemoved
 		return ErrIDRemoved
 	}
 	}
 	switch cc.Type {
 	switch cc.Type {
-	case raftpb.ConfChangeAddNode:
+	case raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode:
 		if members[id] != nil {
 		if members[id] != nil {
 			return ErrIDExists
 			return ErrIDExists
 		}
 		}

+ 23 - 0
etcdserver/api/membership/cluster_test.go

@@ -472,6 +472,29 @@ func TestClusterAddMember(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestClusterAddMemberAsLearner(t *testing.T) {
+	st := mockstore.NewRecorder()
+	c := newTestCluster(nil)
+	c.SetStore(st)
+	c.AddMember(newTestMemberAsLearner(1, nil, "node1", nil))
+
+	wactions := []testutil.Action{
+		{
+			Name: "Create",
+			Params: []interface{}{
+				path.Join(StoreMembersPrefix, "1", "raftAttributes"),
+				false,
+				`{"peerURLs":null,"isLearner":true}`,
+				false,
+				v2store.TTLOptionSet{ExpireTime: v2store.Permanent},
+			},
+		},
+	}
+	if g := st.Action(); !reflect.DeepEqual(g, wactions) {
+		t.Errorf("actions = %v, want %v", g, wactions)
+	}
+}
+
 func TestClusterMembers(t *testing.T) {
 func TestClusterMembers(t *testing.T) {
 	cls := &RaftCluster{
 	cls := &RaftCluster{
 		members: map[types.ID]*Member{
 		members: map[types.ID]*Member{

+ 11 - 3
etcdserver/api/membership/member.go

@@ -35,6 +35,8 @@ type RaftAttributes struct {
 	// PeerURLs is the list of peers in the raft cluster.
 	// PeerURLs is the list of peers in the raft cluster.
 	// TODO(philips): ensure these are URLs
 	// TODO(philips): ensure these are URLs
 	PeerURLs []string `json:"peerURLs"`
 	PeerURLs []string `json:"peerURLs"`
+	// IsLearner indicates if the member is raft learner.
+	IsLearner bool `json:"isLearner,omitempty"`
 }
 }
 
 
 // Attributes represents all the non-raft related attributes of an etcd member.
 // Attributes represents all the non-raft related attributes of an etcd member.
@@ -51,10 +53,13 @@ type Member struct {
 
 
 // NewMember creates a Member without an ID and generates one based on the
 // NewMember creates a Member without an ID and generates one based on the
 // cluster name, peer URLs, and time. This is used for bootstrapping/adding new member.
 // cluster name, peer URLs, and time. This is used for bootstrapping/adding new member.
-func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.Time) *Member {
+func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.Time, isLearner bool) *Member {
 	m := &Member{
 	m := &Member{
-		RaftAttributes: RaftAttributes{PeerURLs: peerURLs.StringSlice()},
-		Attributes:     Attributes{Name: name},
+		RaftAttributes: RaftAttributes{
+			PeerURLs:  peerURLs.StringSlice(),
+			IsLearner: isLearner,
+		},
+		Attributes: Attributes{Name: name},
 	}
 	}
 
 
 	var b []byte
 	var b []byte
@@ -88,6 +93,9 @@ func (m *Member) Clone() *Member {
 	}
 	}
 	mm := &Member{
 	mm := &Member{
 		ID: m.ID,
 		ID: m.ID,
+		RaftAttributes: RaftAttributes{
+			IsLearner: m.IsLearner,
+		},
 		Attributes: Attributes{
 		Attributes: Attributes{
 			Name: m.Name,
 			Name: m.Name,
 		},
 		},

+ 15 - 7
etcdserver/api/membership/member_test.go

@@ -36,17 +36,17 @@ func TestMemberTime(t *testing.T) {
 		mem *Member
 		mem *Member
 		id  types.ID
 		id  types.ID
 	}{
 	}{
-		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil), 14544069596553697298},
+		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil, false), 14544069596553697298},
 		// Same ID, different name (names shouldn't matter)
 		// Same ID, different name (names shouldn't matter)
-		{NewMember("memfoo", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil), 14544069596553697298},
+		{NewMember("memfoo", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil, false), 14544069596553697298},
 		// Same ID, different Time
 		// Same ID, different Time
-		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", timeParse("1984-12-23T15:04:05Z")), 2448790162483548276},
+		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", timeParse("1984-12-23T15:04:05Z"), false), 2448790162483548276},
 		// Different cluster name
 		// Different cluster name
-		{NewMember("mcm1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "etcd", timeParse("1984-12-23T15:04:05Z")), 6973882743191604649},
-		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}}, "", timeParse("1984-12-23T15:04:05Z")), 1466075294948436910},
+		{NewMember("mcm1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "etcd", timeParse("1984-12-23T15:04:05Z"), false), 6973882743191604649},
+		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}}, "", timeParse("1984-12-23T15:04:05Z"), false), 1466075294948436910},
 		// Order shouldn't matter
 		// Order shouldn't matter
-		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}, {Scheme: "http", Host: "10.0.0.2:2379"}}, "", nil), 16552244735972308939},
-		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.2:2379"}, {Scheme: "http", Host: "10.0.0.1:2379"}}, "", nil), 16552244735972308939},
+		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}, {Scheme: "http", Host: "10.0.0.2:2379"}}, "", nil, false), 16552244735972308939},
+		{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.2:2379"}, {Scheme: "http", Host: "10.0.0.1:2379"}}, "", nil, false), 16552244735972308939},
 	}
 	}
 	for i, tt := range tests {
 	for i, tt := range tests {
 		if tt.mem.ID != tt.id {
 		if tt.mem.ID != tt.id {
@@ -113,3 +113,11 @@ func newTestMember(id uint64, peerURLs []string, name string, clientURLs []strin
 		Attributes:     Attributes{Name: name, ClientURLs: clientURLs},
 		Attributes:     Attributes{Name: name, ClientURLs: clientURLs},
 	}
 	}
 }
 }
+
+func newTestMemberAsLearner(id uint64, peerURLs []string, name string, clientURLs []string) *Member {
+	return &Member{
+		ID:             types.ID(id),
+		RaftAttributes: RaftAttributes{PeerURLs: peerURLs, IsLearner: true},
+		Attributes:     Attributes{Name: name, ClientURLs: clientURLs},
+	}
+}

+ 1 - 1
etcdserver/api/v2http/client.go

@@ -238,7 +238,7 @@ func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 		now := h.clock.Now()
 		now := h.clock.Now()
-		m := membership.NewMember("", req.PeerURLs, "", &now)
+		m := membership.NewMember("", req.PeerURLs, "", &now, false) // does not support adding learner via v2http
 		_, err := h.server.AddMember(ctx, *m)
 		_, err := h.server.AddMember(ctx, *m)
 		switch {
 		switch {
 		case err == membership.ErrIDExists || err == membership.ErrPeerURLexists:
 		case err == membership.ErrIDExists || err == membership.ErrPeerURLexists:

+ 8 - 3
etcdserver/api/v3rpc/member.go

@@ -46,15 +46,19 @@ func (cs *ClusterServer) MemberAdd(ctx context.Context, r *pb.MemberAddRequest)
 	}
 	}
 
 
 	now := time.Now()
 	now := time.Now()
-	m := membership.NewMember("", urls, "", &now)
+	m := membership.NewMember("", urls, "", &now, r.IsLearner)
 	membs, merr := cs.server.AddMember(ctx, *m)
 	membs, merr := cs.server.AddMember(ctx, *m)
 	if merr != nil {
 	if merr != nil {
 		return nil, togRPCError(merr)
 		return nil, togRPCError(merr)
 	}
 	}
 
 
 	return &pb.MemberAddResponse{
 	return &pb.MemberAddResponse{
-		Header:  cs.header(),
-		Member:  &pb.Member{ID: uint64(m.ID), PeerURLs: m.PeerURLs},
+		Header: cs.header(),
+		Member: &pb.Member{
+			ID:        uint64(m.ID),
+			PeerURLs:  m.PeerURLs,
+			IsLearner: m.IsLearner,
+		},
 		Members: membersToProtoMembers(membs),
 		Members: membersToProtoMembers(membs),
 	}, nil
 	}, nil
 }
 }
@@ -101,6 +105,7 @@ func membersToProtoMembers(membs []*membership.Member) []*pb.Member {
 			ID:         uint64(membs[i].ID),
 			ID:         uint64(membs[i].ID),
 			PeerURLs:   membs[i].PeerURLs,
 			PeerURLs:   membs[i].PeerURLs,
 			ClientURLs: membs[i].ClientURLs,
 			ClientURLs: membs[i].ClientURLs,
+			IsLearner:  membs[i].IsLearner,
 		}
 		}
 	}
 	}
 	return protoMembs
 	return protoMembs

+ 7 - 1
etcdserver/server.go

@@ -1544,6 +1544,7 @@ func (s *EtcdServer) AddMember(ctx context.Context, memb membership.Member) ([]*
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	// TODO: might switch to less strict check when adding raft learner
 	if s.Cfg.StrictReconfigCheck {
 	if s.Cfg.StrictReconfigCheck {
 		// by default StrictReconfigCheck is enabled; reject new members if unhealthy
 		// by default StrictReconfigCheck is enabled; reject new members if unhealthy
 		if !s.cluster.IsReadyToAddNewMember() {
 		if !s.cluster.IsReadyToAddNewMember() {
@@ -1585,6 +1586,11 @@ func (s *EtcdServer) AddMember(ctx context.Context, memb membership.Member) ([]*
 		NodeID:  uint64(memb.ID),
 		NodeID:  uint64(memb.ID),
 		Context: b,
 		Context: b,
 	}
 	}
+
+	if memb.IsLearner {
+		cc.Type = raftpb.ConfChangeAddLearnerNode
+	}
+
 	return s.configure(ctx, cc)
 	return s.configure(ctx, cc)
 }
 }
 
 
@@ -2054,7 +2060,7 @@ func (s *EtcdServer) applyConfChange(cc raftpb.ConfChange, confState *raftpb.Con
 	lg := s.getLogger()
 	lg := s.getLogger()
 	*confState = *s.r.ApplyConfChange(cc)
 	*confState = *s.r.ApplyConfChange(cc)
 	switch cc.Type {
 	switch cc.Type {
-	case raftpb.ConfChangeAddNode:
+	case raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode:
 		m := new(membership.Member)
 		m := new(membership.Member)
 		if err := json.Unmarshal(cc.Context, m); err != nil {
 		if err := json.Unmarshal(cc.Context, m); err != nil {
 			if lg != nil {
 			if lg != nil {

+ 8 - 8
etcdserver/server_test.go

@@ -634,7 +634,7 @@ func TestApplyConfigChangeUpdatesConsistIndex(t *testing.T) {
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	m := membership.NewMember("", urls, "", &now)
+	m := membership.NewMember("", urls, "", &now, false)
 	m.ID = types.ID(2)
 	m.ID = types.ID(2)
 	b, err := json.Marshal(m)
 	b, err := json.Marshal(m)
 	if err != nil {
 	if err != nil {
@@ -1564,23 +1564,23 @@ func TestGetOtherPeerURLs(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			[]*membership.Member{
 			[]*membership.Member{
-				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil),
+				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil, false),
 			},
 			},
 			[]string{},
 			[]string{},
 		},
 		},
 		{
 		{
 			[]*membership.Member{
 			[]*membership.Member{
-				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil),
-				membership.NewMember("2", types.MustNewURLs([]string{"http://10.0.0.2:2"}), "a", nil),
-				membership.NewMember("3", types.MustNewURLs([]string{"http://10.0.0.3:3"}), "a", nil),
+				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil, false),
+				membership.NewMember("2", types.MustNewURLs([]string{"http://10.0.0.2:2"}), "a", nil, false),
+				membership.NewMember("3", types.MustNewURLs([]string{"http://10.0.0.3:3"}), "a", nil, false),
 			},
 			},
 			[]string{"http://10.0.0.2:2", "http://10.0.0.3:3"},
 			[]string{"http://10.0.0.2:2", "http://10.0.0.3:3"},
 		},
 		},
 		{
 		{
 			[]*membership.Member{
 			[]*membership.Member{
-				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil),
-				membership.NewMember("3", types.MustNewURLs([]string{"http://10.0.0.3:3"}), "a", nil),
-				membership.NewMember("2", types.MustNewURLs([]string{"http://10.0.0.2:2"}), "a", nil),
+				membership.NewMember("1", types.MustNewURLs([]string{"http://10.0.0.1:1"}), "a", nil, false),
+				membership.NewMember("3", types.MustNewURLs([]string{"http://10.0.0.3:3"}), "a", nil, false),
+				membership.NewMember("2", types.MustNewURLs([]string{"http://10.0.0.2:2"}), "a", nil, false),
 			},
 			},
 			[]string{"http://10.0.0.2:2", "http://10.0.0.3:3"},
 			[]string{"http://10.0.0.2:2", "http://10.0.0.3:3"},
 		},
 		},