launchdarkly-server-sdk 6.3.4 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +121 -55
  3. data/lib/ldclient-rb/context.rb +487 -0
  4. data/lib/ldclient-rb/evaluation_detail.rb +20 -20
  5. data/lib/ldclient-rb/events.rb +77 -132
  6. data/lib/ldclient-rb/flags_state.rb +4 -4
  7. data/lib/ldclient-rb/impl/big_segments.rb +17 -17
  8. data/lib/ldclient-rb/impl/context.rb +96 -0
  9. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  10. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  11. data/lib/ldclient-rb/impl/evaluator.rb +378 -139
  12. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  13. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  14. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  15. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  16. data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
  17. data/lib/ldclient-rb/impl/event_types.rb +18 -30
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
  22. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  23. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  24. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  25. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  26. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  27. data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
  28. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  29. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  30. data/lib/ldclient-rb/impl/util.rb +59 -1
  31. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  32. data/lib/ldclient-rb/integrations/consul.rb +1 -1
  33. data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
  34. data/lib/ldclient-rb/integrations/file_data.rb +3 -3
  35. data/lib/ldclient-rb/integrations/redis.rb +4 -4
  36. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
  37. data/lib/ldclient-rb/integrations/test_data.rb +16 -12
  38. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
  39. data/lib/ldclient-rb/interfaces.rb +14 -14
  40. data/lib/ldclient-rb/ldclient.rb +94 -144
  41. data/lib/ldclient-rb/memoized_value.rb +1 -1
  42. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  43. data/lib/ldclient-rb/polling.rb +2 -2
  44. data/lib/ldclient-rb/reference.rb +274 -0
  45. data/lib/ldclient-rb/requestor.rb +7 -7
  46. data/lib/ldclient-rb/stream.rb +8 -9
  47. data/lib/ldclient-rb/util.rb +4 -19
  48. data/lib/ldclient-rb/version.rb +1 -1
  49. data/lib/ldclient-rb.rb +2 -3
  50. metadata +36 -17
  51. data/lib/ldclient-rb/file_data_source.rb +0 -23
  52. data/lib/ldclient-rb/newrelic.rb +0 -17
  53. data/lib/ldclient-rb/redis_store.rb +0 -88
  54. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,6 +1,9 @@
1
1
  require "ldclient-rb/evaluation_detail"
2
2
  require "ldclient-rb/impl/evaluator_bucketing"
3
+ require "ldclient-rb/impl/evaluator_helpers"
3
4
  require "ldclient-rb/impl/evaluator_operators"
5
+ require "ldclient-rb/impl/model/feature_flag"
6
+ require "ldclient-rb/impl/model/segment"
4
7
 
5
8
  module LaunchDarkly
6
9
  module Impl
@@ -11,6 +14,81 @@ module LaunchDarkly
11
14
  :detail # the EvaluationDetail representing the evaluation result
12
15
  )
13
16
 
