github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/consensus_check_builder.go (about) 1 // Copyright (C) 2015-2022 Asynkron AB All rights reserved 2 3 package cluster 4 5 import ( 6 "fmt" 7 "log/slog" 8 "strings" 9 10 "google.golang.org/protobuf/types/known/anypb" 11 ) 12 13 type ConsensusCheckDefinition interface { 14 Check() *ConsensusCheck 15 AffectedKeys() map[string]struct{} 16 } 17 18 type consensusValue struct { 19 Key string 20 Value func(*anypb.Any) interface{} 21 } 22 23 type consensusMemberValue struct { 24 memberID string 25 key string 26 value uint64 27 } 28 29 type ConsensusCheckBuilder struct { 30 getConsensusValues []*consensusValue 31 check ConsensusChecker 32 logger *slog.Logger 33 } 34 35 func NewConsensusCheckBuilder(logger *slog.Logger, key string, getValue func(*anypb.Any) interface{}) *ConsensusCheckBuilder { 36 builder := ConsensusCheckBuilder{ 37 getConsensusValues: []*consensusValue{ 38 { 39 Key: key, 40 Value: getValue, 41 }, 42 }, 43 logger: logger, 44 } 45 builder.check = builder.build() 46 return &builder 47 } 48 49 // Build builds a new ConsensusHandler and ConsensusCheck values and returns pointers to them 50 func (ccb *ConsensusCheckBuilder) Build() (ConsensusHandler, *ConsensusCheck) { 51 handle := NewGossipConsensusHandler() 52 onConsensus := handle.TrySetConsensus 53 lostConsensus := handle.TryResetConsensus 54 55 check := func() *ConsensusCheck { 56 hasConsensus := ccb.Check() 57 hadConsensus := false 58 59 checkConsensus := func(state *GossipState, members map[string]empty) { 60 consensus, value := hasConsensus(state, members) 61 if consensus { 62 if hadConsensus { 63 return 64 } 65 66 onConsensus(value) 67 hadConsensus = true 68 } else if hadConsensus { 69 lostConsensus() 70 hadConsensus = false 71 } 72 } 73 74 consensusCheck := NewConsensusCheck(ccb.AffectedKeys(), checkConsensus) 75 return &consensusCheck 76 } 77 78 return handle, check() 79 } 80 81 func (ccb *ConsensusCheckBuilder) Check() ConsensusChecker { return ccb.check } 82 83 func (ccb *ConsensusCheckBuilder) AffectedKeys() []string { 84 var keys []string 85 for _, value := range ccb.getConsensusValues { 86 keys = append(keys, value.Key) 87 } 88 return keys 89 } 90 91 func (ccb *ConsensusCheckBuilder) MapToValue(valueTuple *consensusValue) func(string, *GossipMemberState) (string, string, uint64) { 92 // REVISIT: in .NET implementation the ConsensusCheckBuilder can be of any given T type 93 // so this method returns (string, string, T) in .NET, it just feels wrong to 94 // return an interface{} from here as so far only checkers for uint64 are 95 // being used but this is not acceptable, and we shall put this implementation 96 // on par with .NET version, so maybe with new go1.18 generics or making the 97 // ConsensusCheckBuilder struct to store an additional field of type empty 98 // interface to operate with internally and then provide of a custom callback 99 // from users of the data structure to convert back and forth ¯\_(ツ)_/¯ 100 key := valueTuple.Key 101 unpack := valueTuple.Value 102 103 return func(member string, state *GossipMemberState) (string, string, uint64) { 104 var value uint64 105 106 gossipKey, ok := state.Values[key] 107 if !ok { 108 value = 0 109 } else { 110 // REVISIT: the valueTuple is here supposedly to be able to convert 111 // the protobuf Any values contained by GossipMemberState 112 // into the right value, this is true in the .NET version 113 // as ConsensusCheckBuilder is defined as a generic type 114 // ConsensusCheckBuilder<T> so the unpacker can unpack from 115 // Any into T, but we can not do that (for now) so we have 116 // to stick to unpack to the concrete uint64 type here 117 value = unpack(gossipKey.Value).(uint64) 118 } 119 return member, key, value 120 } 121 } 122 123 func (ccb *ConsensusCheckBuilder) build() func(*GossipState, map[string]empty) (bool, interface{}) { 124 getValidMemberStates := func(state *GossipState, ids map[string]empty, result []map[string]*GossipMemberState) { 125 for member, memberState := range state.Members { 126 if _, ok := ids[member]; ok { 127 result = append(result, map[string]*GossipMemberState{ 128 member: memberState, 129 }) 130 } 131 } 132 } 133 134 showLog := func(hasConsensus bool, topologyHash uint64, valueTuples []*consensusMemberValue) { 135 if ccb.logger.Enabled(nil, slog.LevelDebug) { 136 groups := map[string]int{} 137 for _, memberValue := range valueTuples { 138 key := fmt.Sprintf("%s:%d", memberValue.key, memberValue.value) 139 if _, ok := groups[key]; ok { 140 groups[key]++ 141 } else { 142 groups[key] = 1 143 } 144 } 145 146 for k, value := range groups { 147 suffix := strings.Split(k, ":")[0] 148 if value > 1 { 149 suffix = fmt.Sprintf("%s, %d nodes", k, value) 150 } 151 ccb.logger.Debug("consensus", slog.Bool("consensus", hasConsensus), slog.String("values", suffix)) 152 } 153 } 154 } 155 156 if len(ccb.getConsensusValues) == 1 { 157 mapToValue := ccb.MapToValue(ccb.getConsensusValues[0]) 158 159 return func(state *GossipState, ids map[string]empty) (bool, interface{}) { 160 var memberStates []map[string]*GossipMemberState 161 getValidMemberStates(state, ids, memberStates) 162 163 if len(memberStates) < len(ids) { // Not all members have state... 164 return false, nil 165 } 166 167 var valueTuples []*consensusMemberValue 168 for _, memberState := range memberStates { 169 for id, state := range memberState { 170 member, key, value := mapToValue(id, state) 171 valueTuples = append(valueTuples, &consensusMemberValue{member, key, value}) 172 } 173 } 174 175 hasConsensus, topologyHash := ccb.HasConsensus(valueTuples) 176 showLog(hasConsensus, topologyHash, valueTuples) 177 178 return hasConsensus, topologyHash 179 } 180 } 181 182 return func(state *GossipState, ids map[string]empty) (bool, interface{}) { 183 var memberStates []map[string]*GossipMemberState 184 getValidMemberStates(state, ids, memberStates) 185 186 if len(memberStates) < len(ids) { // Not all members have state... 187 return false, nil 188 } 189 190 var valueTuples []*consensusMemberValue 191 for _, consensusValues := range ccb.getConsensusValues { 192 mapToValue := ccb.MapToValue(consensusValues) 193 for _, memberState := range memberStates { 194 for id, state := range memberState { 195 member, key, value := mapToValue(id, state) 196 valueTuples = append(valueTuples, &consensusMemberValue{member, key, value}) 197 } 198 } 199 } 200 201 hasConsensus, topologyHash := ccb.HasConsensus(valueTuples) 202 showLog(hasConsensus, topologyHash, valueTuples) 203 204 return hasConsensus, topologyHash 205 } 206 } 207 208 func (ccb *ConsensusCheckBuilder) HasConsensus(memberValues []*consensusMemberValue) (bool, uint64) { 209 var hasConsensus bool 210 var topologyHash uint64 211 212 if len(memberValues) == 0 { 213 return hasConsensus, topologyHash 214 } 215 216 first := memberValues[0] 217 for i, next := range memberValues { 218 if i == 0 { 219 continue 220 } 221 222 if first.value != next.value { 223 return hasConsensus, topologyHash 224 } 225 } 226 227 hasConsensus = true 228 topologyHash = first.value 229 return hasConsensus, topologyHash 230 }