func TestStreamReaderDialRequest(t *testing.T) { for i, tt := range []streamType{streamTypeMsgApp, streamTypeMessage, streamTypeMsgAppV2} { tr := &roundTripperRecorder{} sr := &streamReader{ tr: tr, picker: mustNewURLPicker(t, []string{"http://localhost:2380"}), local: types.ID(1), remote: types.ID(2), cid: types.ID(1), msgAppTerm: 1, } sr.dial(tt) req := tr.Request() wurl := fmt.Sprintf("http://localhost:2380" + tt.endpoint() + "/1") if req.URL.String() != wurl { t.Errorf("#%d: url = %s, want %s", i, req.URL.String(), wurl) } if w := "GET"; req.Method != w { t.Errorf("#%d: method = %s, want %s", i, req.Method, w) } if g := req.Header.Get("X-Etcd-Cluster-ID"); g != "1" { t.Errorf("#%d: header X-Etcd-Cluster-ID = %s, want 1", i, g) } if g := req.Header.Get("X-Raft-To"); g != "2" { t.Errorf("#%d: header X-Raft-To = %s, want 2", i, g) } if g := req.Header.Get("X-Raft-Term"); tt == streamTypeMsgApp && g != "1" { t.Errorf("#%d: header X-Raft-Term = %s, want 1", i, g) } } }
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 TestPipelinePost(t *testing.T) { tr := &roundTripperRecorder{} picker := mustNewURLPicker(t, []string{"http://localhost:2380"}) p := newPipeline(tr, picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), nil, &fakeRaft{}, nil) if err := p.post([]byte("some data")); err != nil { t.Fatalf("unexpect post error: %v", err) } p.stop() if g := tr.Request().Method; g != "POST" { t.Errorf("method = %s, want %s", g, "POST") } if g := tr.Request().URL.String(); g != "http://localhost:2380/raft" { t.Errorf("url = %s, want %s", g, "http://localhost:2380/raft") } if g := tr.Request().Header.Get("Content-Type"); g != "application/protobuf" { t.Errorf("content type = %s, want %s", g, "application/protobuf") } if g := tr.Request().Header.Get("X-Server-Version"); g != version.Version { t.Errorf("version = %s, want %s", g, version.Version) } if g := tr.Request().Header.Get("X-Min-Cluster-Version"); g != version.MinClusterVersion { t.Errorf("min version = %s, want %s", g, version.MinClusterVersion) } if g := tr.Request().Header.Get("X-Etcd-Cluster-ID"); g != "1" { t.Errorf("cluster id = %s, want %s", g, "1") } b, err := ioutil.ReadAll(tr.Request().Body) if err != nil { t.Fatalf("unexpected ReadAll error: %v", err) } if string(b) != "some data" { t.Errorf("body = %s, want %s", b, "some data") } }
func readWAL(waldir string, snap walpb.Snapshot) (w *wal.WAL, id, cid types.ID, st raftpb.HardState, ents []raftpb.Entry) { var ( err error wmetadata []byte ) repaired := false for { if w, err = wal.Open(waldir, snap); err != nil { plog.Fatalf("open wal error: %v", err) } if wmetadata, st, ents, err = w.ReadAll(); err != nil { w.Close() // we can only repair ErrUnexpectedEOF and we never repair twice. if repaired || err != io.ErrUnexpectedEOF { plog.Fatalf("read wal error (%v) and cannot be repaired", err) } if !wal.Repair(waldir) { plog.Fatalf("WAL error (%v) cannot be repaired", err) } else { plog.Infof("repaired WAL error (%v)", err) repaired = true } continue } break } var metadata pb.Metadata pbutil.MustUnmarshal(&metadata, wmetadata) id = types.ID(metadata.NodeID) cid = types.ID(metadata.ClusterID) return }
func TestTransportAdd(t *testing.T) { ls := stats.NewLeaderStats("") term := uint64(10) tr := &Transport{ LeaderStats: ls, streamRt: &roundTripperRecorder{}, term: term, peers: make(map[types.ID]Peer), prober: probing.NewProber(nil), } tr.AddPeer(1, []string{"http://localhost:2380"}) if _, ok := ls.Followers["1"]; !ok { t.Errorf("FollowerStats[1] is nil, want exists") } s, ok := tr.peers[types.ID(1)] if !ok { tr.Stop() t.Fatalf("senders[1] is nil, want exists") } // duplicate AddPeer is ignored tr.AddPeer(1, []string{"http://localhost:2380"}) ns := tr.peers[types.ID(1)] if s != ns { t.Errorf("sender = %v, want %v", ns, s) } tr.Stop() if g := s.(*peer).msgAppReader.msgAppTerm; g != term { t.Errorf("peer.term = %d, want %d", g, term) } }
func TestServeRaftStreamPrefix(t *testing.T) { tests := []struct { path string wtype streamType }{ { RaftStreamPrefix + "/message/1", streamTypeMessage, }, { RaftStreamPrefix + "/msgapp/1", streamTypeMsgAppV2, }, // backward compatibility { RaftStreamPrefix + "/1", streamTypeMsgApp, }, } for i, tt := range tests { req, err := http.NewRequest("GET", "http://localhost:2380"+tt.path, nil) if err != nil { t.Fatalf("#%d: could not create request: %#v", i, err) } req.Header.Set("X-Etcd-Cluster-ID", "1") req.Header.Set("X-Server-Version", version.Version) req.Header.Set("X-Raft-To", "2") wterm := "1" req.Header.Set("X-Raft-Term", wterm) peer := newFakePeer() peerGetter := &fakePeerGetter{peers: map[types.ID]Peer{types.ID(1): peer}} h := newStreamHandler(peerGetter, &fakeRaft{}, types.ID(2), types.ID(1)) rw := httptest.NewRecorder() go h.ServeHTTP(rw, req) var conn *outgoingConn select { case conn = <-peer.connc: case <-time.After(time.Second): t.Fatalf("#%d: failed to attach outgoingConn", i) } if g := rw.Header().Get("X-Server-Version"); g != version.Version { t.Errorf("#%d: X-Server-Version = %s, want %s", i, g, version.Version) } if conn.t != tt.wtype { t.Errorf("#%d: type = %s, want %s", i, conn.t, tt.wtype) } if conn.termStr != wterm { t.Errorf("#%d: term = %s, want %s", i, conn.termStr, wterm) } conn.Close() } }
func TestTransportUpdate(t *testing.T) { peer := newFakePeer() tr := &Transport{ peers: map[types.ID]Peer{types.ID(1): peer}, prober: probing.NewProber(nil), } u := "http://localhost:2380" tr.UpdatePeer(types.ID(1), []string{u}) wurls := types.URLs(testutil.MustNewURLs(t, []string{"http://localhost:2380"})) if !reflect.DeepEqual(peer.urls, wurls) { t.Errorf("urls = %+v, want %+v", peer.urls, wurls) } }
// TestPipelineSendFailed tests that when send func meets the post error, // it increases fail count in stats. func TestPipelineSendFailed(t *testing.T) { picker := mustNewURLPicker(t, []string{"http://localhost:2380"}) fs := &stats.FollowerStats{} p := newPipeline(newRespRoundTripper(0, errors.New("blah")), picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), fs, &fakeRaft{}, nil) p.msgc <- raftpb.Message{Type: raftpb.MsgApp} testutil.WaitSchedule() p.stop() fs.Lock() defer fs.Unlock() if fs.Counts.Fail != 1 { t.Errorf("fail = %d, want 1", fs.Counts.Fail) } }
func TestTransportRemove(t *testing.T) { tr := &Transport{ LeaderStats: stats.NewLeaderStats(""), streamRt: &roundTripperRecorder{}, peers: make(map[types.ID]Peer), prober: probing.NewProber(nil), } tr.AddPeer(1, []string{"http://localhost:2380"}) tr.RemovePeer(types.ID(1)) defer tr.Stop() if _, ok := tr.peers[types.ID(1)]; ok { t.Fatalf("senders[1] exists, want removed") } }
// TestStreamWriterAttachBadOutgoingConn tests that streamWriter with bad // outgoingConn will close the outgoingConn and fall back to non-working status. func TestStreamWriterAttachBadOutgoingConn(t *testing.T) { sw := startStreamWriter(types.ID(1), newPeerStatus(types.ID(1)), &stats.FollowerStats{}, &fakeRaft{}) defer sw.stop() wfc := &fakeWriteFlushCloser{err: errors.New("blah")} sw.attach(&outgoingConn{t: streamTypeMessage, Writer: wfc, Flusher: wfc, Closer: wfc}) sw.msgc <- raftpb.Message{} testutil.WaitSchedule() // no longer working if _, ok := sw.writec(); ok != false { t.Errorf("working = %v, want false", ok) } if wfc.closed != true { t.Errorf("failed to close the underlying connection") } }
func newTestMember(id uint64, peerURLs []string, name string, clientURLs []string) *Member { return &Member{ ID: types.ID(id), RaftAttributes: RaftAttributes{PeerURLs: peerURLs}, Attributes: Attributes{Name: name, ClientURLs: clientURLs}, } }
func (t *Transport) Send(msgs []raftpb.Message) { for _, m := range msgs { // intentionally dropped message if m.To == 0 { continue } to := types.ID(m.To) if m.Type != raftpb.MsgProp { // proposal message does not have a valid term t.maybeUpdatePeersTerm(m.Term) } p, ok := t.peers[to] if ok { if m.Type == raftpb.MsgApp { t.ServerStats.SendAppendReq(m.Size()) } p.Send(m) continue } g, ok := t.remotes[to] if ok { g.Send(m) continue } plog.Debugf("ignored message %s (sent to unknown peer %s)", m.Type, to) } }
func (c *cluster) RemoveMember(t *testing.T, id uint64) { // send remove request to the cluster cc := mustNewHTTPClient(t, c.URLs()) ma := client.NewMembersAPI(cc) ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) if err := ma.Remove(ctx, types.ID(id).String()); err != nil { t.Fatalf("unexpected remove error %v", err) } cancel() newMembers := make([]*member, 0) for _, m := range c.Members { if uint64(m.s.ID()) != id { newMembers = append(newMembers, m) } else { select { case <-m.s.StopNotify(): m.Terminate(t) // 1s stop delay + election timeout + 1s disk and network delay + connection write timeout // TODO: remove connection write timeout by selecting on http response closeNotifier // blocking on https://github.com/golang/go/issues/9524 case <-time.After(time.Second + time.Duration(electionTicks)*tickDuration + time.Second + rafthttp.ConnWriteTimeout): t.Fatalf("failed to remove member %s in time", m.s.ID()) } } } c.Members = newMembers c.waitMembersMatch(t, c.HTTPMembers()) }
// ValidateConfigurationChange takes a proposed ConfChange and // ensures that it is still valid. func (c *cluster) ValidateConfigurationChange(cc raftpb.ConfChange) error { members, removed := membersFromStore(c.store) id := types.ID(cc.NodeID) if removed[id] { return ErrIDRemoved } switch cc.Type { case raftpb.ConfChangeAddNode: if members[id] != nil { return ErrIDExists } urls := make(map[string]bool) for _, m := range members { for _, u := range m.PeerURLs { urls[u] = true } } m := new(Member) if err := json.Unmarshal(cc.Context, m); err != nil { plog.Panicf("unmarshal member should never fail: %v", err) } for _, u := range m.PeerURLs { if urls[u] { return ErrPeerURLexists } } case raftpb.ConfChangeRemoveNode: if members[id] == nil { return ErrIDNotFound } case raftpb.ConfChangeUpdateNode: if members[id] == nil { return ErrIDNotFound } urls := make(map[string]bool) for _, m := range members { if m.ID == id { continue } for _, u := range m.PeerURLs { urls[u] = true } } m := new(Member) if err := json.Unmarshal(cc.Context, m); err != nil { plog.Panicf("unmarshal member should never fail: %v", err) } for _, u := range m.PeerURLs { if urls[u] { return ErrPeerURLexists } } default: plog.Panicf("ConfChange type should be either AddNode, RemoveNode or UpdateNode") } return nil }
func (c *cluster) genID() { mIDs := c.MemberIDs() b := make([]byte, 8*len(mIDs)) for i, id := range mIDs { binary.BigEndian.PutUint64(b[8*i:], uint64(id)) } hash := sha1.Sum(b) c.id = types.ID(binary.BigEndian.Uint64(hash[:8])) }
func TestMsgApp(t *testing.T) { tests := []raftpb.Message{ { Type: raftpb.MsgApp, From: 1, To: 2, Term: 1, LogTerm: 1, Index: 3, Entries: []raftpb.Entry{{Term: 1, Index: 4}}, }, { Type: raftpb.MsgApp, From: 1, To: 2, Term: 1, LogTerm: 1, Index: 0, Entries: []raftpb.Entry{ {Term: 1, Index: 1, Data: []byte("some data")}, {Term: 1, Index: 2, Data: []byte("some data")}, {Term: 1, Index: 3, Data: []byte("some data")}, }, }, linkHeartbeatMessage, } for i, tt := range tests { b := &bytes.Buffer{} enc := &msgAppEncoder{w: b, fs: &stats.FollowerStats{}} if err := enc.encode(tt); err != nil { t.Errorf("#%d: unexpected encode message error: %v", i, err) continue } dec := &msgAppDecoder{r: b, local: types.ID(tt.To), remote: types.ID(tt.From), term: tt.Term} m, err := dec.decode() if err != nil { t.Errorf("#%d: unexpected decode message error: %v", i, err) continue } if !reflect.DeepEqual(m, tt) { t.Errorf("#%d: message = %+v, want %+v", i, m, tt) } } }
// TestPipelineSend tests that pipeline could send data using roundtripper // and increase success count in stats. func TestPipelineSend(t *testing.T) { tr := &roundTripperRecorder{} picker := mustNewURLPicker(t, []string{"http://localhost:2380"}) fs := &stats.FollowerStats{} p := newPipeline(tr, picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), fs, &fakeRaft{}, nil) p.msgc <- raftpb.Message{Type: raftpb.MsgApp} testutil.WaitSchedule() p.stop() if tr.Request() == nil { t.Errorf("sender fails to post the data") } fs.Lock() defer fs.Unlock() if fs.Counts.Success != 1 { t.Errorf("success = %d, want 1", fs.Counts.Success) } }
func TestStopBlockedPipeline(t *testing.T) { picker := mustNewURLPicker(t, []string{"http://localhost:2380"}) p := newPipeline(newRoundTripperBlocker(), picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), nil, &fakeRaft{}, nil) // send many messages that most of them will be blocked in buffer for i := 0; i < connPerPipeline*10; i++ { p.msgc <- raftpb.Message{} } done := make(chan struct{}) go func() { p.stop() done <- struct{}{} }() select { case <-done: case <-time.After(time.Second): t.Fatalf("failed to stop pipeline in 1s") } }
func (cr *streamReader) decodeLoop(rc io.ReadCloser, t streamType) error { var dec decoder cr.mu.Lock() switch t { case streamTypeMsgApp: dec = &msgAppDecoder{r: rc, local: cr.local, remote: cr.remote, term: cr.msgAppTerm} case streamTypeMsgAppV2: dec = newMsgAppV2Decoder(rc, cr.local, cr.remote) case streamTypeMessage: dec = &messageDecoder{r: rc} default: plog.Panicf("unhandled stream type %s", t) } cr.closer = rc cr.mu.Unlock() for { m, err := dec.decode() switch { case err != nil: cr.mu.Lock() cr.close() cr.mu.Unlock() return err case isLinkHeartbeatMessage(m): // do nothing for linkHeartbeatMessage default: recvc := cr.recvc if m.Type == raftpb.MsgProp { recvc = cr.propc } select { case recvc <- m: default: if cr.status.isActive() { plog.Warningf("dropped %s from %s since receiving buffer is full", m.Type, types.ID(m.From)) } else { plog.Debugf("dropped %s from %s since receiving buffer is full", m.Type, types.ID(m.From)) } } } } }
// TestStreamWriterAttachOutgoingConn tests that outgoingConn can be attached // to streamWriter. After that, streamWriter can use it to send messages // continuously, and closes it when stopped. func TestStreamWriterAttachOutgoingConn(t *testing.T) { sw := startStreamWriter(types.ID(1), newPeerStatus(types.ID(1)), &stats.FollowerStats{}, &fakeRaft{}) // the expected initial state of streamWrite is not working if _, ok := sw.writec(); ok != false { t.Errorf("initial working status = %v, want false", ok) } // repeatitive tests to ensure it can use latest connection var wfc *fakeWriteFlushCloser for i := 0; i < 3; i++ { prevwfc := wfc wfc = &fakeWriteFlushCloser{} sw.attach(&outgoingConn{t: streamTypeMessage, Writer: wfc, Flusher: wfc, Closer: wfc}) testutil.WaitSchedule() // previous attached connection should be closed if prevwfc != nil && prevwfc.closed != true { t.Errorf("#%d: close of previous connection = %v, want true", i, prevwfc.closed) } // starts working if _, ok := sw.writec(); ok != true { t.Errorf("#%d: working status = %v, want true", i, ok) } sw.msgc <- raftpb.Message{} testutil.WaitSchedule() // still working if _, ok := sw.writec(); ok != true { t.Errorf("#%d: working status = %v, want true", i, ok) } if wfc.written == 0 { t.Errorf("#%d: failed to write to the underlying connection", i) } } sw.stop() // no longer in working status now if _, ok := sw.writec(); ok != false { t.Errorf("working status after stop = %v, want false", ok) } if wfc.closed != true { t.Errorf("failed to close the underlying connection") } }
func TestSendMessage(t *testing.T) { // member 1 tr := &Transport{ ID: types.ID(1), ClusterID: types.ID(1), Raft: &fakeRaft{}, ServerStats: newServerStats(), LeaderStats: stats.NewLeaderStats("1"), } tr.Start() srv := httptest.NewServer(tr.Handler()) defer srv.Close() // member 2 recvc := make(chan raftpb.Message, 1) p := &fakeRaft{recvc: recvc} tr2 := &Transport{ ID: types.ID(2), ClusterID: types.ID(1), Raft: p, ServerStats: newServerStats(), LeaderStats: stats.NewLeaderStats("2"), } tr2.Start() srv2 := httptest.NewServer(tr2.Handler()) defer srv2.Close() tr.AddPeer(types.ID(2), []string{srv2.URL}) defer tr.Stop() tr2.AddPeer(types.ID(1), []string{srv.URL}) defer tr2.Stop() if !waitStreamWorking(tr.Get(types.ID(2)).(*peer)) { t.Fatalf("stream from 1 to 2 is not in work as expected") } data := []byte("some data") tests := []raftpb.Message{ // these messages are set to send to itself, which facilitates testing. {Type: raftpb.MsgProp, From: 1, To: 2, Entries: []raftpb.Entry{{Data: data}}}, // TODO: send out MsgApp which fits msgapp stream but the term doesn't match {Type: raftpb.MsgApp, From: 1, To: 2, Term: 1, Index: 3, LogTerm: 0, Entries: []raftpb.Entry{{Index: 4, Term: 1, Data: data}}, Commit: 3}, {Type: raftpb.MsgAppResp, From: 1, To: 2, Term: 1, Index: 3}, {Type: raftpb.MsgVote, From: 1, To: 2, Term: 1, Index: 3, LogTerm: 0}, {Type: raftpb.MsgVoteResp, From: 1, To: 2, Term: 1}, {Type: raftpb.MsgSnap, From: 1, To: 2, Term: 1, Snapshot: raftpb.Snapshot{Metadata: raftpb.SnapshotMetadata{Index: 1000, Term: 1}, Data: data}}, {Type: raftpb.MsgHeartbeat, From: 1, To: 2, Term: 1, Commit: 3}, {Type: raftpb.MsgHeartbeatResp, From: 1, To: 2, Term: 1}, } for i, tt := range tests { tr.Send([]raftpb.Message{tt}) msg := <-recvc if !reflect.DeepEqual(msg, tt) { t.Errorf("#%d: msg = %+v, want %+v", i, msg, tt) } } }
func TestPipelinePostErrorc(t *testing.T) { tests := []struct { u string code int err error }{ {"http://localhost:2380", http.StatusForbidden, nil}, } for i, tt := range tests { picker := mustNewURLPicker(t, []string{tt.u}) errorc := make(chan error, 1) p := newPipeline(newRespRoundTripper(tt.code, tt.err), picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), nil, &fakeRaft{}, errorc) p.post([]byte("some data")) p.stop() select { case <-errorc: default: t.Fatalf("#%d: cannot receive from errorc", i) } } }
// TestStreamReaderDialDetectUnsupport tests that dial func could find // out that the stream type is not supported by the remote. func TestStreamReaderDialDetectUnsupport(t *testing.T) { for i, typ := range []streamType{streamTypeMsgAppV2, streamTypeMessage} { // the response from etcd 2.0 tr := &respRoundTripper{ code: http.StatusNotFound, header: http.Header{}, } sr := &streamReader{ tr: tr, picker: mustNewURLPicker(t, []string{"http://localhost:2380"}), local: types.ID(1), remote: types.ID(2), cid: types.ID(1), } _, err := sr.dial(typ) if err != errUnsupportedStreamType { t.Errorf("#%d: error = %v, want %v", i, err, errUnsupportedStreamType) } } }
// TestStreamReaderDialResult tests the result of the dial func call meets the // HTTP response received. func TestStreamReaderDialResult(t *testing.T) { tests := []struct { code int err error wok bool whalt bool }{ {0, errors.New("blah"), false, false}, {http.StatusOK, nil, true, false}, {http.StatusMethodNotAllowed, nil, false, false}, {http.StatusNotFound, nil, false, false}, {http.StatusPreconditionFailed, nil, false, false}, {http.StatusGone, nil, false, true}, } for i, tt := range tests { h := http.Header{} h.Add("X-Server-Version", version.Version) tr := &respRoundTripper{ code: tt.code, header: h, err: tt.err, } sr := &streamReader{ tr: tr, picker: mustNewURLPicker(t, []string{"http://localhost:2380"}), local: types.ID(1), remote: types.ID(2), cid: types.ID(1), errorc: make(chan error, 1), } _, err := sr.dial(streamTypeMessage) if ok := err == nil; ok != tt.wok { t.Errorf("#%d: ok = %v, want %v", i, ok, tt.wok) } if halt := len(sr.errorc) > 0; halt != tt.whalt { t.Errorf("#%d: halt = %v, want %v", i, halt, tt.whalt) } } }
func TestPipelineExceedMaximumServing(t *testing.T) { tr := newRoundTripperBlocker() picker := mustNewURLPicker(t, []string{"http://localhost:2380"}) fs := &stats.FollowerStats{} p := newPipeline(tr, picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), fs, &fakeRaft{}, nil) // keep the sender busy and make the buffer full // nothing can go out as we block the sender testutil.WaitSchedule() for i := 0; i < connPerPipeline+pipelineBufSize; i++ { select { case p.msgc <- raftpb.Message{}: default: t.Errorf("failed to send out message") } // force the sender to grab data testutil.WaitSchedule() } // try to send a data when we are sure the buffer is full select { case p.msgc <- raftpb.Message{}: t.Errorf("unexpected message sendout") default: } // unblock the senders and force them to send out the data tr.unblock() testutil.WaitSchedule() // It could send new data after previous ones succeed select { case p.msgc <- raftpb.Message{}: default: t.Errorf("failed to send out message") } p.stop() }
// TestSendMessageWhenStreamIsBroken tests that message can be sent to the // remote in a limited time when all underlying connections are broken. func TestSendMessageWhenStreamIsBroken(t *testing.T) { // member 1 tr := &Transport{ ID: types.ID(1), ClusterID: types.ID(1), Raft: &fakeRaft{}, ServerStats: newServerStats(), LeaderStats: stats.NewLeaderStats("1"), } tr.Start() srv := httptest.NewServer(tr.Handler()) defer srv.Close() // member 2 recvc := make(chan raftpb.Message, 1) p := &fakeRaft{recvc: recvc} tr2 := &Transport{ ID: types.ID(2), ClusterID: types.ID(1), Raft: p, ServerStats: newServerStats(), LeaderStats: stats.NewLeaderStats("2"), } tr2.Start() srv2 := httptest.NewServer(tr2.Handler()) defer srv2.Close() tr.AddPeer(types.ID(2), []string{srv2.URL}) defer tr.Stop() tr2.AddPeer(types.ID(1), []string{srv.URL}) defer tr2.Stop() if !waitStreamWorking(tr.Get(types.ID(2)).(*peer)) { t.Fatalf("stream from 1 to 2 is not in work as expected") } // break the stream srv.CloseClientConnections() srv2.CloseClientConnections() var n int for { select { // TODO: remove this resend logic when we add retry logic into the code case <-time.After(time.Millisecond): n++ tr.Send([]raftpb.Message{{Type: raftpb.MsgHeartbeat, From: 1, To: 2, Term: 1, Commit: 3}}) case <-recvc: if n > 10 { t.Errorf("disconnection time = %dms, want < 10ms", n) } return } } }
func TestPipelinePostBad(t *testing.T) { tests := []struct { u string code int err error }{ // RoundTrip returns error {"http://localhost:2380", 0, errors.New("blah")}, // unexpected response status code {"http://localhost:2380", http.StatusOK, nil}, {"http://localhost:2380", http.StatusCreated, nil}, } for i, tt := range tests { picker := mustNewURLPicker(t, []string{tt.u}) p := newPipeline(newRespRoundTripper(tt.code, tt.err), picker, types.ID(2), types.ID(1), types.ID(1), newPeerStatus(types.ID(1)), nil, &fakeRaft{}, make(chan error)) err := p.post([]byte("some data")) p.stop() if err == nil { t.Errorf("#%d: err = nil, want not nil", i) } } }
// TestTransportSend tests that transport can send messages using correct // underlying peer, and drop local or unknown-target messages. func TestTransportSend(t *testing.T) { ss := &stats.ServerStats{} ss.Initialize() peer1 := newFakePeer() peer2 := newFakePeer() tr := &Transport{ ServerStats: ss, peers: map[types.ID]Peer{types.ID(1): peer1, types.ID(2): peer2}, } wmsgsIgnored := []raftpb.Message{ // bad local message {Type: raftpb.MsgBeat}, // bad remote message {Type: raftpb.MsgProp, To: 3}, } wmsgsTo1 := []raftpb.Message{ // good message {Type: raftpb.MsgProp, To: 1}, {Type: raftpb.MsgApp, To: 1}, } wmsgsTo2 := []raftpb.Message{ // good message {Type: raftpb.MsgProp, To: 2}, {Type: raftpb.MsgApp, To: 2}, } tr.Send(wmsgsIgnored) tr.Send(wmsgsTo1) tr.Send(wmsgsTo2) if !reflect.DeepEqual(peer1.msgs, wmsgsTo1) { t.Errorf("msgs to peer 1 = %+v, want %+v", peer1.msgs, wmsgsTo1) } if !reflect.DeepEqual(peer2.msgs, wmsgsTo2) { t.Errorf("msgs to peer 2 = %+v, want %+v", peer2.msgs, wmsgsTo2) } }
// createConfigChangeEnts creates a series of Raft entries (i.e. // EntryConfChange) to remove the set of given IDs from the cluster. The ID // `self` is _not_ removed, even if present in the set. // If `self` is not inside the given ids, it creates a Raft entry to add a // default member with the given `self`. func createConfigChangeEnts(ids []uint64, self uint64, term, index uint64) []raftpb.Entry { ents := make([]raftpb.Entry, 0) next := index + 1 found := false for _, id := range ids { if id == self { found = true continue } cc := &raftpb.ConfChange{ Type: raftpb.ConfChangeRemoveNode, NodeID: id, } e := raftpb.Entry{ Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(cc), Term: term, Index: next, } ents = append(ents, e) next++ } if !found { m := Member{ ID: types.ID(self), RaftAttributes: RaftAttributes{PeerURLs: []string{"http://localhost:7001", "http://localhost:2380"}}, } ctx, err := json.Marshal(m) if err != nil { plog.Panicf("marshal member should never fail: %v", err) } cc := &raftpb.ConfChange{ Type: raftpb.ConfChangeAddNode, NodeID: self, Context: ctx, } e := raftpb.Entry{ Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(cc), Term: term, Index: next, } ents = append(ents, e) } return ents }
// NewMember creates a Member without an ID and generates one based on the // name, peer URLs. This is used for bootstrapping/adding new member. func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.Time) *Member { m := &Member{ RaftAttributes: RaftAttributes{PeerURLs: peerURLs.StringSlice()}, Attributes: Attributes{Name: name}, } var b []byte sort.Strings(m.PeerURLs) for _, p := range m.PeerURLs { b = append(b, []byte(p)...) } b = append(b, []byte(clusterName)...) if now != nil { b = append(b, []byte(fmt.Sprintf("%d", now.Unix()))...) } hash := sha1.Sum(b) m.ID = types.ID(binary.BigEndian.Uint64(hash[:8])) return m }