17
+ class EvaluationException < StandardError
18
+ def initialize(msg, error_kind = EvaluationReason::ERROR_MALFORMED_FLAG)
19
+ super(msg)
20
+ @error_kind = error_kind
21
+ end
22
+
23
+ # @return [Symbol]
24
+ attr_reader :error_kind
25
+ end
26
+
27
+ class InvalidReferenceException < EvaluationException
28
+ end
29
+
30
+ class EvaluatorState
31
+ # @param original_flag [LaunchDarkly::Impl::Model::FeatureFlag]
32
+ def initialize(original_flag)
33
+ @prereq_stack = EvaluatorStack.new(original_flag.key)
34
+ @segment_stack = EvaluatorStack.new(nil)
35
+ end
36
+
37
+ attr_reader :prereq_stack
38
+ attr_reader :segment_stack
39
+ end
40
+
41
+ #
42
+ # A helper class for managing cycle detection.
43
+ #
44
+ # Each time a method sees a new flag or segment, they can push that
45
+ # object's key onto the stack. Once processing for that object has
46
+ # finished, you can call pop to remove it.
47
+ #
48
+ # Because the most common use case would be a flag or segment without ANY
49
+ # prerequisites, this stack has a small optimization in place-- the stack
50
+ # is not created until absolutely necessary.
51
+ #
52
+ class EvaluatorStack
53
+ # @param original [String, nil]
54
+ def initialize(original)
55
+ @original = original
56
+ # @type [Array<String>, nil]
57
+ @stack = nil
58
+ end
59
+
60
+ # @param key [String]
61
+ def push(key)
62
+ # No need to store the key if we already have a record in our instance
63
+ # variable.
64
+ return if @original == key
65
+
66
+ # The common use case is that flags/segments won't have prereqs, so we
67
+ # don't allocate the stack memory until we absolutely must.
68
+ if @stack.nil?
69
+ @stack = []
70
+ end
71
+
72
+ @stack.push(key)
73
+ end
74
+
75
+ def pop
76
+ return if @stack.nil? || @stack.empty?
77
+ @stack.pop
78
+ end
79
+
80
+ #
81
+ # @param key [String]
82
+ # @return [Boolean]
83
+ #
84
+ def include?(key)
85
+ return true if key == @original
86
+ return false if @stack.nil?
87
+
88
+ @stack.include? key
89
+ end
90
+ end
91
+
14
92
  # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
15
93
  # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
16
94
  # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
@@ -21,7 +99,7 @@ module LaunchDarkly
21
99
  # @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
22
100
  # currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
23
101
  # flag data - or nil if the flag is unknown or deleted
24
- # @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
102
+ # @param get_segment [Function] similar to `get_flag`, but is used to query a context segment.
25
103
  # @param logger [Logger] the client's logger
26
104
  def initialize(get_flag, get_segment, get_big_segments_membership, logger)
27
105
  @get_flag = get_flag
@@ -29,15 +107,15 @@ module LaunchDarkly
29
107
  @get_big_segments_membership = get_big_segments_membership
30
108
  @logger = logger
31
109
  end
32
-
110
+
33
111
  # Used internally to hold an evaluation result and additional state that may be accumulated during an
34
112
  # evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
35
113
  # trying to use a pure functional approach, and since we're not exposing this object to any application code
36
114
  # or retaining it anywhere, we don't have to be quite as strict about immutability.
37
115
  #
38
116
  # The big_segments_status and big_segments_membership properties are not used by the caller; they are used
39
- # during an evaluation to cache the result of any Big Segments query that we've done for this user, because
40
- # we don't want to do multiple queries for the same user if multiple Big Segments are referenced in the same
117
+ # during an evaluation to cache the result of any Big Segments query that we've done for this context, because
118
+ # we don't want to do multiple queries for the same context if multiple Big Segments are referenced in the same
41
119
  # evaluation.
42
120
  EvalResult = Struct.new(
43
121
  :detail, # the EvaluationDetail representing the evaluation result
@@ -55,233 +133,394 @@ module LaunchDarkly
55
133
  # any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
56
134
  # default value. Error conditions produce a result with a nil value and an error reason, not an exception.
57
135
  #
58
- # @param flag [Object] the flag
59
- # @param user [Object] the user properties
60
- # @return [EvalResult] the evaluation result
61
- def evaluate(flag, user)
136
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
137
+ # @param context [LaunchDarkly::LDContext] the evaluation context
138
+ # @return [EvalResult] the evaluation result
139
+ def evaluate(flag, context)
140
+ state = EvaluatorState.new(flag)
141
+
62
142
  result = EvalResult.new
63
- if user.nil? || user[:key].nil?
64
- result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
143
+ begin
144
+ detail = eval_internal(flag, context, result, state)
145
+ rescue EvaluationException => exn
146
+ LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
147
+ result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(exn.error_kind))
148
+ return result
149
+ rescue => exn
150
+ LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
151
+ result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
65
152
  return result
66
153
  end
67
-
68
- detail = eval_internal(flag, user, result)
69
- if !result.big_segments_status.nil?
154
+
155
+ unless result.big_segments_status.nil?
70
156
  # If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
