forked from etcd-io/etcd
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
354 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright 2019 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 confchange | ||
|
||
import ( | ||
pb "go.etcd.io/etcd/raft/raftpb" | ||
"go.etcd.io/etcd/raft/tracker" | ||
) | ||
|
||
// toConfChangeSingle translates a conf state into 1) a slice of operations creating | ||
// first the config that will become the outgoing one, and then the incoming one, and | ||
// b) another slice that, when applied to the config resulted from 1), represents the | ||
// ConfState. | ||
func toConfChangeSingle(cs pb.ConfState) (out []pb.ConfChangeSingle, in []pb.ConfChangeSingle) { | ||
for _, id := range cs.VotersOutgoing { | ||
// If there are outgoing voters, first add them one by one so that the | ||
// (non-joint) config has them all. | ||
out = append(out, pb.ConfChangeSingle{ | ||
Type: pb.ConfChangeAddNode, | ||
NodeID: id, | ||
}) | ||
|
||
} | ||
|
||
// NB: we're done constructing the outgoing slice, now on to the incoming | ||
// one (which will apply on top of the config created by the outgoing slice). | ||
|
||
// First, we'll remove all of the outgoing voters. | ||
for _, id := range cs.VotersOutgoing { | ||
in = append(in, pb.ConfChangeSingle{ | ||
Type: pb.ConfChangeRemoveNode, | ||
NodeID: id, | ||
}) | ||
} | ||
// Then we'll add the incoming voters and learners. | ||
for _, id := range cs.Voters { | ||
in = append(in, pb.ConfChangeSingle{ | ||
Type: pb.ConfChangeAddNode, | ||
NodeID: id, | ||
}) | ||
} | ||
for _, id := range cs.Learners { | ||
in = append(in, pb.ConfChangeSingle{ | ||
Type: pb.ConfChangeAddLearnerNode, | ||
NodeID: id, | ||
}) | ||
} | ||
// Same for LearnersNext; these are nodes we want to be learners but which | ||
// are currently voters in the outgoing config. | ||
for _, id := range cs.LearnersNext { | ||
in = append(in, pb.ConfChangeSingle{ | ||
Type: pb.ConfChangeAddLearnerNode, | ||
NodeID: id, | ||
}) | ||
} | ||
return out, in | ||
} | ||
|
||
func chain(chg Changer, ops ...func(Changer) (tracker.Config, tracker.ProgressMap, error)) (tracker.Config, tracker.ProgressMap, error) { | ||
for _, op := range ops { | ||
cfg, prs, err := op(chg) | ||
if err != nil { | ||
return tracker.Config{}, nil, err | ||
} | ||
chg.Tracker.Config = cfg | ||
chg.Tracker.Progress = prs | ||
} | ||
return chg.Tracker.Config, chg.Tracker.Progress, nil | ||
} | ||
|
||
// Restore takes a Changer (which must represent an empty configuration), and | ||
// brings returns the state described by the supplied ConfState. | ||
// | ||
// TODO(tbg) it's silly that this takes a Changer. Unravel this by making sure | ||
// the Changer only needs a ProgressMap (not a whole Tracker) at which point | ||
// this can just take LastIndex and MaxInflight directly instead and cook up | ||
// the results from that alone. | ||
func Restore(chg Changer, cs pb.ConfState) (tracker.Config, tracker.ProgressMap, error) { | ||
outgoing, incoming := toConfChangeSingle(cs) | ||
|
||
var ops []func(Changer) (tracker.Config, tracker.ProgressMap, error) | ||
|
||
// First, apply all of the changes of the outgoing config one by one (so that | ||
// it temporarily becomes the incoming active config. For example, if the | ||
// config is (1 2 3)&(2 3 4), this will establish (2 3 4)&(). | ||
for _, cc := range outgoing { | ||
cc := cc // loop-local copy | ||
ops = append(ops, func(chg Changer) (tracker.Config, tracker.ProgressMap, error) { | ||
return chg.Simple(cc) | ||
}) | ||
} | ||
|
||
if len(outgoing) > 0 { | ||
// If we added anything above, enter the joint state, which rotates the | ||
// above additions into the outgoing config, and adds the incoming config | ||
// in. Continuing the example above, we'd get (1 2 3)&(2 3 4), i.e. the | ||
// incoming operations would be removing 2,3,4 and then adding in 1,2,3. | ||
ops = append(ops, func(chg Changer) (tracker.Config, tracker.ProgressMap, error) { | ||
return chg.EnterJoint(cs.AutoLeave, incoming...) | ||
}) | ||
} else { | ||
// No outgoing config, so just apply the incoming changes one by one. | ||
for _, cc := range incoming { | ||
cc := cc // loop-local copy | ||
ops = append(ops, func(chg Changer) (tracker.Config, tracker.ProgressMap, error) { | ||
return chg.Simple(cc) | ||
}) | ||
} | ||
} | ||
|
||
return chain(chg, ops...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright 2019 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 confchange | ||
|
||
import ( | ||
"math/rand" | ||
"reflect" | ||
"sort" | ||
"testing" | ||
"testing/quick" | ||
|
||
pb "go.etcd.io/etcd/raft/raftpb" | ||
"go.etcd.io/etcd/raft/tracker" | ||
) | ||
|
||
type rndConfChange pb.ConfState | ||
|
||
// Generate creates a random (valid) ConfState for use with quickcheck. | ||
func (_ rndConfChange) Generate(rand *rand.Rand, _ int) reflect.Value { | ||
conv := func(sl []int) []uint64 { | ||
out := make([]uint64, len(sl)) | ||
for i := range sl { | ||
out[i] = uint64(sl[i] + 1) | ||
} | ||
return out | ||
} | ||
var cs pb.ConfState | ||
nVoters := 1 + rand.Intn(5) // at least one voter | ||
nLearners := rand.Intn(5) | ||
nRemovedVoters := rand.Intn(3) | ||
|
||
ids := conv(rand.Perm(2 * (nVoters + nLearners + nRemovedVoters))) | ||
|
||
cs.Voters = ids[:nVoters] | ||
ids = ids[nVoters:] | ||
|
||
if nLearners > 0 { | ||
cs.Learners = ids[:nLearners] | ||
ids = ids[nLearners:] | ||
} | ||
|
||
nOutgoingRetainedVoters := rand.Intn(nVoters) | ||
if nOutgoingRetainedVoters > 0 || nRemovedVoters > 0 { | ||
cs.VotersOutgoing = append([]uint64(nil), cs.Voters[:nOutgoingRetainedVoters]...) | ||
cs.VotersOutgoing = append(cs.VotersOutgoing, ids[:nRemovedVoters]...) | ||
} | ||
// Next learners must be removed voters (i.e. demotions). | ||
if nRemovedVoters > 0 { | ||
nLearnersNext := rand.Intn(nRemovedVoters) | ||
if nLearnersNext > 0 { | ||
cs.LearnersNext = ids[:nLearnersNext] | ||
} | ||
} | ||
|
||
cs.AutoLeave = len(cs.VotersOutgoing) > 0 && rand.Intn(2) == 1 | ||
return reflect.ValueOf(rndConfChange(cs)) | ||
} | ||
|
||
func TestRestore(t *testing.T) { | ||
cfg := quick.Config{MaxCount: 1000} | ||
|
||
f := func(rndCS rndConfChange) bool { | ||
cs := pb.ConfState(rndCS) | ||
chg := Changer{ | ||
Tracker: tracker.MakeProgressTracker(20), | ||
LastIndex: 10, | ||
} | ||
cfg, prs, err := Restore(chg, cs) | ||
if err != nil { | ||
t.Error(err) | ||
return false | ||
} | ||
chg.Tracker.Config = cfg | ||
chg.Tracker.Progress = prs | ||
|
||
for _, sl := range [][]uint64{ | ||
cs.Voters, | ||
cs.Learners, | ||
cs.VotersOutgoing, | ||
cs.LearnersNext, | ||
} { | ||
sort.Slice(sl, func(i, j int) bool { return sl[i] < sl[j] }) | ||
} | ||
|
||
cs2 := chg.Tracker.ConfState() | ||
if reflect.DeepEqual(cs, cs2) { | ||
return true // success | ||
} | ||
t.Errorf(` | ||
before: %+#v | ||
after: %+#v`, cs, cs2) | ||
return false | ||
} | ||
|
||
if err := quick.Check(f, &cfg); err != nil { | ||
t.Error(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.