...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/evaluator_segment.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2

     1  package evaluation
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
     7  	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
     9  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
    10  )
    11  
    12  func makeBigSegmentRef(s *ldmodel.Segment) string {
    13  	// The format of big segment references is independent of what store implementation is being
    14  	// used; the store implementation receives only this string and does not know the details of
    15  	// the data model. The Relay Proxy will use the same format when writing to the store.
    16  	return fmt.Sprintf("%s.g%d", s.Key, s.Generation.IntValue())
    17  }
    18  
    19  func (es *evaluationScope) segmentContainsContext(s *ldmodel.Segment, stack evaluationStack) (bool, error) {
    20  	// Have we already visited this segment recursively?
    21  	for _, visitedKey := range stack.segmentChain {
    22  		if visitedKey == s.Key {
    23  			return false, circularSegmentReferenceError(s.Key)
    24  		}
    25  	}
    26  
    27  	// Add this segment key to the visited list. Since stack is passed by value, this change does not
    28  	// persist after we return from this method. See comments in evaluationScope.checkPrerequisites().
    29  	stack.segmentChain = append(stack.segmentChain, s.Key)
    30  
    31  	// Check if the user is specifically included in or excluded from the segment by key
    32  	if s.Unbounded {
    33  		if !s.Generation.IsDefined() {
    34  			// Big segment queries can only be done if the generation is known. If it's unset,
    35  			// that probably means the data store was populated by an older SDK that doesn't know
    36  			// about the Generation property and therefore dropped it from the JSON data. We'll treat
    37  			// that as a "not configured" condition.
    38  			es.bigSegmentsStatus = ldreason.BigSegmentsNotConfigured
    39  			return false, nil
    40  		}
    41  		// A big segment can only apply to one context kind, so if we don't have a key for that kind,
    42  		// we don't need to bother querying the data.
    43  		key, ok := getApplicableContextKeyByKind(&es.context, s.UnboundedContextKind)
    44  		if !ok {
    45  			return false, nil
    46  		}
    47  		// Even if multiple big segments are referenced within a single flag evaluation, we only need
    48  		// to do this query once per context key, since it returns *all* of the user's segment
    49  		// memberships.
    50  		membership, wasCached := es.bigSegmentsMemberships[key]
    51  		if !wasCached {
    52  			if es.owner.bigSegmentProvider == nil {
    53  				// If the provider is nil, that means the SDK hasn't been configured to be able to
    54  				// use big segments.
    55  				es.bigSegmentsStatus = ldreason.BigSegmentsNotConfigured
    56  			} else {
    57  				var thisQueryStatus ldreason.BigSegmentsStatus
    58  				membership, thisQueryStatus = es.owner.bigSegmentProvider.GetMembership(key)
    59  				// Note that this query is just by key; the context kind doesn't matter because any given
    60  				// Big Segment can only reference one context kind. So if segment A for the "user" kind
    61  				// includes a "user" context with key X, and segment B for the "org" kind includes an "org"
    62  				// context with the same key X, it is fine to say that the membership for key X is
    63  				// segment A and segment B-- there is no ambiguity.
    64  				if es.bigSegmentsMemberships == nil {
    65  					es.bigSegmentsMemberships = make(map[string]BigSegmentMembership)
    66  				}
    67  				es.bigSegmentsMemberships[key] = membership
    68  				es.bigSegmentsStatus = computeUpdatedBigSegmentsStatus(es.bigSegmentsStatus, thisQueryStatus)
    69  			}
    70  		}
    71  		if membership != nil {
    72  			included := membership.CheckMembership(makeBigSegmentRef(s))
    73  			if included.IsDefined() {
    74  				return included.BoolValue(), nil
    75  			}
    76  		}
    77  	} else {
    78  		// always check for included before excluded
    79  		defaultKindKey, hasDefaultKindKey := getApplicableContextKeyByKind(&es.context, ldcontext.DefaultKind)
    80  		isOnlyDefaultKind := es.context.Kind() == ldcontext.DefaultKind
    81  		if hasDefaultKindKey && ldmodel.EvaluatorAccessors.SegmentFindKeyInIncluded(s, defaultKindKey) {
    82  			return true, nil
    83  		}
    84  		if !isOnlyDefaultKind {
    85  			for i := range s.IncludedContexts {
    86  				if es.segmentTargetMatchesContext(&s.IncludedContexts[i]) {
    87  					return true, nil
    88  				}
    89  			}
    90  		}
    91  		if hasDefaultKindKey && ldmodel.EvaluatorAccessors.SegmentFindKeyInExcluded(s, defaultKindKey) {
    92  			return false, nil
    93  		}
    94  		if !isOnlyDefaultKind {
    95  			for i := range s.ExcludedContexts {
    96  				if es.segmentTargetMatchesContext(&s.ExcludedContexts[i]) {
    97  					return false, nil
    98  				}
    99  			}
   100  		}
   101  	}
   102  
   103  	// Check if any of the segment rules match
   104  	for _, rule := range s.Rules {
   105  		// Note, taking address of range variable here is OK because it's not used outside the loop
   106  		match, err := es.segmentRuleMatchesContext(&rule, stack, s.Key, s.Salt) //nolint:gosec // see comment above
   107  		if err != nil {
   108  			return false, malformedSegmentError{SegmentKey: s.Key, Err: err}
   109  		}
   110  		if match {
   111  			return true, nil
   112  		}
   113  	}
   114  
   115  	return false, nil
   116  }
   117  
   118  func (es *evaluationScope) segmentTargetMatchesContext(t *ldmodel.SegmentTarget) bool {
   119  	if key, ok := getApplicableContextKeyByKind(&es.context, t.ContextKind); ok {
   120  		return ldmodel.EvaluatorAccessors.SegmentTargetFindKey(t, key)
   121  	}
   122  	return false
   123  }
   124  
   125  func (es *evaluationScope) segmentRuleMatchesContext(
   126  	r *ldmodel.SegmentRule,
   127  	stack evaluationStack,
   128  	key, salt string,
   129  ) (bool, error) {
   130  	for i := range r.Clauses {
   131  		// Note that the clause is passed by address only for efficiency; we do not modify it
   132  		match, err := es.clauseMatchesContext(&r.Clauses[i], stack)
   133  		if !match || err != nil {
   134  			return false, err
   135  		}
   136  	}
   137  
   138  	// If the Weight is absent, this rule matches
   139  	if !r.Weight.IsDefined() {
   140  		return true, nil
   141  	}
   142  
   143  	// All of the clauses are met. Check to see if the user buckets in
   144  	// Note: passing r.RolloutContextKind to computeBucketValue here ensures that 1. we will get any necessary
   145  	// context attributes from the right context if the evaluation context is multi-kind, and 2. if the desired
   146  	// context kind is not available,
   147  	// TEMPORARY - instead of ldcontext.DefaultKind here, we will eventually have a Kind field in the segment
   148  	bucket, failReason, err := es.computeBucketValue(
   149  		false,                 // this is not an experiment
   150  		ldvalue.OptionalInt{}, // seed parameter is only used in experiments, never in segment rollouts
   151  		r.RolloutContextKind,
   152  		key,
   153  		r.BucketBy,
   154  		salt,
   155  	)
   156  	if err != nil {
   157  		// err is only non-nil for problems serious enough to indicate a malformed segment configuration
   158  		return false, err
   159  	}
   160  	if failReason == bucketingFailureContextLacksDesiredKind {
   161  		// This particular bucketing failure condition is specified to cause an automatic non-match for the rule.
   162  		// Other kinds of bucketing failures (such as an unknown bucketBy attribute) do not cause a non-match;
   163  		// they just cause the bucket value to be zero, which in this code path will result in a match. The latter
   164  		// behavior isn't logically consistent, but is preserved for historical reasons since changing it would
   165  		// change existing evaluation results.
   166  		return false, nil
   167  	}
   168  	weight := float32(r.Weight.IntValue()) / 100000.0
   169  	return bucket < weight, nil
   170  }
   171  
   172  func computeUpdatedBigSegmentsStatus(old, new ldreason.BigSegmentsStatus) ldreason.BigSegmentsStatus {
   173  	// A single evaluation could end up doing more than one big segments query if there are two different
   174  	// context keys involved. If those queries don't return the same status, we want to make sure we
   175  	// report whichever status is most problematic.
   176  	if old != "" && getBigSegmentsStatusPriority(old) > getBigSegmentsStatusPriority(new) {
   177  		return old
   178  	}
   179  	return new
   180  }
   181  
   182  func getBigSegmentsStatusPriority(status ldreason.BigSegmentsStatus) int {
   183  	switch status {
   184  	case ldreason.BigSegmentsStale:
   185  		return 1
   186  	case ldreason.BigSegmentsStoreError:
   187  		return 2
   188  	case ldreason.BigSegmentsNotConfigured:
   189  		// NotConfigured is considered a higher-priority problem than StoreError because it implies that the
   190  		// application can't possibly be working right, whereas StoreError could be a transient database problem.
   191  		return 3
   192  	default:
   193  		return 0
   194  	}
   195  }
   196  

View as plain text