| // 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 raftpb |
| |
| import ( |
| "fmt" |
| "strconv" |
| "strings" |
| |
| "github.com/gogo/protobuf/proto" |
| ) |
| |
| // ConfChangeI abstracts over ConfChangeV2 and (legacy) ConfChange to allow |
| // treating them in a unified manner. |
| type ConfChangeI interface { |
| AsV2() ConfChangeV2 |
| AsV1() (ConfChange, bool) |
| } |
| |
| // MarshalConfChange calls Marshal on the underlying ConfChange or ConfChangeV2 |
| // and returns the result along with the corresponding EntryType. |
| func MarshalConfChange(c ConfChangeI) (EntryType, []byte, error) { |
| var typ EntryType |
| var ccdata []byte |
| var err error |
| if ccv1, ok := c.AsV1(); ok { |
| typ = EntryConfChange |
| ccdata, err = ccv1.Marshal() |
| } else { |
| ccv2 := c.AsV2() |
| typ = EntryConfChangeV2 |
| ccdata, err = ccv2.Marshal() |
| } |
| return typ, ccdata, err |
| } |
| |
| // AsV2 returns a V2 configuration change carrying out the same operation. |
| func (c ConfChange) AsV2() ConfChangeV2 { |
| return ConfChangeV2{ |
| Changes: []ConfChangeSingle{{ |
| Type: c.Type, |
| NodeID: c.NodeID, |
| }}, |
| Context: c.Context, |
| } |
| } |
| |
| // AsV1 returns the ConfChange and true. |
| func (c ConfChange) AsV1() (ConfChange, bool) { |
| return c, true |
| } |
| |
| // AsV2 is the identity. |
| func (c ConfChangeV2) AsV2() ConfChangeV2 { return c } |
| |
| // AsV1 returns ConfChange{} and false. |
| func (c ConfChangeV2) AsV1() (ConfChange, bool) { return ConfChange{}, false } |
| |
| // EnterJoint returns two bools. The second bool is true if and only if this |
| // config change will use Joint Consensus, which is the case if it contains more |
| // than one change or if the use of Joint Consensus was requested explicitly. |
| // The first bool can only be true if second one is, and indicates whether the |
| // Joint State will be left automatically. |
| func (c *ConfChangeV2) EnterJoint() (autoLeave bool, ok bool) { |
| // NB: in theory, more config changes could qualify for the "simple" |
| // protocol but it depends on the config on top of which the changes apply. |
| // For example, adding two learners is not OK if both nodes are part of the |
| // base config (i.e. two voters are turned into learners in the process of |
| // applying the conf change). In practice, these distinctions should not |
| // matter, so we keep it simple and use Joint Consensus liberally. |
| if c.Transition != ConfChangeTransitionAuto || len(c.Changes) > 1 { |
| // Use Joint Consensus. |
| var autoLeave bool |
| switch c.Transition { |
| case ConfChangeTransitionAuto: |
| autoLeave = true |
| case ConfChangeTransitionJointImplicit: |
| autoLeave = true |
| case ConfChangeTransitionJointExplicit: |
| default: |
| panic(fmt.Sprintf("unknown transition: %+v", c)) |
| } |
| return autoLeave, true |
| } |
| return false, false |
| } |
| |
| // LeaveJoint is true if the configuration change leaves a joint configuration. |
| // This is the case if the ConfChangeV2 is zero, with the possible exception of |
| // the Context field. |
| func (c *ConfChangeV2) LeaveJoint() bool { |
| cpy := *c |
| cpy.Context = nil |
| return proto.Equal(&cpy, &ConfChangeV2{}) |
| } |
| |
| // ConfChangesFromString parses a Space-delimited sequence of operations into a |
| // slice of ConfChangeSingle. The supported operations are: |
| // - vn: make n a voter, |
| // - ln: make n a learner, |
| // - rn: remove n, and |
| // - un: update n. |
| func ConfChangesFromString(s string) ([]ConfChangeSingle, error) { |
| var ccs []ConfChangeSingle |
| toks := strings.Split(strings.TrimSpace(s), " ") |
| if toks[0] == "" { |
| toks = nil |
| } |
| for _, tok := range toks { |
| if len(tok) < 2 { |
| return nil, fmt.Errorf("unknown token %s", tok) |
| } |
| var cc ConfChangeSingle |
| switch tok[0] { |
| case 'v': |
| cc.Type = ConfChangeAddNode |
| case 'l': |
| cc.Type = ConfChangeAddLearnerNode |
| case 'r': |
| cc.Type = ConfChangeRemoveNode |
| case 'u': |
| cc.Type = ConfChangeUpdateNode |
| default: |
| return nil, fmt.Errorf("unknown input: %s", tok) |
| } |
| id, err := strconv.ParseUint(tok[1:], 10, 64) |
| if err != nil { |
| return nil, err |
| } |
| cc.NodeID = id |
| ccs = append(ccs, cc) |
| } |
| return ccs, nil |
| } |
| |
| // ConfChangesToString is the inverse to ConfChangesFromString. |
| func ConfChangesToString(ccs []ConfChangeSingle) string { |
| var buf strings.Builder |
| for i, cc := range ccs { |
| if i > 0 { |
| buf.WriteByte(' ') |
| } |
| switch cc.Type { |
| case ConfChangeAddNode: |
| buf.WriteByte('v') |
| case ConfChangeAddLearnerNode: |
| buf.WriteByte('l') |
| case ConfChangeRemoveNode: |
| buf.WriteByte('r') |
| case ConfChangeUpdateNode: |
| buf.WriteByte('u') |
| default: |
| buf.WriteString("unknown") |
| } |
| fmt.Fprintf(&buf, "%d", cc.NodeID) |
| } |
| return buf.String() |
| } |