Scott Baker | 8487c5d | 2019-10-18 12:49:46 -0700 | [diff] [blame^] | 1 | // Copyright 2019 The etcd Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package raftpb |
| 16 | |
| 17 | import ( |
| 18 | "fmt" |
| 19 | "strconv" |
| 20 | "strings" |
| 21 | |
| 22 | "github.com/gogo/protobuf/proto" |
| 23 | ) |
| 24 | |
| 25 | // ConfChangeI abstracts over ConfChangeV2 and (legacy) ConfChange to allow |
| 26 | // treating them in a unified manner. |
| 27 | type ConfChangeI interface { |
| 28 | AsV2() ConfChangeV2 |
| 29 | AsV1() (ConfChange, bool) |
| 30 | } |
| 31 | |
| 32 | // MarshalConfChange calls Marshal on the underlying ConfChange or ConfChangeV2 |
| 33 | // and returns the result along with the corresponding EntryType. |
| 34 | func MarshalConfChange(c ConfChangeI) (EntryType, []byte, error) { |
| 35 | var typ EntryType |
| 36 | var ccdata []byte |
| 37 | var err error |
| 38 | if ccv1, ok := c.AsV1(); ok { |
| 39 | typ = EntryConfChange |
| 40 | ccdata, err = ccv1.Marshal() |
| 41 | } else { |
| 42 | ccv2 := c.AsV2() |
| 43 | typ = EntryConfChangeV2 |
| 44 | ccdata, err = ccv2.Marshal() |
| 45 | } |
| 46 | return typ, ccdata, err |
| 47 | } |
| 48 | |
| 49 | // AsV2 returns a V2 configuration change carrying out the same operation. |
| 50 | func (c ConfChange) AsV2() ConfChangeV2 { |
| 51 | return ConfChangeV2{ |
| 52 | Changes: []ConfChangeSingle{{ |
| 53 | Type: c.Type, |
| 54 | NodeID: c.NodeID, |
| 55 | }}, |
| 56 | Context: c.Context, |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | // AsV1 returns the ConfChange and true. |
| 61 | func (c ConfChange) AsV1() (ConfChange, bool) { |
| 62 | return c, true |
| 63 | } |
| 64 | |
| 65 | // AsV2 is the identity. |
| 66 | func (c ConfChangeV2) AsV2() ConfChangeV2 { return c } |
| 67 | |
| 68 | // AsV1 returns ConfChange{} and false. |
| 69 | func (c ConfChangeV2) AsV1() (ConfChange, bool) { return ConfChange{}, false } |
| 70 | |
| 71 | // EnterJoint returns two bools. The second bool is true if and only if this |
| 72 | // config change will use Joint Consensus, which is the case if it contains more |
| 73 | // than one change or if the use of Joint Consensus was requested explicitly. |
| 74 | // The first bool can only be true if second one is, and indicates whether the |
| 75 | // Joint State will be left automatically. |
| 76 | func (c *ConfChangeV2) EnterJoint() (autoLeave bool, ok bool) { |
| 77 | // NB: in theory, more config changes could qualify for the "simple" |
| 78 | // protocol but it depends on the config on top of which the changes apply. |
| 79 | // For example, adding two learners is not OK if both nodes are part of the |
| 80 | // base config (i.e. two voters are turned into learners in the process of |
| 81 | // applying the conf change). In practice, these distinctions should not |
| 82 | // matter, so we keep it simple and use Joint Consensus liberally. |
| 83 | if c.Transition != ConfChangeTransitionAuto || len(c.Changes) > 1 { |
| 84 | // Use Joint Consensus. |
| 85 | var autoLeave bool |
| 86 | switch c.Transition { |
| 87 | case ConfChangeTransitionAuto: |
| 88 | autoLeave = true |
| 89 | case ConfChangeTransitionJointImplicit: |
| 90 | autoLeave = true |
| 91 | case ConfChangeTransitionJointExplicit: |
| 92 | default: |
| 93 | panic(fmt.Sprintf("unknown transition: %+v", c)) |
| 94 | } |
| 95 | return autoLeave, true |
| 96 | } |
| 97 | return false, false |
| 98 | } |
| 99 | |
| 100 | // LeaveJoint is true if the configuration change leaves a joint configuration. |
| 101 | // This is the case if the ConfChangeV2 is zero, with the possible exception of |
| 102 | // the Context field. |
| 103 | func (c *ConfChangeV2) LeaveJoint() bool { |
| 104 | cpy := *c |
| 105 | cpy.Context = nil |
| 106 | return proto.Equal(&cpy, &ConfChangeV2{}) |
| 107 | } |
| 108 | |
| 109 | // ConfChangesFromString parses a Space-delimited sequence of operations into a |
| 110 | // slice of ConfChangeSingle. The supported operations are: |
| 111 | // - vn: make n a voter, |
| 112 | // - ln: make n a learner, |
| 113 | // - rn: remove n, and |
| 114 | // - un: update n. |
| 115 | func ConfChangesFromString(s string) ([]ConfChangeSingle, error) { |
| 116 | var ccs []ConfChangeSingle |
| 117 | toks := strings.Split(strings.TrimSpace(s), " ") |
| 118 | if toks[0] == "" { |
| 119 | toks = nil |
| 120 | } |
| 121 | for _, tok := range toks { |
| 122 | if len(tok) < 2 { |
| 123 | return nil, fmt.Errorf("unknown token %s", tok) |
| 124 | } |
| 125 | var cc ConfChangeSingle |
| 126 | switch tok[0] { |
| 127 | case 'v': |
| 128 | cc.Type = ConfChangeAddNode |
| 129 | case 'l': |
| 130 | cc.Type = ConfChangeAddLearnerNode |
| 131 | case 'r': |
| 132 | cc.Type = ConfChangeRemoveNode |
| 133 | case 'u': |
| 134 | cc.Type = ConfChangeUpdateNode |
| 135 | default: |
| 136 | return nil, fmt.Errorf("unknown input: %s", tok) |
| 137 | } |
| 138 | id, err := strconv.ParseUint(tok[1:], 10, 64) |
| 139 | if err != nil { |
| 140 | return nil, err |
| 141 | } |
| 142 | cc.NodeID = id |
| 143 | ccs = append(ccs, cc) |
| 144 | } |
| 145 | return ccs, nil |
| 146 | } |
| 147 | |
| 148 | // ConfChangesToString is the inverse to ConfChangesFromString. |
| 149 | func ConfChangesToString(ccs []ConfChangeSingle) string { |
| 150 | var buf strings.Builder |
| 151 | for i, cc := range ccs { |
| 152 | if i > 0 { |
| 153 | buf.WriteByte(' ') |
| 154 | } |
| 155 | switch cc.Type { |
| 156 | case ConfChangeAddNode: |
| 157 | buf.WriteByte('v') |
| 158 | case ConfChangeAddLearnerNode: |
| 159 | buf.WriteByte('l') |
| 160 | case ConfChangeRemoveNode: |
| 161 | buf.WriteByte('r') |
| 162 | case ConfChangeUpdateNode: |
| 163 | buf.WriteByte('u') |
| 164 | default: |
| 165 | buf.WriteString("unknown") |
| 166 | } |
| 167 | fmt.Fprintf(&buf, "%d", cc.NodeID) |
| 168 | } |
| 169 | return buf.String() |
| 170 | } |