71
157
  # some point and we will want to include the status in the evaluation reason.
72
158
  detail = EvaluationDetail.new(detail.value, detail.variation_index,
73
159
  detail.reason.with_big_segments_status(result.big_segments_status))
74
160
  end
75
161
  result.detail = detail
76
- return result
162
+ result
77
163
  end
78
164
 
165
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
79
166
  def self.make_big_segment_ref(segment) # method is visible for testing
80
167
  # The format of Big Segment references is independent of what store implementation is being
81
168
  # used; the store implementation receives only this string and does not know the details of
82
169
  # the data model. The Relay Proxy will use the same format when writing to the store.
83
- "#{segment[:key]}.g#{segment[:generation]}"
170
+ "#{segment.key}.g#{segment.generation}"
84
171
  end
85
172
 
86
- private
87
-
88
- def eval_internal(flag, user, state)
89
- if !flag[:on]
90
- return get_off_value(flag, EvaluationReason::off)
173
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
174
+ # @param context [LaunchDarkly::LDContext] the evaluation context
175
+ # @param eval_result [EvalResult]
176
+ # @param state [EvaluatorState]
177
+ # @raise [EvaluationException]
178
+ private def eval_internal(flag, context, eval_result, state)
179
+ unless flag.on
180
+ return flag.off_result
91
181
  end
92
182
 
93
- prereq_failure_reason = check_prerequisites(flag, user, state)
94
- if !prereq_failure_reason.nil?
95
- return get_off_value(flag, prereq_failure_reason)
96
- end
183
+ prereq_failure_result = check_prerequisites(flag, context, eval_result, state)
184
+ return prereq_failure_result unless prereq_failure_result.nil?
185
+
186
+ # Check context target matches
187
+ target_result = check_targets(context, flag)
188
+ return target_result unless target_result.nil?
97
189
 
98
- # Check user target matches
99
- (flag[:targets] || []).each do |target|
100
- (target[:values] || []).each do |value|
101
- if value == user[:key]
102
- return get_variation(flag, target[:variation], EvaluationReason::target_match)
103
- end
104
- end
105
- end
106
-
107
190
  # Check custom rules
108
- rules = flag[:rules] || []
109
- rules.each_index do |i|
110
- rule = rules[i]
111
- if rule_match_user(rule, user, state)
112
- reason = rule[:_reason] # try to use cached reason for this rule
113
- reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
114
- return get_value_for_variation_or_rollout(flag, rule, user, reason)
191
+ flag.rules.each do |rule|
192
+ if rule_match_context(rule, context, eval_result, state)
193
+ return get_value_for_variation_or_rollout(flag, rule.variation_or_rollout, context, rule.match_results)
115
194
  end
116
195
  end
117
196
 
118
197
  # Check the fallthrough rule
119
- if !flag[:fallthrough].nil?
120
- return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough)
198
+ unless flag.fallthrough.nil?
199
+ return get_value_for_variation_or_rollout(flag, flag.fallthrough, context, flag.fallthrough_results)
121
200
  end
122
201
 
123
- return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
202
+ EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
124
203
  end
125
204
 
126
- def check_prerequisites(flag, user, state)
127
- (flag[:prerequisites] || []).each do |prerequisite|
128
- prereq_ok = true
129
- prereq_key = prerequisite[:key]
130
- prereq_flag = @get_flag.call(prereq_key)
205
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] the flag
206
+ # @param context [LaunchDarkly::LDContext] the evaluation context
207
+ # @param eval_result [EvalResult]
208
+ # @param state [EvaluatorState]
209
+ # @raise [EvaluationException] if a flag prereq cycle is detected
210
+ private def check_prerequisites(flag, context, eval_result, state)
211
+ return if flag.prerequisites.empty?
131
212
 
