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  }