github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/member_list.go (about) 1 package cluster 2 3 import ( 4 "context" 5 "log/slog" 6 "sync" 7 8 "github.com/asynkron/protoactor-go/actor" 9 "github.com/asynkron/protoactor-go/eventstream" 10 "github.com/asynkron/protoactor-go/remote" 11 "google.golang.org/protobuf/types/known/anypb" 12 ) 13 14 // MemberList is responsible to keep track of the current cluster topology 15 // it does so by listening to changes from the ClusterProvider. 16 // the default ClusterProvider is consul.ConsulProvider which uses the Consul HTTP API to scan for changes 17 type MemberList struct { 18 cluster *Cluster 19 mutex sync.RWMutex 20 members *MemberSet 21 memberStrategyByKind map[string]MemberStrategy 22 23 eventSteam *eventstream.EventStream 24 topologyConsensus ConsensusHandler 25 } 26 27 func NewMemberList(cluster *Cluster) *MemberList { 28 memberList := &MemberList{ 29 cluster: cluster, 30 members: emptyMemberSet, 31 memberStrategyByKind: make(map[string]MemberStrategy), 32 eventSteam: cluster.ActorSystem.EventStream, 33 } 34 memberList.eventSteam.Subscribe(func(evt interface{}) { 35 switch t := evt.(type) { 36 case *GossipUpdate: 37 if t.Key != "topology" { 38 break 39 } 40 41 // get blocked members from all other member states 42 // and merge that without own blocked set 43 var topology ClusterTopology 44 if err := t.Value.UnmarshalTo(&topology); err != nil { 45 cluster.Logger().Warn("could not unpack into ClusterTopology proto.Message form Any", slog.Any("error", err)) 46 47 break 48 } 49 blocked := topology.Blocked 50 memberList.cluster.Remote.BlockList().Block(blocked...) 51 } 52 }) 53 54 return memberList 55 } 56 57 func (ml *MemberList) stopMemberList() { 58 // ml.cluster.ActorSystem.EventStream.Unsubscribe(ml.membershipSub) 59 } 60 61 func (ml *MemberList) InitializeTopologyConsensus() { 62 ml.topologyConsensus = ml.cluster.Gossip.RegisterConsensusCheck("topology", func(any *anypb.Any) interface{} { 63 var topology ClusterTopology 64 if unpackErr := any.UnmarshalTo(&topology); unpackErr != nil { 65 ml.cluster.Logger().Error("could not unpack topology message", slog.Any("error", unpackErr)) 66 67 return nil 68 } 69 70 return topology.TopologyHash 71 }) 72 } 73 74 func (ml *MemberList) TopologyConsensus(ctx context.Context) (uint64, bool) { 75 result, ok := ml.topologyConsensus.TryGetConsensus(ctx) 76 if ok { 77 res, _ := result.(uint64) 78 79 return res, true 80 } 81 82 return 0, false 83 } 84 85 func (ml *MemberList) getPartitionMember(name, kind string) string { 86 ml.mutex.RLock() 87 defer ml.mutex.RUnlock() 88 89 var res string 90 if memberStrategy, ok := ml.memberStrategyByKind[kind]; ok { 91 res = memberStrategy.GetPartition(name) 92 } 93 94 return res 95 } 96 97 func (ml *MemberList) getPartitionMemberV2(clusterIdentity *ClusterIdentity) string { 98 ml.mutex.RLock() 99 defer ml.mutex.RUnlock() 100 101 if ms, ok := ml.memberStrategyByKind[clusterIdentity.Kind]; ok { 102 return ms.GetPartition(clusterIdentity.Identity) 103 } 104 105 return "" 106 } 107 108 func (ml *MemberList) GetActivatorMember(kind string, requestSourceAddress string) string { 109 ml.mutex.RLock() 110 defer ml.mutex.RUnlock() 111 112 var res string 113 if memberStrategy, ok := ml.memberStrategyByKind[kind]; ok { 114 res = memberStrategy.GetActivator(requestSourceAddress) 115 } 116 117 return res 118 } 119 120 func (ml *MemberList) Length() int { 121 return ml.members.Len() 122 } 123 124 func (ml *MemberList) Members() *MemberSet { 125 return ml.members 126 } 127 128 func (ml *MemberList) UpdateClusterTopology(members Members) { 129 ml.mutex.Lock() 130 defer ml.mutex.Unlock() 131 132 // TLDR: 133 // this method basically filters out any member status in the blocked list 134 // then makes a delta between new and old members 135 // notifying the cluster accordingly which members left or joined 136 137 topology, done, active, joined, left := ml.getTopologyChanges(members) 138 if done { 139 return 140 } 141 142 // include any new blocked members into the known set of blocked members 143 for _, m := range left.Members() { 144 ml.cluster.Remote.BlockList().Block(m.Id) 145 } 146 147 ml.members = active 148 149 // notify that these members left 150 for _, m := range left.Members() { 151 ml.memberLeave(m) 152 ml.TerminateMember(m) 153 } 154 155 // notify that these members joined 156 for _, m := range joined.Members() { 157 ml.memberJoin(m) 158 } 159 160 ml.cluster.ActorSystem.EventStream.Publish(topology) 161 162 ml.cluster.Logger().Info("Updated ClusterTopology", 163 slog.Uint64("topology-hash", topology.TopologyHash), 164 slog.Int("members", len(topology.Members)), 165 slog.Int("joined", len(topology.Joined)), 166 slog.Int("left", len(topology.Left)), 167 slog.Int("blocked", len(topology.Blocked)), 168 slog.Int("membersFromProvider", len(members))) 169 } 170 171 func (ml *MemberList) memberJoin(joiningMember *Member) { 172 ml.cluster.Logger().Info("member joined", slog.String("member", joiningMember.Id)) 173 174 for _, kind := range joiningMember.Kinds { 175 if ml.memberStrategyByKind[kind] == nil { 176 ml.memberStrategyByKind[kind] = ml.getMemberStrategyByKind(kind) 177 } 178 179 ml.memberStrategyByKind[kind].AddMember(joiningMember) 180 } 181 } 182 183 func (ml *MemberList) memberLeave(leavingMember *Member) { 184 for _, kind := range leavingMember.Kinds { 185 if ml.memberStrategyByKind[kind] == nil { 186 continue 187 } 188 189 ml.memberStrategyByKind[kind].RemoveMember(leavingMember) 190 } 191 } 192 193 func (ml *MemberList) getTopologyChanges(members Members) (topology *ClusterTopology, unchanged bool, active *MemberSet, joined *MemberSet, left *MemberSet) { 194 memberSet := NewMemberSet(members) 195 196 // get active members 197 // (this bit means that we will never allow a member that failed a health check to join back in) 198 blocked := ml.cluster.GetBlockedMembers().ToSlice() 199 200 active = memberSet.ExceptIds(blocked) 201 202 // nothing changed? exit 203 if active.Equals(ml.members) { 204 return nil, true, nil, nil, nil 205 } 206 207 left = ml.members.Except(active) 208 joined = active.Except(ml.members) 209 210 topology = &ClusterTopology{ 211 TopologyHash: active.TopologyHash(), 212 Members: active.Members(), 213 Left: left.Members(), 214 Joined: joined.Members(), 215 } 216 217 return topology, false, active, joined, left 218 } 219 220 func (ml *MemberList) TerminateMember(m *Member) { 221 // tell the world that this endpoint should is no longer relevant 222 ml.cluster.ActorSystem.EventStream.Publish(&remote.EndpointTerminatedEvent{ 223 Address: m.Address(), 224 }) 225 } 226 227 func (ml *MemberList) BroadcastEvent(message interface{}, includeSelf bool) { 228 for _, m := range ml.members.members { 229 if !includeSelf && m.Id == ml.cluster.ActorSystem.ID { 230 continue 231 } 232 233 pid := actor.NewPID(m.Address(), "eventstream") 234 ml.cluster.ActorSystem.Root.Send(pid, message) 235 } 236 } 237 238 func (ml *MemberList) ContainsMemberID(memberID string) bool { 239 return ml.members.ContainsID(memberID) 240 } 241 242 func (ml *MemberList) getMemberStrategyByKind(kind string) MemberStrategy { 243 ml.cluster.Logger().Info("creating member strategy", slog.String("kind", kind)) 244 245 clusterKind, ok := ml.cluster.TryGetClusterKind(kind) 246 247 if ok { 248 if clusterKind.Strategy != nil { 249 return clusterKind.Strategy 250 } 251 } 252 253 strategy := ml.cluster.Config.MemberStrategyBuilder(ml.cluster, kind) 254 if strategy != nil { 255 return strategy 256 } 257 258 return newDefaultMemberStrategy(ml.cluster, kind) 259 }