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 15package raftpb 16 17import ( 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. 27type 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. 34func 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. 50func (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. 61func (c ConfChange) AsV1() (ConfChange, bool) { 62 return c, true 63} 64 65// AsV2 is the identity. 66func (c ConfChangeV2) AsV2() ConfChangeV2 { return c } 67 68// AsV1 returns ConfChange{} and false. 69func (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. 76func (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. 103func (c ConfChangeV2) LeaveJoint() bool { 104 // NB: c is already a copy. 105 c.Context = nil 106 return proto.Equal(&c, &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. 115func 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. 149func 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} 171