Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

raft: add learner and tests. #2491

Merged
merged 31 commits into from
Dec 18, 2017
Merged

raft: add learner and tests. #2491

merged 31 commits into from
Dec 18, 2017

Conversation

hicqu
Copy link
Contributor

@hicqu hicqu commented Nov 20, 2017

This PR ports raft learner from etcd.

@siddontang
Copy link
Contributor

PTAL @overvenus

src/raft/raft.rs Outdated
@@ -443,7 +481,11 @@ impl<T: Storage> Raft<T> {
}

fn prepare_send_snapshot(&mut self, m: &mut Message, to: u64) -> bool {
let pr = self.prs.get_mut(&to).unwrap();
let mut pr = match self.prs.get_mut(&to) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not using get_mut_progress_by_id here?

src/raft/raft.rs Outdated
@@ -498,12 +540,14 @@ impl<T: Storage> Raft<T> {
}

fn prepare_send_entries(&mut self, m: &mut Message, to: u64, term: u64, ents: Vec<Entry>) {
let pr = self.prs.get_mut(&to).unwrap();
let tag = self.tag.clone(); // TODO: recude clone.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why clone here?

src/raft/raft.rs Outdated
.maybe_update(self.raft_log.last_index());

{
let pr = match self.prs.get_mut(&self.id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use get_mut_progress_by_id?

src/raft/raft.rs Outdated
@@ -308,8 +322,19 @@ impl<T: Storage> Raft<T> {
skip_bcast_commit: c.skip_bcast_commit,
tag: c.tag.to_owned(),
};
for p in peers {
r.prs.insert(*p, new_progress(1, r.max_inflight));
for &p in peers {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer dereferencing to taking references.

src/raft/raft.rs Outdated
self.prs.get(&id).or_else(|| self.learner_prs.get(&id))
}

fn get_mut_progress_by_id(&mut self, id: u64) -> Option<&mut Progress> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by_id is redundant.

src/raft/raft.rs Outdated
@@ -182,9 +188,12 @@ pub struct Raft<T: Storage> {
pub max_inflight: usize,
pub max_msg_size: u64,
pub prs: FlatMap<u64, Progress>,
pub learner_prs: FlatMap<u64, Progress>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems learner_prs and prs are almost always handled together, why not put them into a struct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xiang90 suggests separating them. Let's keep the same with etcd.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not suggesting to unify them, but use a struct Prs { prs: map, leaner_prs: map } to manage them all. It's easier to deal with ownership in that way.

@hicqu
Copy link
Contributor Author

hicqu commented Nov 27, 2017

@BusyJay @siddontang PTAL.

src/raft/raft.rs Outdated
@@ -1836,21 +2003,22 @@ impl<T: Storage> Raft<T> {
// false.
// check_quorum_active also resets all recent_active to false.
fn check_quorum_active(&mut self) -> bool {
let mut act = 0;
// NOTE: Here is a little different from etcd.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference?

Copy link
Contributor Author

@hicqu hicqu Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In etcd, self is always active even if it's a learner. But I think it's always active only if it's a voter but not learner. Is it correct?
See: https://github.com/coreos/etcd/blob/master/raft/raft.go#L1375

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this function can only be called if the peer is a leader.

src/raft/raft.rs Outdated
// TODO: avoid copy
let ids: Vec<_> = self.prs.keys().cloned().collect();
for id in ids {
for id in self.nodes() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use nodes but not mem replace here?

src/raft/raft.rs Outdated
for id in ids {
if id == self.id {
continue;
let prs = mem::replace(&mut self.prs, ProgressSet::default());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bad idea. What if self.prs is queried before it's reset? It's hard to write correct code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about write code like this:

struct Raft {
    prs: ProgressSet,
    raft: _Raft,  // _Raft is a struct contains all other fields
}

So all method of _Raft need pass a &ProgressSet or &mut ProgressSet in.

src/raft/raft.rs Outdated
if id == self.id {
continue;
let prs = self.take_prs();
for (id, pr) in prs.voters.iter().chain(&prs.learners) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not using chain + filter + for_each as above does?

src/raft/raft.rs Outdated
if id == self.id {
continue;
let prs = self.take_prs();
for (id, pr) in prs.voters.iter().chain(&prs.learners) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not using chain + filter + for_each as above does?

@siddontang
Copy link
Contributor

PTAL @overvenus

let raft_log = RaftLog::new(store, c.tag.clone());
let mut peers: &[u64] = &c.peers;
if !rs.conf_state.get_nodes().is_empty() {
if !peers.is_empty() {
let mut learners: &[u64] = &c.learners;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need type declaration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we need.

src/raft/raft.rs Outdated
// TODO: the peers argument is always nil except in
// tests; the argument should be removed and these tests should be
// updated to specify their nodes through a snap
panic!(
"{} cannot specify both new(peers) and ConfState.Nodes",
"{} cannot specify both new(peers/learners) and ConfState.Nodes",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about "... and ConfState.(Nodes/Learners)",?
Just keep consistent with etcds[0].

[0] https://github.com/coreos/etcd/pull/8751/files?diff=unified#diff-a8629a0eb6da1c6c3805111f1f1af85eR306

src/raft/raft.rs Outdated
@@ -181,10 +187,12 @@ pub struct Raft<T: Storage> {

pub max_inflight: usize,
pub max_msg_size: u64,
pub prs: FlatMap<u64, Progress>,
prs: Option<ProgressSet>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does prs become None?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for bcast to avoid cloning ID

src/raft/raft.rs Outdated
if self.is_learner {
// TODO: learner may need to vote, in case of node down when confchange.
info!(
r#"{} [logterm: {}, index: {}, vote: {}] ignored {:?} from {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use \ for breaking a long string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't pass rustfmt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? See L1774.

src/raft/raft.rs Outdated
@@ -1292,10 +1407,19 @@ impl<T: Storage> Raft<T> {
);
}
MessageType::MsgTransferLeader => {
self.handle_transfer_leader(m);
let pr = prs.get_mut_progress(m.get_from()).unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could from be 0?

CC @BusyJay

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can not unless we meet a serious bug.

src/raft/raft.rs Outdated
}

pub fn add_node(&mut self, id: u64) {
fn add_node_or_learner(&mut self, id: u64, is_learner: bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "node" is a voter, so I suggest rename it to add_voter_or_learner or add_node_or_learner_node.

src/raft/raft.rs Outdated
.iter_mut()
.chain(prs.learners.iter_mut())
.filter(|&(id, _)| *id != self_id)
.for_each(|(id, ref mut pr)| { self.send_append(*id, pr); });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, for loop is straightforward and easier to understand.

pub learners: FlatMap<u64, Progress>,
}

impl ProgressSet {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide a method like fn for_each<F: FnMut(u64, &mut Progress, bool)>(&mut self, func: F)?
So that we do not need to write something like

voter.iter().for_each(do_something); 
leader.iter().for_each(do_something);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thinks we don't need that. If we want to apply same closure on voters and learners, we can call voters.iter().chain(&learners).for_each(...) directly.

Copy link
Member

@BusyJay BusyJay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split this pr into two: change prs to Option and change prs to ProgressSet.

progress
}

pub fn check_intersect(&self) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is redundant if it's checked before inserting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we didn't check before inserting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we check it when we insert every node, we need to write the logic twice, once in Raft::new, and once in set_progress.

If we check it before inserting with voters and learners as two Vec, the check will be O(n^2).

So I think call check_intersect after the ProgressSet is stable is better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to write the logic twice, once in Raft::new, and once in set_progress.

Why? What I mean is to provide an insert method and check in it.

If we check it before inserting with voters and learners as two Vec, the check will be O(n^2).

How? I think it should be O(n), where n is total count of nodes and learners. Besides, usually n <= 5, so the time complexity doesn't matter here.

When define a seperate check_intersect method, it requires developer to remember to invoke it every time ProgressSet is updated, which is very easy to make error.

src/raft/raft.rs Outdated
}

pub fn del_progress(&mut self, id: u64) {
self.mut_prs().voters.remove(&id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check if return value is None.

learners: FlatMap<u64, Progress>,
}

impl<'a> ProgressSet {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare it in method signature instead.

src/raft/raft.rs Outdated
// self is always active
act += 1;
continue;
let act = self.mut_prs().iter_mut_voters().fold(0, |act, (&id, pr)| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use iter_mut, and check if is_learner to decide whether to update act.

}
}

pub fn delete(&mut self, id: u64) -> Option<Progress> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/delete/remove/

if self.learners.contains_key(&id) {
panic!("insert voter {} but already in learners", id);
}
match self.voters.entry(id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check self.voters.insert(id, pr).is_some() instead? Besides it's very strange that insertion of a collection returns the element being inserted.

/// learners contains the IDs of all learner nodes (maybe include self if
/// the local node is a learner) in the raft cluster.
/// learners only receives entries from the leader node. It does not vote
/// or promote itself.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean by not vote or promote itself? Does it mean promote_learner should never be called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ported from etcd. I think it means Learner can't make a proposal to promote itself to Voter. Only Voters can propose to promote a Learner to Voter. I will make the comment more clear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means the learner can't vote for other peers or vote for itself.

src/raft/raft.rs Outdated
if self.is_learner {
// TODO: learner may need to vote, in case of node down when confchange.
info!(
r#"{} [logterm: {}, index: {}, vote: {}] ignored {:?} from {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? See L1774.

src/raft/raft.rs Outdated
@@ -1177,6 +1217,11 @@ impl<T: Storage> Raft<T> {
}

fn handle_transfer_leader(&mut self, m: &Message, pr: &mut Progress) {
if self.is_learner {
debug!("{} is learner. Ignored transferring leadership", self.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.tag instead.

src/raft/raft.rs Outdated
// Ignore any redundant addNode calls (which can happen because the
// initial bootstrapping entries are applied twice).
if self.get_prs().voters().contains_key(&id) {
// If is_learner is true, we don't support change Voter to Learner,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a log for the first case.

src/raft/raft.rs Outdated
// New progress.
let mut pr = new_progress(self.raft_log.last_index() + 1, self.max_inflight);
pr.is_learner = is_learner;
pr.recent_active = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why set it to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ported from etcd.

@@ -1802,29 +1885,19 @@ impl<T: Storage> Raft<T> {
self.pending_conf = false;
}

pub fn set_progress(&mut self, id: u64, matched: u64, next_idx: u64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep the method and add a new arg is_learner? Looks like there are a lot of places using the same pattern.

}

#[allow(needless_lifetimes)]
pub fn iter_voters<'a>(&'a self) -> impl Iterator<Item = (&'a u64, &'a Progress)> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is not used anymore, can be removed.

}

#[allow(needless_lifetimes)]
pub fn iter_mut_voters<'a>(&'a mut self) -> impl Iterator<Item = (&'a u64, &'a mut Progress)> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

}

#[allow(needless_lifetimes)]
pub fn iter_mut_learners<'a>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

src/raft/raft.rs Outdated
info!(
"{} [logterm: {}, index: {}, vote: {}] ignored {:?} from {} \
[logterm: {}, index: {}] at term {}: learner can not vote",
self.id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.tag instead.

src/raft/raft.rs Outdated
if id == self.id {
error!(
"{} can't become learner when restores snapshot [index: {}, term: {}]",
self.id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

src/raft/raft.rs Outdated
@@ -1711,6 +1752,20 @@ impl<T: Storage> Raft<T> {
return Some(false);
}

if !self.is_learner {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is going to work. When a raft node is created, it's not learner by default. So there is no way for a new learner to accept snapshot. So should either set different flag for learner messages or also check if the state machine is initialized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can pass learners info in Config? So the enw learner can set self.is_learner = true in Raft::new.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a raft state machine is created via conf change, there is no way to tell if it's a learner or a voter in current implementations.

src/raft/raft.rs Outdated
return;
}

if !is_learner && self.mut_prs().promote_learner(id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If promotion succeeds, does its active flag need to be set?

src/raft/raft.rs Outdated
let last_index = self.raft_log.last_index();
self.set_progress(id, 0, last_index + 1);
self.set_progress(id, 0, last_index + 1, is_learner);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be ignored if is_learner is true, and self.get_prs().learners().contains_key(&id)?

src/raft/raft.rs Outdated
}

pub fn add_node(&mut self, id: u64) {
fn add_voter_or_learner(&mut self, id: u64, is_learner: bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep the logic in this function be same as etcd? The rewrite have introduced several problems already.

src/raft/raft.rs Outdated
self.prs = Some(prs);
}

pub fn get_prs(&self) -> &FlatMap<u64, Progress> {
pub fn get_prs(&self) -> &ProgressSet {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/get_prs/prs/

@hicqu
Copy link
Contributor Author

hicqu commented Dec 12, 2017

@BusyJay PTAL.

@@ -1427,7 +1433,11 @@ fn test_commit() {

let mut sm = new_test_raft(1, vec![1], 5, 1, store);
for (j, &v) in matches.iter().enumerate() {
sm.set_progress(j as u64 + 1, v, v + 1);
let pr = new_progress(ProgressState::default(), v, v + 1, 0, sm.max_inflight);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use set_progress?

}

pub fn promote_learner(&mut self, id: u64) {
match self.learners.entry(id) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use remove?

prs.insert(
*id,
Progress {
if prs.learners().get(id).is_some() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contains_key?

@@ -584,16 +589,17 @@ fn test_progress_resume_by_heartbeat_resp() {
let mut raft = new_test_raft(1, vec![1, 2], 5, 1, new_storage());
raft.become_candidate();
raft.become_leader();
raft.mut_prs().get_mut(&2).unwrap().paused = true;
raft.mut_prs().get_mut(2).unwrap().paused = true;
raft.mut_prs().get_mut(2).unwrap().paused = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why set twice?

}

#[test]
fn test_learner_election_timeout() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the comment for the test case.

// Promote n2 from learner to follower.
network.peers.get_mut(&1).unwrap().add_node(2);
network.peers.get_mut(&2).unwrap().add_node(2);
assert_eq!(network.peers[&2].state, StateRole::Follower);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check if it's learner.

Copy link
Contributor Author

@hicqu hicqu Dec 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In etcd it is

if n2.isLearner {
    t.Error("peer 2 is learner, want not")
}

I think they are same because n2 must not be leader?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if there is no bug. Unfortunately you can't assume there is no bug in tests.

n2.set_randomized_election_timeout(timeout);

for _ in 0..timeout {
n1.tick();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's meaningless to tick when it's not in a network.


let timeout = n1.get_election_timeout();
n1.set_randomized_election_timeout(timeout);
n2.set_randomized_election_timeout(timeout);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

network.send(vec![msg]);

assert_eq!(
network.peers.get_mut(&2).unwrap().raft_log.committed,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check node 1 too.


#[test]
fn test_restore_with_learner() {
let mut s = Snapshot::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use new_snapshot?

@hicqu
Copy link
Contributor Author

hicqu commented Dec 14, 2017

@BusyJay PTAL thanks.

Copy link
Member

@BusyJay BusyJay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still several places can be replaced by new_snapshot and new_message. Also please keep the comments of all ported test cases.

src/raft/raft.rs Outdated
///
/// The flag will be true if all of `Config.peers` and `Config.learners`
/// and `peers/learners` from `PeerStorage` are empty.
initialized: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just check self.peers and self.learners at L1769?

src/raft/raft.rs Outdated
@@ -1711,6 +1766,20 @@ impl<T: Storage> Raft<T> {
return Some(false);
}

if self.initialized && !self.is_learner {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't behave the same as Etcd now. I think it's reasonable but not sure if it's a good way. Another way is to add an additional flag to the messages sent to learners. Any ideas? For the reason of the changes see discussion at #2491 (comment).

/cc @siddontang @xiang90

@hicqu
Copy link
Contributor Author

hicqu commented Dec 15, 2017

@BusyJay PTAL thanks.

@BusyJay
Copy link
Member

BusyJay commented Dec 15, 2017

LGTM

@hicqu
Copy link
Contributor Author

hicqu commented Dec 15, 2017

@siddontang PTAL again, thanks.

@hicqu
Copy link
Contributor Author

hicqu commented Dec 15, 2017

/run-all-tests

@siddontang
Copy link
Contributor

PTAL @overvenus

progress
}

// We need explicit lifetime here because of a compiler bug.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the reference.

Copy link
Member

@overvenus overvenus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hicqu hicqu merged commit d780a5f into tikv:master Dec 18, 2017
@hicqu hicqu deleted the raft-learner branch December 18, 2017 05:15
@siddontang siddontang mentioned this pull request Dec 19, 2017
sticnarf pushed a commit to sticnarf/tikv that referenced this pull request Oct 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants