github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/informer.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  	"math/rand"
     9  	"reflect"
    10  	"time"
    11  
    12  	"github.com/asynkron/gofun/set"
    13  	"github.com/asynkron/protoactor-go/actor"
    14  	"google.golang.org/protobuf/proto"
    15  )
    16  
    17  const (
    18  	TopologyKey       string = "topology"
    19  	HearthbeatKey     string = "heathbeat"
    20  	GracefullyLeftKey string = "left"
    21  )
    22  
    23  // create and seed a pseudo random numbers generator
    24  var rnd = rand.New(rand.NewSource(time.Now().UnixMicro()))
    25  
    26  // The Informer data structure implements the Gossip interface
    27  type Informer struct {
    28  	myID              string
    29  	localSeqNumber    int64
    30  	state             *GossipState
    31  	committedOffsets  map[string]int64
    32  	activeMemberIDs   map[string]empty
    33  	otherMembers      []*Member
    34  	consensusChecks   *ConsensusChecks
    35  	getBlockedMembers func() set.Set[string]
    36  	gossipFanOut      int
    37  	gossipMaxSend     int
    38  	throttler         actor.ShouldThrottle
    39  	logger            *slog.Logger
    40  }
    41  
    42  // makes sure Informer complies with the Gossip interface
    43  var _ Gossip = (*Informer)(nil)
    44  
    45  // Creates a new Informer value with the given properties and returns
    46  // back a pointer to its memory location in the heap
    47  func newInformer(myID string, getBlockedMembers func() set.Set[string], fanOut int, maxSend int, logger *slog.Logger) *Informer {
    48  	informer := Informer{
    49  		myID: myID,
    50  		state: &GossipState{
    51  			Members: map[string]*GossipState_GossipMemberState{},
    52  		},
    53  		committedOffsets:  map[string]int64{},
    54  		activeMemberIDs:   map[string]empty{},
    55  		otherMembers:      []*Member{},
    56  		consensusChecks:   NewConsensusChecks(),
    57  		getBlockedMembers: getBlockedMembers,
    58  		gossipFanOut:      fanOut,
    59  		gossipMaxSend:     maxSend,
    60  		logger:            logger,
    61  	}
    62  	informer.throttler = actor.NewThrottle(3, 60*time.Second, informer.throttledLog)
    63  	return &informer
    64  }
    65  
    66  // called when there is a cluster topology update
    67  func (inf *Informer) UpdateClusterTopology(topology *ClusterTopology) {
    68  	var others []*Member
    69  	for _, member := range topology.Members {
    70  		if member.Id != inf.myID {
    71  			others = append(others, member)
    72  		}
    73  	}
    74  	inf.otherMembers = others
    75  
    76  	active := make(map[string]empty)
    77  	for _, member := range topology.Members {
    78  		active[member.Id] = empty{}
    79  	}
    80  
    81  	inf.SetState(TopologyKey, topology)
    82  }
    83  
    84  // sets new update key state using the given proto message
    85  func (inf *Informer) SetState(key string, message proto.Message) {
    86  	inf.localSeqNumber = setKey(inf.state, key, message, inf.myID, inf.localSeqNumber)
    87  
    88  	//if inf.throttler() == actor.Open {
    89  	//	sequenceNumbers := map[string]uint64{}
    90  	//
    91  	//	for _, memberState := range inf.state.Members {
    92  	//		for key, value := range memberState.Values {
    93  	//			sequenceNumbers[key] = uint64(value.SequenceNumber)
    94  	//		}
    95  	//	}
    96  	//
    97  	//	// plog.Debug("Setting state", log.String("key", key), log.String("value", message.String()), log.Object("state", sequenceNumbers))
    98  	//}
    99  
   100  	if _, ok := inf.state.Members[inf.myID]; !ok {
   101  		inf.logger.Error("State corrupt")
   102  	}
   103  
   104  	inf.checkConsensusKey(key)
   105  }
   106  
   107  // sends this informer local state to remote informers chosen randomly
   108  // from the slice of other members known by this informer until gossipFanOut
   109  // number of sent has been reached
   110  func (inf *Informer) SendState(sendStateToMember LocalStateSender) {
   111  	// inf.purgeBannedMembers()  // TODO
   112  	for _, member := range inf.otherMembers {
   113  		ensureMemberStateExists(inf.state, member.Id)
   114  	}
   115  
   116  	// make a copy of the otherMembers so we can sort it randomly
   117  	otherMembers := make([]*Member, len(inf.otherMembers))
   118  	copy(otherMembers, inf.otherMembers)
   119  
   120  	// shuffles the order of the slice elements
   121  	rnd.Shuffle(len(otherMembers), func(i, j int) {
   122  		otherMembers[i], otherMembers[j] = otherMembers[j], otherMembers[i]
   123  	})
   124  
   125  	fanOutCount := 0
   126  	for _, member := range otherMembers {
   127  		memberState := inf.GetMemberStateDelta(member.Id)
   128  		if !memberState.HasState {
   129  			// nothing has change, skip it
   130  			continue
   131  		}
   132  
   133  		// fire and forget, we handle results in ReenterAfter
   134  		sendStateToMember(memberState, member)
   135  		fanOutCount++
   136  
   137  		// we reached our limit, break
   138  		if fanOutCount >= inf.gossipFanOut {
   139  			break
   140  		}
   141  	}
   142  }
   143  
   144  func (inf *Informer) GetMemberStateDelta(targetMemberID string) *MemberStateDelta {
   145  	var count int
   146  
   147  	// newState will old the final new state to be sent
   148  	newState := GossipState{Members: make(map[string]*GossipState_GossipMemberState)}
   149  
   150  	// hashmaps in Go are random by nature so no need to randomize state.Members
   151  	pendingOffsets := inf.committedOffsets
   152  
   153  	// create a new map with gossipMaxSend entries max
   154  	members := make(map[string]*GossipState_GossipMemberState)
   155  
   156  	// add ourselves to the gossip list if we are in the members state
   157  	if member, ok := inf.state.Members[inf.myID]; ok {
   158  		members[inf.myID] = member
   159  		count++
   160  	}
   161  
   162  	// Go hash maps are unordered by nature so we don't need to randomize them
   163  	// iterate over our state members skipping ourselves and add them to the
   164  	// local `newState` variable until gossipMaxSend is reached
   165  	for id, member := range inf.state.Members {
   166  		if id == inf.myID {
   167  			continue
   168  		}
   169  
   170  		count++
   171  		members[id] = member
   172  
   173  		if count > inf.gossipMaxSend {
   174  			break
   175  		}
   176  	}
   177  
   178  	// now we iterate over our subset of members and proceed to send them if applicable
   179  	for memberID, memberState := range members {
   180  
   181  		// create an empty state
   182  		newMemberState := GossipState_GossipMemberState{
   183  			Values: make(map[string]*GossipKeyValue),
   184  		}
   185  
   186  		watermarkKey := fmt.Sprintf("%s.%s", targetMemberID, memberID)
   187  
   188  		// get the water mark
   189  		watermark := inf.committedOffsets[watermarkKey]
   190  		newWatermark := watermark
   191  
   192  		// for each value in member state
   193  		for key, value := range memberState.Values {
   194  
   195  			if value.SequenceNumber <= watermark {
   196  				continue
   197  			}
   198  
   199  			if value.SequenceNumber > newWatermark {
   200  				newWatermark = value.SequenceNumber
   201  			}
   202  
   203  			newMemberState.Values[key] = value
   204  		}
   205  
   206  		// do not send memberStates that we have no new data for
   207  		if len(newMemberState.Values) > 0 {
   208  			newState.Members[memberID] = &newMemberState
   209  			pendingOffsets[watermarkKey] = newWatermark
   210  		}
   211  	}
   212  
   213  	hasState := reflect.DeepEqual(inf.committedOffsets, pendingOffsets)
   214  	memberState := &MemberStateDelta{
   215  		TargetMemberID: targetMemberID,
   216  		HasState:       hasState,
   217  		State:          &newState,
   218  		CommitOffsets: func() {
   219  			inf.commitPendingOffsets(pendingOffsets)
   220  		},
   221  	}
   222  
   223  	return memberState
   224  }
   225  
   226  // adds a new consensus checker to this informer
   227  func (inf *Informer) AddConsensusCheck(id string, check *ConsensusCheck) {
   228  	inf.consensusChecks.Add(id, check)
   229  
   230  	// check when adding, if we are already consistent
   231  	check.check(inf.state, inf.activeMemberIDs)
   232  }
   233  
   234  // removes a consensus checker from this informer
   235  func (inf *Informer) RemoveConsensusCheck(id string) {
   236  	inf.consensusChecks.Remove(id)
   237  }
   238  
   239  // retrieves this informer current state for the given key
   240  // returns map containing each known member id and their value
   241  func (inf *Informer) GetState(key string) map[string]*GossipKeyValue {
   242  	entries := make(map[string]*GossipKeyValue)
   243  
   244  	for memberID, memberState := range inf.state.Members {
   245  		if value, ok := memberState.Values[key]; ok {
   246  			entries[memberID] = value
   247  		}
   248  	}
   249  
   250  	return entries
   251  }
   252  
   253  // receives a remote informer state
   254  func (inf *Informer) ReceiveState(remoteState *GossipState) []*GossipUpdate {
   255  	updates, newState, updatedKeys := mergeState(inf.state, remoteState)
   256  	if len(updates) == 0 {
   257  		return nil
   258  	}
   259  
   260  	inf.state = newState
   261  	keys := make([]string, 0, len(updatedKeys))
   262  	for k := range updatedKeys {
   263  		keys = append(keys, k)
   264  	}
   265  
   266  	inf.CheckConsensus(keys...)
   267  	return updates
   268  }
   269  
   270  // check consensus for the given keys
   271  func (inf *Informer) CheckConsensus(updatedKeys ...string) {
   272  	for _, consensusCheck := range inf.consensusChecks.GetByUpdatedKeys(updatedKeys) {
   273  		consensusCheck.check(inf.state, inf.activeMemberIDs)
   274  	}
   275  }
   276  
   277  // runs checkers on key updates
   278  func (inf *Informer) checkConsensusKey(updatedKey string) {
   279  	for _, consensusCheck := range inf.consensusChecks.GetByUpdatedKey(updatedKey) {
   280  		consensusCheck.check(inf.state, inf.activeMemberIDs)
   281  	}
   282  }
   283  
   284  func (inf *Informer) commitPendingOffsets(offsets map[string]int64) {
   285  	for key, seqNumber := range offsets {
   286  		if offset, ok := inf.committedOffsets[key]; !ok || offset < seqNumber {
   287  			inf.committedOffsets[key] = seqNumber
   288  		}
   289  	}
   290  }
   291  
   292  func (inf *Informer) throttledLog(counter int32) {
   293  	inf.logger.Debug("[Gossip] Setting State", slog.Int("throttled", int(counter)))
   294  }