| // Copyright 2015 The etcd Authors |
| // |
| // 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 membership |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "path" |
| "reflect" |
| "testing" |
| |
| "github.com/coreos/etcd/pkg/mock/mockstore" |
| "github.com/coreos/etcd/pkg/testutil" |
| "github.com/coreos/etcd/pkg/types" |
| "github.com/coreos/etcd/raft/raftpb" |
| "github.com/coreos/etcd/store" |
| ) |
| |
| func TestClusterMember(t *testing.T) { |
| membs := []*Member{ |
| newTestMember(1, nil, "node1", nil), |
| newTestMember(2, nil, "node2", nil), |
| } |
| tests := []struct { |
| id types.ID |
| match bool |
| }{ |
| {1, true}, |
| {2, true}, |
| {3, false}, |
| } |
| for i, tt := range tests { |
| c := newTestCluster(membs) |
| m := c.Member(tt.id) |
| if g := m != nil; g != tt.match { |
| t.Errorf("#%d: find member = %v, want %v", i, g, tt.match) |
| } |
| if m != nil && m.ID != tt.id { |
| t.Errorf("#%d: id = %x, want %x", i, m.ID, tt.id) |
| } |
| } |
| } |
| |
| func TestClusterMemberByName(t *testing.T) { |
| membs := []*Member{ |
| newTestMember(1, nil, "node1", nil), |
| newTestMember(2, nil, "node2", nil), |
| } |
| tests := []struct { |
| name string |
| match bool |
| }{ |
| {"node1", true}, |
| {"node2", true}, |
| {"node3", false}, |
| } |
| for i, tt := range tests { |
| c := newTestCluster(membs) |
| m := c.MemberByName(tt.name) |
| if g := m != nil; g != tt.match { |
| t.Errorf("#%d: find member = %v, want %v", i, g, tt.match) |
| } |
| if m != nil && m.Name != tt.name { |
| t.Errorf("#%d: name = %v, want %v", i, m.Name, tt.name) |
| } |
| } |
| } |
| |
| func TestClusterMemberIDs(t *testing.T) { |
| c := newTestCluster([]*Member{ |
| newTestMember(1, nil, "", nil), |
| newTestMember(4, nil, "", nil), |
| newTestMember(100, nil, "", nil), |
| }) |
| w := []types.ID{1, 4, 100} |
| g := c.MemberIDs() |
| if !reflect.DeepEqual(w, g) { |
| t.Errorf("IDs = %+v, want %+v", g, w) |
| } |
| } |
| |
| func TestClusterPeerURLs(t *testing.T) { |
| tests := []struct { |
| mems []*Member |
| wurls []string |
| }{ |
| // single peer with a single address |
| { |
| mems: []*Member{ |
| newTestMember(1, []string{"http://192.0.2.1"}, "", nil), |
| }, |
| wurls: []string{"http://192.0.2.1"}, |
| }, |
| |
| // single peer with a single address with a port |
| { |
| mems: []*Member{ |
| newTestMember(1, []string{"http://192.0.2.1:8001"}, "", nil), |
| }, |
| wurls: []string{"http://192.0.2.1:8001"}, |
| }, |
| |
| // several members explicitly unsorted |
| { |
| mems: []*Member{ |
| newTestMember(2, []string{"http://192.0.2.3", "http://192.0.2.4"}, "", nil), |
| newTestMember(3, []string{"http://192.0.2.5", "http://192.0.2.6"}, "", nil), |
| newTestMember(1, []string{"http://192.0.2.1", "http://192.0.2.2"}, "", nil), |
| }, |
| wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"}, |
| }, |
| |
| // no members |
| { |
| mems: []*Member{}, |
| wurls: []string{}, |
| }, |
| |
| // peer with no peer urls |
| { |
| mems: []*Member{ |
| newTestMember(3, []string{}, "", nil), |
| }, |
| wurls: []string{}, |
| }, |
| } |
| |
| for i, tt := range tests { |
| c := newTestCluster(tt.mems) |
| urls := c.PeerURLs() |
| if !reflect.DeepEqual(urls, tt.wurls) { |
| t.Errorf("#%d: PeerURLs = %v, want %v", i, urls, tt.wurls) |
| } |
| } |
| } |
| |
| func TestClusterClientURLs(t *testing.T) { |
| tests := []struct { |
| mems []*Member |
| wurls []string |
| }{ |
| // single peer with a single address |
| { |
| mems: []*Member{ |
| newTestMember(1, nil, "", []string{"http://192.0.2.1"}), |
| }, |
| wurls: []string{"http://192.0.2.1"}, |
| }, |
| |
| // single peer with a single address with a port |
| { |
| mems: []*Member{ |
| newTestMember(1, nil, "", []string{"http://192.0.2.1:8001"}), |
| }, |
| wurls: []string{"http://192.0.2.1:8001"}, |
| }, |
| |
| // several members explicitly unsorted |
| { |
| mems: []*Member{ |
| newTestMember(2, nil, "", []string{"http://192.0.2.3", "http://192.0.2.4"}), |
| newTestMember(3, nil, "", []string{"http://192.0.2.5", "http://192.0.2.6"}), |
| newTestMember(1, nil, "", []string{"http://192.0.2.1", "http://192.0.2.2"}), |
| }, |
| wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"}, |
| }, |
| |
| // no members |
| { |
| mems: []*Member{}, |
| wurls: []string{}, |
| }, |
| |
| // peer with no client urls |
| { |
| mems: []*Member{ |
| newTestMember(3, nil, "", []string{}), |
| }, |
| wurls: []string{}, |
| }, |
| } |
| |
| for i, tt := range tests { |
| c := newTestCluster(tt.mems) |
| urls := c.ClientURLs() |
| if !reflect.DeepEqual(urls, tt.wurls) { |
| t.Errorf("#%d: ClientURLs = %v, want %v", i, urls, tt.wurls) |
| } |
| } |
| } |
| |
| func TestClusterValidateAndAssignIDsBad(t *testing.T) { |
| tests := []struct { |
| clmembs []*Member |
| membs []*Member |
| }{ |
| { |
| // unmatched length |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil), |
| }, |
| []*Member{}, |
| }, |
| { |
| // unmatched peer urls |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil), |
| }, |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:4001"}, "", nil), |
| }, |
| }, |
| { |
| // unmatched peer urls |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil), |
| newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil), |
| }, |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil), |
| newTestMember(2, []string{"http://127.0.0.2:4001"}, "", nil), |
| }, |
| }, |
| } |
| for i, tt := range tests { |
| ecl := newTestCluster(tt.clmembs) |
| lcl := newTestCluster(tt.membs) |
| if err := ValidateClusterAndAssignIDs(lcl, ecl); err == nil { |
| t.Errorf("#%d: unexpected update success", i) |
| } |
| } |
| } |
| |
| func TestClusterValidateAndAssignIDs(t *testing.T) { |
| tests := []struct { |
| clmembs []*Member |
| membs []*Member |
| wids []types.ID |
| }{ |
| { |
| []*Member{ |
| newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil), |
| newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil), |
| }, |
| []*Member{ |
| newTestMember(3, []string{"http://127.0.0.1:2379"}, "", nil), |
| newTestMember(4, []string{"http://127.0.0.2:2379"}, "", nil), |
| }, |
| []types.ID{3, 4}, |
| }, |
| } |
| for i, tt := range tests { |
| lcl := newTestCluster(tt.clmembs) |
| ecl := newTestCluster(tt.membs) |
| if err := ValidateClusterAndAssignIDs(lcl, ecl); err != nil { |
| t.Errorf("#%d: unexpect update error: %v", i, err) |
| } |
| if !reflect.DeepEqual(lcl.MemberIDs(), tt.wids) { |
| t.Errorf("#%d: ids = %v, want %v", i, lcl.MemberIDs(), tt.wids) |
| } |
| } |
| } |
| |
| func TestClusterValidateConfigurationChange(t *testing.T) { |
| cl := NewCluster("") |
| cl.SetStore(store.New()) |
| for i := 1; i <= 4; i++ { |
| attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", i)}} |
| cl.AddMember(&Member{ID: types.ID(i), RaftAttributes: attr}) |
| } |
| cl.RemoveMember(4) |
| |
| attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 1)}} |
| ctx, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}} |
| ctx5, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 3)}} |
| ctx2to3, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}} |
| ctx2to5, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tests := []struct { |
| cc raftpb.ConfChange |
| werr error |
| }{ |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeRemoveNode, |
| NodeID: 3, |
| }, |
| nil, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeAddNode, |
| NodeID: 4, |
| }, |
| ErrIDRemoved, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeRemoveNode, |
| NodeID: 4, |
| }, |
| ErrIDRemoved, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeAddNode, |
| NodeID: 1, |
| }, |
| ErrIDExists, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeAddNode, |
| NodeID: 5, |
| Context: ctx, |
| }, |
| ErrPeerURLexists, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeRemoveNode, |
| NodeID: 5, |
| }, |
| ErrIDNotFound, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeAddNode, |
| NodeID: 5, |
| Context: ctx5, |
| }, |
| nil, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeUpdateNode, |
| NodeID: 5, |
| Context: ctx, |
| }, |
| ErrIDNotFound, |
| }, |
| // try to change the peer url of 2 to the peer url of 3 |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeUpdateNode, |
| NodeID: 2, |
| Context: ctx2to3, |
| }, |
| ErrPeerURLexists, |
| }, |
| { |
| raftpb.ConfChange{ |
| Type: raftpb.ConfChangeUpdateNode, |
| NodeID: 2, |
| Context: ctx2to5, |
| }, |
| nil, |
| }, |
| } |
| for i, tt := range tests { |
| err := cl.ValidateConfigurationChange(tt.cc) |
| if err != tt.werr { |
| t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr) |
| } |
| } |
| } |
| |
| func TestClusterGenID(t *testing.T) { |
| cs := newTestCluster([]*Member{ |
| newTestMember(1, nil, "", nil), |
| newTestMember(2, nil, "", nil), |
| }) |
| |
| cs.genID() |
| if cs.ID() == 0 { |
| t.Fatalf("cluster.ID = %v, want not 0", cs.ID()) |
| } |
| previd := cs.ID() |
| |
| cs.SetStore(mockstore.NewNop()) |
| cs.AddMember(newTestMember(3, nil, "", nil)) |
| cs.genID() |
| if cs.ID() == previd { |
| t.Fatalf("cluster.ID = %v, want not %v", cs.ID(), previd) |
| } |
| } |
| |
| func TestNodeToMemberBad(t *testing.T) { |
| tests := []*store.NodeExtern{ |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/strange"}, |
| }}, |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/raftAttributes", Value: stringp("garbage")}, |
| }}, |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)}, |
| }}, |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)}, |
| {Key: "/1234/strange"}, |
| }}, |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)}, |
| {Key: "/1234/attributes", Value: stringp("garbage")}, |
| }}, |
| {Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)}, |
| {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)}, |
| {Key: "/1234/strange"}, |
| }}, |
| } |
| for i, tt := range tests { |
| if _, err := nodeToMember(tt); err == nil { |
| t.Errorf("#%d: unexpected nil error", i) |
| } |
| } |
| } |
| |
| func TestClusterAddMember(t *testing.T) { |
| st := mockstore.NewRecorder() |
| c := newTestCluster(nil) |
| c.SetStore(st) |
| c.AddMember(newTestMember(1, nil, "node1", nil)) |
| |
| wactions := []testutil.Action{ |
| { |
| Name: "Create", |
| Params: []interface{}{ |
| path.Join(StoreMembersPrefix, "1", "raftAttributes"), |
| false, |
| `{"peerURLs":null}`, |
| false, |
| store.TTLOptionSet{ExpireTime: store.Permanent}, |
| }, |
| }, |
| } |
| if g := st.Action(); !reflect.DeepEqual(g, wactions) { |
| t.Errorf("actions = %v, want %v", g, wactions) |
| } |
| } |
| |
| func TestClusterMembers(t *testing.T) { |
| cls := &RaftCluster{ |
| members: map[types.ID]*Member{ |
| 1: {ID: 1}, |
| 20: {ID: 20}, |
| 100: {ID: 100}, |
| 5: {ID: 5}, |
| 50: {ID: 50}, |
| }, |
| } |
| w := []*Member{ |
| {ID: 1}, |
| {ID: 5}, |
| {ID: 20}, |
| {ID: 50}, |
| {ID: 100}, |
| } |
| if g := cls.Members(); !reflect.DeepEqual(g, w) { |
| t.Fatalf("Members()=%#v, want %#v", g, w) |
| } |
| } |
| |
| func TestClusterRemoveMember(t *testing.T) { |
| st := mockstore.NewRecorder() |
| c := newTestCluster(nil) |
| c.SetStore(st) |
| c.RemoveMember(1) |
| |
| wactions := []testutil.Action{ |
| {Name: "Delete", Params: []interface{}{MemberStoreKey(1), true, true}}, |
| {Name: "Create", Params: []interface{}{RemovedMemberStoreKey(1), false, "", false, store.TTLOptionSet{ExpireTime: store.Permanent}}}, |
| } |
| if !reflect.DeepEqual(st.Action(), wactions) { |
| t.Errorf("actions = %v, want %v", st.Action(), wactions) |
| } |
| } |
| |
| func TestClusterUpdateAttributes(t *testing.T) { |
| name := "etcd" |
| clientURLs := []string{"http://127.0.0.1:4001"} |
| tests := []struct { |
| mems []*Member |
| removed map[types.ID]bool |
| wmems []*Member |
| }{ |
| // update attributes of existing member |
| { |
| []*Member{ |
| newTestMember(1, nil, "", nil), |
| }, |
| nil, |
| []*Member{ |
| newTestMember(1, nil, name, clientURLs), |
| }, |
| }, |
| // update attributes of removed member |
| { |
| nil, |
| map[types.ID]bool{types.ID(1): true}, |
| nil, |
| }, |
| } |
| for i, tt := range tests { |
| c := newTestCluster(tt.mems) |
| c.removed = tt.removed |
| |
| c.UpdateAttributes(types.ID(1), Attributes{Name: name, ClientURLs: clientURLs}) |
| if g := c.Members(); !reflect.DeepEqual(g, tt.wmems) { |
| t.Errorf("#%d: members = %+v, want %+v", i, g, tt.wmems) |
| } |
| } |
| } |
| |
| func TestNodeToMember(t *testing.T) { |
| n := &store.NodeExtern{Key: "/1234", Nodes: []*store.NodeExtern{ |
| {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)}, |
| {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)}, |
| }} |
| wm := &Member{ID: 0x1234, RaftAttributes: RaftAttributes{}, Attributes: Attributes{Name: "node1"}} |
| m, err := nodeToMember(n) |
| if err != nil { |
| t.Fatalf("unexpected nodeToMember error: %v", err) |
| } |
| if !reflect.DeepEqual(m, wm) { |
| t.Errorf("member = %+v, want %+v", m, wm) |
| } |
| } |
| |
| func newTestCluster(membs []*Member) *RaftCluster { |
| c := &RaftCluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)} |
| for _, m := range membs { |
| c.members[m.ID] = m |
| } |
| return c |
| } |
| |
| func stringp(s string) *string { return &s } |
| |
| func TestIsReadyToAddNewMember(t *testing.T) { |
| tests := []struct { |
| members []*Member |
| want bool |
| }{ |
| { |
| // 0/3 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "", nil), |
| newTestMember(2, nil, "", nil), |
| newTestMember(3, nil, "", nil), |
| }, |
| false, |
| }, |
| { |
| // 1/2 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "", nil), |
| }, |
| false, |
| }, |
| { |
| // 1/3 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "", nil), |
| newTestMember(3, nil, "", nil), |
| }, |
| false, |
| }, |
| { |
| // 1/1 members ready, should succeed (special case of 1-member cluster for recovery) |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| }, |
| true, |
| }, |
| { |
| // 2/3 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "", nil), |
| }, |
| false, |
| }, |
| { |
| // 3/3 members ready, should be fine to add one member and retain quorum |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "3", nil), |
| }, |
| true, |
| }, |
| { |
| // 3/4 members ready, should be fine to add one member and retain quorum |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "3", nil), |
| newTestMember(4, nil, "", nil), |
| }, |
| true, |
| }, |
| { |
| // empty cluster, it is impossible but should fail |
| []*Member{}, |
| false, |
| }, |
| } |
| for i, tt := range tests { |
| c := newTestCluster(tt.members) |
| if got := c.IsReadyToAddNewMember(); got != tt.want { |
| t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want) |
| } |
| } |
| } |
| |
| func TestIsReadyToRemoveMember(t *testing.T) { |
| tests := []struct { |
| members []*Member |
| removeID uint64 |
| want bool |
| }{ |
| { |
| // 1/1 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| }, |
| 1, |
| false, |
| }, |
| { |
| // 0/3 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "", nil), |
| newTestMember(2, nil, "", nil), |
| newTestMember(3, nil, "", nil), |
| }, |
| 1, |
| false, |
| }, |
| { |
| // 1/2 members ready, should be fine to remove unstarted member |
| // (isReadyToRemoveMember() logic should return success, but operation itself would fail) |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "", nil), |
| }, |
| 2, |
| true, |
| }, |
| { |
| // 2/3 members ready, should fail |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "", nil), |
| }, |
| 2, |
| false, |
| }, |
| { |
| // 3/3 members ready, should be fine to remove one member and retain quorum |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "3", nil), |
| }, |
| 3, |
| true, |
| }, |
| { |
| // 3/4 members ready, should be fine to remove one member |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "3", nil), |
| newTestMember(4, nil, "", nil), |
| }, |
| 3, |
| true, |
| }, |
| { |
| // 3/4 members ready, should be fine to remove unstarted member |
| []*Member{ |
| newTestMember(1, nil, "1", nil), |
| newTestMember(2, nil, "2", nil), |
| newTestMember(3, nil, "3", nil), |
| newTestMember(4, nil, "", nil), |
| }, |
| 4, |
| true, |
| }, |
| } |
| for i, tt := range tests { |
| c := newTestCluster(tt.members) |
| if got := c.IsReadyToRemoveMember(tt.removeID); got != tt.want { |
| t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want) |
| } |
| } |
| } |