132
- if prereq_flag.nil?
133
- @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
134
- prereq_ok = false
135
- else
136
- begin
137
- prereq_res = eval_internal(prereq_flag, user, state)
213
+ state.prereq_stack.push(flag.key)
214
+
215
+ begin
216
+ flag.prerequisites.each do |prerequisite|
217
+ prereq_ok = true
218
+ prereq_key = prerequisite.key
219
+
220
+ if state.prereq_stack.include?(prereq_key)
221
+ raise LaunchDarkly::Impl::EvaluationException.new(
222
+ "prerequisite relationship to \"#{prereq_key}\" caused a circular reference; this is probably a temporary condition due to an incomplete update"
223
+ )
224
+ end
225
+
226
+ prereq_flag = @get_flag.call(prereq_key)
227
+
228
+ if prereq_flag.nil?
229
+ @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag.key}\"" }
230
+ prereq_ok = false
231
+ else
232
+ prereq_res = eval_internal(prereq_flag, context, eval_result, state)
138
233
  # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
139
234
  # off variation was. But we still need to evaluate it in order to generate an event.
140
- if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
235
+ if !prereq_flag.on || prereq_res.variation_index != prerequisite.variation
141
236
  prereq_ok = false
142
237
  end
143
238
  prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
144
- state.prereq_evals = [] if state.prereq_evals.nil?
145
- state.prereq_evals.push(prereq_eval)
146
- rescue => exn
147
- Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
148
- prereq_ok = false
239
+ eval_result.prereq_evals = [] if eval_result.prereq_evals.nil?
240
+ eval_result.prereq_evals.push(prereq_eval)
241
+ end
242
+
243
+ unless prereq_ok
244
+ return prerequisite.failure_result
149
245
  end
150
246
  end
151
- if !prereq_ok
152
- reason = prerequisite[:_reason] # try to use cached reason
153
- return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
154
- end
247
+ ensure
248
+ state.prereq_stack.pop
155
249
  end
250
+
156
251
  nil
157
252
  end
158
253
 
159
- def rule_match_user(rule, user, state)
160
- return false if !rule[:clauses]
161
-
162
- (rule[:clauses] || []).each do |clause|
163
- return false if !clause_match_user(clause, user, state)
254
+ # @param rule [LaunchDarkly::Impl::Model::FlagRule]
255
+ # @param context [LaunchDarkly::LDContext]
256
+ # @param eval_result [EvalResult]
257
+ # @param state [EvaluatorState]
258
+ # @raise [InvalidReferenceException]
259
+ private def rule_match_context(rule, context, eval_result, state)
260
+ rule.clauses.each do |clause|
261
+ return false unless clause_match_context(clause, context, eval_result, state)
164
262
  end
165
263
 
166
- return true
264
+ true
167
265
  end
168
266
 
169
- def clause_match_user(clause, user, state)
170
- # In the case of a segment match operator, we check if the user is in any of the segments,
267
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
268
+ # @param context [LaunchDarkly::LDContext]
269
+ # @param eval_result [EvalResult]
270
+ # @param state [EvaluatorState]
271
+ # @raise [InvalidReferenceException]
272
+ private def clause_match_context(clause, context, eval_result, state)
273
+ # In the case of a segment match operator, we check if the context is in any of the segments,
171
274
  # and possibly negate
