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  }