package evaluation import ( "fmt" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldreason" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" ) func makeBigSegmentRef(s *ldmodel.Segment) string { // The format of big segment references is independent of what store implementation is being // used; the store implementation receives only this string and does not know the details of // the data model. The Relay Proxy will use the same format when writing to the store. return fmt.Sprintf("%s.g%d", s.Key, s.Generation.IntValue()) } func (es *evaluationScope) segmentContainsContext(s *ldmodel.Segment, stack evaluationStack) (bool, error) { // Have we already visited this segment recursively? for _, visitedKey := range stack.segmentChain { if visitedKey == s.Key { return false, circularSegmentReferenceError(s.Key) } } // Add this segment key to the visited list. Since stack is passed by value, this change does not // persist after we return from this method. See comments in evaluationScope.checkPrerequisites(). stack.segmentChain = append(stack.segmentChain, s.Key) // Check if the user is specifically included in or excluded from the segment by key if s.Unbounded { if !s.Generation.IsDefined() { // Big segment queries can only be done if the generation is known. If it's unset, // that probably means the data store was populated by an older SDK that doesn't know // about the Generation property and therefore dropped it from the JSON data. We'll treat // that as a "not configured" condition. es.bigSegmentsStatus = ldreason.BigSegmentsNotConfigured return false, nil } // A big segment can only apply to one context kind, so if we don't have a key for that kind, // we don't need to bother querying the data. key, ok := getApplicableContextKeyByKind(&es.context, s.UnboundedContextKind) if !ok { return false, nil } // Even if multiple big segments are referenced within a single flag evaluation, we only need // to do this query once per context key, since it returns *all* of the user's segment // memberships. membership, wasCached := es.bigSegmentsMemberships[key] if !wasCached { if es.owner.bigSegmentProvider == nil { // If the provider is nil, that means the SDK hasn't been configured to be able to // use big segments. es.bigSegmentsStatus = ldreason.BigSegmentsNotConfigured } else { var thisQueryStatus ldreason.BigSegmentsStatus membership, thisQueryStatus = es.owner.bigSegmentProvider.GetMembership(key) // Note that this query is just by key; the context kind doesn't matter because any given // Big Segment can only reference one context kind. So if segment A for the "user" kind // includes a "user" context with key X, and segment B for the "org" kind includes an "org" // context with the same key X, it is fine to say that the membership for key X is // segment A and segment B-- there is no ambiguity. if es.bigSegmentsMemberships == nil { es.bigSegmentsMemberships = make(map[string]BigSegmentMembership) } es.bigSegmentsMemberships[key] = membership es.bigSegmentsStatus = computeUpdatedBigSegmentsStatus(es.bigSegmentsStatus, thisQueryStatus) } } if membership != nil { included := membership.CheckMembership(makeBigSegmentRef(s)) if included.IsDefined() { return included.BoolValue(), nil } } } else { // always check for included before excluded defaultKindKey, hasDefaultKindKey := getApplicableContextKeyByKind(&es.context, ldcontext.DefaultKind) isOnlyDefaultKind := es.context.Kind() == ldcontext.DefaultKind if hasDefaultKindKey && ldmodel.EvaluatorAccessors.SegmentFindKeyInIncluded(s, defaultKindKey) { return true, nil } if !isOnlyDefaultKind { for i := range s.IncludedContexts { if es.segmentTargetMatchesContext(&s.IncludedContexts[i]) { return true, nil } } } if hasDefaultKindKey && ldmodel.EvaluatorAccessors.SegmentFindKeyInExcluded(s, defaultKindKey) { return false, nil } if !isOnlyDefaultKind { for i := range s.ExcludedContexts { if es.segmentTargetMatchesContext(&s.ExcludedContexts[i]) { return false, nil } } } } // Check if any of the segment rules match for _, rule := range s.Rules { // Note, taking address of range variable here is OK because it's not used outside the loop match, err := es.segmentRuleMatchesContext(&rule, stack, s.Key, s.Salt) //nolint:gosec // see comment above if err != nil { return false, malformedSegmentError{SegmentKey: s.Key, Err: err} } if match { return true, nil } } return false, nil } func (es *evaluationScope) segmentTargetMatchesContext(t *ldmodel.SegmentTarget) bool { if key, ok := getApplicableContextKeyByKind(&es.context, t.ContextKind); ok { return ldmodel.EvaluatorAccessors.SegmentTargetFindKey(t, key) } return false } func (es *evaluationScope) segmentRuleMatchesContext( r *ldmodel.SegmentRule, stack evaluationStack, key, salt string, ) (bool, error) { for i := range r.Clauses { // Note that the clause is passed by address only for efficiency; we do not modify it match, err := es.clauseMatchesContext(&r.Clauses[i], stack) if !match || err != nil { return false, err } } // If the Weight is absent, this rule matches if !r.Weight.IsDefined() { return true, nil } // All of the clauses are met. Check to see if the user buckets in // Note: passing r.RolloutContextKind to computeBucketValue here ensures that 1. we will get any necessary // context attributes from the right context if the evaluation context is multi-kind, and 2. if the desired // context kind is not available, // TEMPORARY - instead of ldcontext.DefaultKind here, we will eventually have a Kind field in the segment bucket, failReason, err := es.computeBucketValue( false, // this is not an experiment ldvalue.OptionalInt{}, // seed parameter is only used in experiments, never in segment rollouts r.RolloutContextKind, key, r.BucketBy, salt, ) if err != nil { // err is only non-nil for problems serious enough to indicate a malformed segment configuration return false, err } if failReason == bucketingFailureContextLacksDesiredKind { // This particular bucketing failure condition is specified to cause an automatic non-match for the rule. // Other kinds of bucketing failures (such as an unknown bucketBy attribute) do not cause a non-match; // they just cause the bucket value to be zero, which in this code path will result in a match. The latter // behavior isn't logically consistent, but is preserved for historical reasons since changing it would // change existing evaluation results. return false, nil } weight := float32(r.Weight.IntValue()) / 100000.0 return bucket < weight, nil } func computeUpdatedBigSegmentsStatus(old, new ldreason.BigSegmentsStatus) ldreason.BigSegmentsStatus { // A single evaluation could end up doing more than one big segments query if there are two different // context keys involved. If those queries don't return the same status, we want to make sure we // report whichever status is most problematic. if old != "" && getBigSegmentsStatusPriority(old) > getBigSegmentsStatusPriority(new) { return old } return new } func getBigSegmentsStatusPriority(status ldreason.BigSegmentsStatus) int { switch status { case ldreason.BigSegmentsStale: return 1 case ldreason.BigSegmentsStoreError: return 2 case ldreason.BigSegmentsNotConfigured: // NotConfigured is considered a higher-priority problem than StoreError because it implies that the // application can't possibly be working right, whereas StoreError could be a transient database problem. return 3 default: return 0 } }