172
- if clause[:op].to_sym == :segmentMatch
173
- result = (clause[:values] || []).any? { |v|
275
+ if clause.op == :segmentMatch
276
+ result = clause.values.any? { |v|
277
+ if state.segment_stack.include?(v)
278
+ raise LaunchDarkly::Impl::EvaluationException.new(
279
+ "segment rule referencing segment \"#{v}\" caused a circular reference; this is probably a temporary condition due to an incomplete update"
280
+ )
281
+ end
282
+
174
283
  segment = @get_segment.call(v)
175
- !segment.nil? && segment_match_user(segment, user, state)
284
+ !segment.nil? && segment_match_context(segment, context, eval_result, state)
176
285
  }
177
- clause[:negate] ? !result : result
286
+ clause.negate ? !result : result
178
287
  else
179
- clause_match_user_no_segments(clause, user)
288
+ clause_match_context_no_segments(clause, context)
180
289
  end
181
290
  end
182
291
 
183
- def clause_match_user_no_segments(clause, user)
184
- user_val = EvaluatorOperators.user_value(user, clause[:attribute])
185
- return false if user_val.nil?
292
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
293
+ # @param context_value [any]
294
+ # @return [Boolean]
295
+ private def match_any_clause_value(clause, context_value)
296
+ op = clause.op
297
+ clause.values.any? { |cv| EvaluatorOperators.apply(op, context_value, cv) }
298
+ end
299
+
300
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
301
+ # @param context [LaunchDarkly::LDContext]
302
+ # @return [Boolean]
303
+ private def clause_match_by_kind(clause, context)
304
+ # If attribute is "kind", then we treat operator and values as a match
305
+ # expression against a list of all individual kinds in the context.
306
+ # That is, for a multi-kind context with kinds of "org" and "user", it
307
+ # is a match if either of those strings is a match with Operator and
308
+ # Values.
186
309
 
187
- op = clause[:op].to_sym
188
- clause_vals = clause[:values]
189
- result = if user_val.is_a? Enumerable
190
- user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
310
+ (0...context.individual_context_count).each do |i|
311
+ c = context.individual_context(i)
312
+ if !c.nil? && match_any_clause_value(clause, c.kind)
313
+ return true
314
+ end
315
+ end
316
+
317
+ false
318
+ end
319
+
320
+ # @param clause [LaunchDarkly::Impl::Model::Clause]
321
+ # @param context [LaunchDarkly::LDContext]
322
+ # @return [Boolean]
323
+ # @raise [InvalidReferenceException] Raised if the clause.attribute is an invalid reference
324
+ private def clause_match_context_no_segments(clause, context)
325
+ raise InvalidReferenceException.new(clause.attribute.error) unless clause.attribute.error.nil?
326
+
327
+ if clause.attribute.depth == 1 && clause.attribute.component(0) == :kind
328
+ result = clause_match_by_kind(clause, context)
329
+ return clause.negate ? !result : result
330
+ end
331
+
332
+ matched_context = context.individual_context(clause.context_kind || LaunchDarkly::LDContext::KIND_DEFAULT)
333
+ return false if matched_context.nil?
334
+
335
+ context_val = matched_context.get_value_for_reference(clause.attribute)
336
+ return false if context_val.nil?
337
+
338
+ result = if context_val.is_a? Enumerable
339
+ context_val.any? { |uv| match_any_clause_value(clause, uv) }
191
340
  else
192
- clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
341
+ match_any_clause_value(clause, context_val)
193
342
  end
194
- clause[:negate] ? !result : result
343
+ clause.negate ? !result : result
195
344
  end
196
345
 
197
- def segment_match_user(segment, user, state)
198
- return false unless user[:key]
199
- segment[:unbounded] ? big_segment_match_user(segment, user, state) : simple_segment_match_user(segment, user, true)
346
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
347
+ # @param context [LaunchDarkly::LDContext]
348
+ # @param eval_result [EvalResult]
349
+ # @param state [EvaluatorState]
350
+ # @return [Boolean]
351
+ private def segment_match_context(segment, context, eval_result, state)
352
+ return big_segment_match_context(segment, context, eval_result, state) if segment.unbounded
353
+
354
+ simple_segment_match_context(segment, context, true, eval_result, state)
200
355
  end
201
356
 
202
- def big_segment_match_user(segment, user, state)
203
- if !segment[:generation]
357
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
358
+ # @param context [LaunchDarkly::LDContext]
359
+ # @param eval_result [EvalResult]
360
+ # @param state [EvaluatorState]
361
+ # @return [Boolean]
362
+ private def big_segment_match_context(segment, context, eval_result, state)
363
+ unless segment.generation
204
364
  # Big segment queries can only be done if the generation is known. If it's unset,
205
365
  # that probably means the data store was populated by an older SDK that doesn't know
206
366
  # about the generation property and therefore dropped it from the JSON data. We'll treat
207
367
  # that as a "not configured" condition.
208
- state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
368
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
209
369
  return false
210
370
  end
211
- if !state.big_segments_status
212
- result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(user[:key])
371
+
372
+ matched_context = context.individual_context(segment.unbounded_context_kind)
373
+ return false if matched_context.nil?
374
+
375
+ membership = eval_result.big_segments_membership.nil? ? nil : eval_result.big_segments_membership[matched_context.key]
376
+
377
+ if membership.nil?
378
+ # Note that this query is just by key; the context kind doesn't matter because any given
379
+ # Big Segment can only reference one context kind. So if segment A for the "user" kind
380
+ # includes a "user" context with key X, and segment B for the "org" kind includes an "org"
381
+ # context with the same key X, it is fine to say that the membership for key X is
382
+ # segment A and segment B-- there is no ambiguity.
383
+ result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(matched_context.key)
213
384
  if result
214
- state.big_segments_membership = result.membership
215
- state.big_segments_status = result.status
385
+ eval_result.big_segments_status = result.status
386
+
387
+ membership = result.membership
388
+ eval_result.big_segments_membership = {} if eval_result.big_segments_membership.nil?
389
+ eval_result.big_segments_membership[matched_context.key] = membership
216
390
  else
217
- state.big_segments_membership = nil
218
- state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
391
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
219
392
  end
220
393
  end
221
- segment_ref = Evaluator.make_big_segment_ref(segment)
222
- membership = state.big_segments_membership
223
- included = membership.nil? ? nil : membership[segment_ref]
224
- return included if !included.nil?
225
- simple_segment_match_user(segment, user, false)
394
+
395
+ membership_result = nil
396
+ unless membership.nil?
397
+ segment_ref = Evaluator.make_big_segment_ref(segment)
398
+ membership_result = membership.nil? ? nil : membership[segment_ref]
399
+ end
400
+
401
+ return membership_result unless membership_result.nil?
402
+ simple_segment_match_context(segment, context, false, eval_result, state)
226
403
  end
227
404
 
228
- def simple_segment_match_user(segment, user, use_includes_and_excludes)
405
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
406
+ # @param context [LaunchDarkly::LDContext]
407
+ # @param use_includes_and_excludes [Boolean]
408
+ # @param state [EvaluatorState]
409
+ # @return [Boolean]
410
+ private def simple_segment_match_context(segment, context, use_includes_and_excludes, eval_result, state)
229
411
  if use_includes_and_excludes
230
- return true if segment[:included].include?(user[:key])
231
- return false if segment[:excluded].include?(user[:key])
412
+ if EvaluatorHelpers.context_key_in_target_list(context, nil, segment.included)
413
+ return true
414
+ end
415
+
416
+ segment.included_contexts.each do |target|
417
+ if EvaluatorHelpers.context_key_in_target_list(context, target.context_kind, target.values)
418
+ return true
419
+ end
420
+ end
421
+
422
+ if EvaluatorHelpers.context_key_in_target_list(context, nil, segment.excluded)
423
+ return false
424
+ end
425
+
426
+ segment.excluded_contexts.each do |target|
427
+ if EvaluatorHelpers.context_key_in_target_list(context, target.context_kind, target.values)
428
+ return false
429
+ end
430
+ end
232
431
  end
233
432
 
234
- (segment[:rules] || []).each do |r|
235
- return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
433
+ rules = segment.rules
434
+ state.segment_stack.push(segment.key) unless rules.empty?
435
+
436
+ begin
437
+ rules.each do |r|
438
+ return true if segment_rule_match_context(r, context, segment.key, segment.salt, eval_result, state)
439
+ end
440
+ ensure
441
+ state.segment_stack.pop
236
442
  end
237
443
 
238
- return false
444
+ false
239
445
  end
240
446
 
241
- def segment_rule_match_user(rule, user, segment_key, salt)
242
- (rule[:clauses] || []).each do |c|
243
- return false unless clause_match_user_no_segments(c, user)
447
+ # @param rule [LaunchDarkly::Impl::Model::SegmentRule]
448
+ # @param context [LaunchDarkly::LDContext]
449
+ # @param segment_key [String]
450
+ # @param salt [String]
451
+ # @return [Boolean]
452
+ # @raise [InvalidReferenceException]
453
+ private def segment_rule_match_context(rule, context, segment_key, salt, eval_result, state)
454
+ rule.clauses.each do |c|
455
+ return false unless clause_match_context(c, context, eval_result, state)
244
456
  end
245
457
 
246
458
  # If the weight is absent, this rule matches
247
- return true if !rule[:weight]
248
-
459
+ return true unless rule.weight
460
+
249
461
  # All of the clauses are met. See if the user buckets in
250
- bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
251
- weight = rule[:weight].to_f / 100000.0
252
- return bucket < weight
462
+ begin
463
+ bucket = EvaluatorBucketing.bucket_context(context, rule.rollout_context_kind, segment_key, rule.bucket_by || "key", salt, nil)
464
+ rescue InvalidReferenceException
465
+ return false
466
+ end
467
+
468
+ weight = rule.weight.to_f / 100000.0
469
+ bucket.nil? || bucket < weight
253
470
  end
254
471
 
255
- private
472
+ private def get_value_for_variation_or_rollout(flag, vr, context, precomputed_results)
473
+ index, in_experiment = EvaluatorBucketing.variation_index_for_context(flag, vr, context)
256
474
 
257
- def get_variation(flag, index, reason)
258
- if index < 0 || index >= flag[:variations].length
259
- @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
475
+ if index.nil?
476
+ @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag.key}\": variation/rollout object with no variation or rollout")
260
477
  return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
