launchdarkly-server-sdk 6.3.4 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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