261
478
  end
262
- EvaluationDetail.new(flag[:variations][index], index, reason)
479
+ precomputed_results.for_variation(index, in_experiment)
263
480
  end
264
481
 
265
- def get_off_value(flag, reason)
266
- if flag[:offVariation].nil? # off variation unspecified - return default value
267
- return EvaluationDetail.new(nil, nil, reason)
268
- end
269
- get_variation(flag, flag[:offVariation], reason)
270
- end
482
+ # @param [LaunchDarkly::LDContext] context
483
+ # @param [LaunchDarkly::Impl::Model::FeatureFlag] flag
484
+ # @return [LaunchDarkly::EvaluationDetail, nil]
485
+ private def check_targets(context, flag)
486
+ targets = flag.targets
487
+ context_targets = flag.context_targets
488
+
489
+ if context_targets.empty?
490
+ unless targets.empty?
491
+ user_context = context.individual_context(LDContext::KIND_DEFAULT)
492
+ return nil if user_context.nil?
271
493
 
272
- def get_value_for_variation_or_rollout(flag, vr, user, reason)
273
- index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
274
- #if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
275
- if in_experiment && reason.kind == :FALLTHROUGH
276
- reason = EvaluationReason::fallthrough(in_experiment)
277
- elsif in_experiment && reason.kind == :RULE_MATCH
278
- reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
494
+ targets.each do |target|
495
+ if target.values.include?(user_context.key) # rubocop:disable Performance/InefficientHashSearch
496
+ return target.match_result
497
+ end
498
+ end
499
+ end
500
+
501
+ return nil
279
502
  end
280
- if index.nil?
281
- @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
282
- return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
503
+
504
+ context_targets.each do |target|
505
+ if target.kind == LDContext::KIND_DEFAULT
506
+ user_context = context.individual_context(LDContext::KIND_DEFAULT)
507
+ next if user_context.nil?
508
+
509
+ user_key = user_context.key
510
+ targets.each do |user_target|
511
+ if user_target.variation == target.variation
512
+ if user_target.values.include?(user_key) # rubocop:disable Performance/InefficientHashSearch
513
+ return target.match_result
514
+ end
515
+ break
516
+ end
517
+ end
518
+ elsif EvaluatorHelpers.context_key_in_target_list(context, target.kind, target.values)
519
+ return target.match_result
520
+ end
283
521
  end
284
- return get_variation(flag, index, reason)
522
+
523
+ nil
285
524
  end
286
525
  end
287
526
  end