launchdarkly-server-sdk 6.2.5 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/lib/ldclient-rb/config.rb +203 -43
  4. data/lib/ldclient-rb/context.rb +487 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +85 -26
  6. data/lib/ldclient-rb/events.rb +185 -146
  7. data/lib/ldclient-rb/flags_state.rb +25 -14
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/context.rb +96 -0
  10. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  11. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  12. data/lib/ldclient-rb/impl/evaluator.rb +428 -132
  13. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  14. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  15. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  16. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  17. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  18. data/lib/ldclient-rb/impl/event_types.rb +78 -0
  19. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  20. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +92 -28
  21. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  22. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +165 -32
  23. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  24. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  25. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  26. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  27. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  28. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  29. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  30. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  32. data/lib/ldclient-rb/impl/util.rb +62 -1
  33. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  34. data/lib/ldclient-rb/integrations/consul.rb +9 -2
  35. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  36. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +43 -3
  38. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +594 -0
  39. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  40. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +14 -9
  41. data/lib/ldclient-rb/integrations.rb +2 -51
  42. data/lib/ldclient-rb/interfaces.rb +151 -1
  43. data/lib/ldclient-rb/ldclient.rb +175 -133
  44. data/lib/ldclient-rb/memoized_value.rb +1 -1
  45. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  46. data/lib/ldclient-rb/polling.rb +22 -41
  47. data/lib/ldclient-rb/reference.rb +274 -0
  48. data/lib/ldclient-rb/requestor.rb +7 -7
  49. data/lib/ldclient-rb/stream.rb +9 -9
  50. data/lib/ldclient-rb/util.rb +11 -17
  51. data/lib/ldclient-rb/version.rb +1 -1
  52. data/lib/ldclient-rb.rb +2 -4
  53. metadata +49 -23
  54. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  55. data/lib/ldclient-rb/file_data_source.rb +0 -314
  56. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  57. data/lib/ldclient-rb/newrelic.rb +0 -17
  58. data/lib/ldclient-rb/redis_store.rb +0 -88
  59. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,9 +1,94 @@
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
10
+ # Used internally to record that we evaluated a prerequisite flag.
11
+ PrerequisiteEvalRecord = Struct.new(
12
+ :prereq_flag, # the prerequisite flag that we evaluated
13
+ :prereq_of_flag, # the flag that it was a prerequisite of
14
+ :detail # the EvaluationDetail representing the evaluation result
15
+ )
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
+
7
92
  # Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
8
93
  # if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
9
94
  # is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
@@ -14,18 +99,30 @@ module LaunchDarkly
14
99
  # @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
15
100
  # currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
16
101
  # flag data - or nil if the flag is unknown or deleted
17
- # @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.
18
103
  # @param logger [Logger] the client's logger
19
- def initialize(get_flag, get_segment, logger)
104
+ def initialize(get_flag, get_segment, get_big_segments_membership, logger)
20
105
  @get_flag = get_flag
21
106
  @get_segment = get_segment
107
+ @get_big_segments_membership = get_big_segments_membership
22
108
  @logger = logger
23
109
  end
24
110
 
25
- # Used internally to hold an evaluation result and the events that were generated from prerequisites. The
26
- # `detail` property is an EvaluationDetail. The `events` property can be either an array of feature request
27
- # events or nil.
28
- EvalResult = Struct.new(:detail, :events)
111
+ # Used internally to hold an evaluation result and additional state that may be accumulated during an
112
+ # evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
113
+ # trying to use a pure functional approach, and since we're not exposing this object to any application code
114
+ # or retaining it anywhere, we don't have to be quite as strict about immutability.
115
+ #
116
+ # The big_segments_status and big_segments_membership properties are not used by the caller; they are used
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
119
+ # evaluation.
120
+ EvalResult = Struct.new(
121
+ :detail, # the EvaluationDetail representing the evaluation result
122
+ :prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil
123
+ :big_segments_status,
124
+ :big_segments_membership
125
+ )
29
126
 
30
127
  # Helper function used internally to construct an EvaluationDetail for an error result.
31
128
  def self.error_result(errorKind, value = nil)
@@ -36,195 +133,394 @@ module LaunchDarkly
36
133
  # any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
37
134
  # default value. Error conditions produce a result with a nil value and an error reason, not an exception.
38
135
  #
39
- # @param flag [Object] the flag
40
- # @param user [Object] the user properties
41
- # @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
42
- # evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
43
- # @return [EvalResult] the evaluation result
44
- def evaluate(flag, user, event_factory)
45
- if user.nil? || user[:key].nil?
46
- return EvalResult.new(Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED), [])
47
- end
48
-
49
- # If the flag doesn't have any prerequisites (which most flags don't) then it cannot generate any feature
50
- # request events for prerequisites and we can skip allocating an array.
51
- if flag[:prerequisites] && !flag[:prerequisites].empty?
52
- events = []
53
- else
54
- events = nil
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
+
142
+ result = EvalResult.new
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))
152
+ return result
55
153
  end
56
154
 
57
- detail = eval_internal(flag, user, events, event_factory)
58
- return EvalResult.new(detail, events.nil? || events.empty? ? nil : events)
155
+ unless result.big_segments_status.nil?
156
+ # If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
157
+ # some point and we will want to include the status in the evaluation reason.
158
+ detail = EvaluationDetail.new(detail.value, detail.variation_index,
159
+ detail.reason.with_big_segments_status(result.big_segments_status))
160
+ end
161
+ result.detail = detail
162
+ result
59
163
  end
60
164
 
61
- private
62
-
63
- def eval_internal(flag, user, events, event_factory)
64
- if !flag[:on]
65
- return get_off_value(flag, EvaluationReason::off)
66
- end
165
+ # @param segment [LaunchDarkly::Impl::Model::Segment]
166
+ def self.make_big_segment_ref(segment) # method is visible for testing
167
+ # The format of Big Segment references is independent of what store implementation is being
168
+ # used; the store implementation receives only this string and does not know the details of
169
+ # the data model. The Relay Proxy will use the same format when writing to the store.
170
+ "#{segment.key}.g#{segment.generation}"
171
+ end
67
172
 
68
- prereq_failure_reason = check_prerequisites(flag, user, events, event_factory)
69
- if !prereq_failure_reason.nil?
70
- return get_off_value(flag, prereq_failure_reason)
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
71
181
  end
72
182
 
73
- # Check user target matches
74
- (flag[:targets] || []).each do |target|
75
- (target[:values] || []).each do |value|
76
- if value == user[:key]
77
- return get_variation(flag, target[:variation], EvaluationReason::target_match)
78
- end
79
- end
80
- end
81
-
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?
189
+
82
190
  # Check custom rules
83
- rules = flag[:rules] || []
84
- rules.each_index do |i|
85
- rule = rules[i]
86
- if rule_match_user(rule, user)
87
- reason = rule[:_reason] # try to use cached reason for this rule
88
- reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
89
- 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)
90
194
  end
91
195
  end
92
196
 
93
197
  # Check the fallthrough rule
94
- if !flag[:fallthrough].nil?
95
- 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)
96
200
  end
97
201
 
98
- return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
202
+ EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
99
203
  end
100
204
 
101
- def check_prerequisites(flag, user, events, event_factory)
102
- (flag[:prerequisites] || []).each do |prerequisite|
103
- prereq_ok = true
104
- prereq_key = prerequisite[:key]
105
- 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?
106
212
 
107
- if prereq_flag.nil?
108
- @logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
109
- prereq_ok = false
110
- else
111
- begin
112
- prereq_res = eval_internal(prereq_flag, user, events, event_factory)
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)
113
233
  # Note that if the prerequisite flag is off, we don't consider it a match no matter what its
114
234
  # off variation was. But we still need to evaluate it in order to generate an event.
115
- if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
235
+ if !prereq_flag.on || prereq_res.variation_index != prerequisite.variation
116
236
  prereq_ok = false
117
237
  end
118
- event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
119
- events.push(event)
120
- rescue => exn
121
- Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
122
- prereq_ok = false
238
+ prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
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
123
245
  end
124
246
  end
125
- if !prereq_ok
126
- reason = prerequisite[:_reason] # try to use cached reason
127
- return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
128
- end
247
+ ensure
248
+ state.prereq_stack.pop
129
249
  end
250
+
130
251
  nil
131
252
  end
132
253
 
133
- def rule_match_user(rule, user)
134
- return false if !rule[:clauses]
135
-
136
- (rule[:clauses] || []).each do |clause|
137
- return false if !clause_match_user(clause, user)
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)
138
262
  end
139
263
 
140
- return true
264
+ true
141
265
  end
142
266
 
143
- def clause_match_user(clause, user)
144
- # 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,
145
274
  # and possibly negate
146
- if clause[:op].to_sym == :segmentMatch
147
- 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
+
148
283
  segment = @get_segment.call(v)
149
- !segment.nil? && segment_match_user(segment, user)
284
+ !segment.nil? && segment_match_context(segment, context, eval_result, state)
150
285
  }
151
- clause[:negate] ? !result : result
286
+ clause.negate ? !result : result
152
287
  else
153
- clause_match_user_no_segments(clause, user)
288
+ clause_match_context_no_segments(clause, context)
154
289
  end
155
290
  end
156
291
 
157
- def clause_match_user_no_segments(clause, user)
158
- user_val = EvaluatorOperators.user_value(user, clause[:attribute])
159
- 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
160
299
 
161
- op = clause[:op].to_sym
162
- clause_vals = clause[:values]
163
- result = if user_val.is_a? Enumerable
164
- user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
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.
309
+
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) }
165
340
  else
166
- clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
341
+ match_any_clause_value(clause, context_val)
167
342
  end
168
- clause[:negate] ? !result : result
343
+ clause.negate ? !result : result
344
+ end
345
+
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)
169
355
  end
170
356
 
171
- def segment_match_user(segment, user)
172
- return false unless user[:key]
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
364
+ # Big segment queries can only be done if the generation is known. If it's unset,
365
+ # that probably means the data store was populated by an older SDK that doesn't know
366
+ # about the generation property and therefore dropped it from the JSON data. We'll treat
367
+ # that as a "not configured" condition.
368
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
369
+ return false
370
+ end
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]
173
376
 
174
- return true if segment[:included].include?(user[:key])
175
- return false if segment[:excluded].include?(user[:key])
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)
384
+ if result
385
+ eval_result.big_segments_status = result.status
176
386
 
177
- (segment[:rules] || []).each do |r|
178
- return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
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
390
+ else
391
+ eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
392
+ end
393
+ end
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]
179
399
  end
180
400
 
181
- return false
401
+ return membership_result unless membership_result.nil?
402
+ simple_segment_match_context(segment, context, false, eval_result, state)
182
403
  end
183
404
 
184
- def segment_rule_match_user(rule, user, segment_key, salt)
185
- (rule[:clauses] || []).each do |c|
186
- return false unless clause_match_user_no_segments(c, user)
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)
411
+ if use_includes_and_excludes
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
431
+ end
432
+
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
442
+ end
443
+
444
+ false
445
+ end
446
+
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)
187
456
  end
188
457
 
189
458
  # If the weight is absent, this rule matches
190
- return true if !rule[:weight]
191
-
459
+ return true unless rule.weight
460
+
192
461
  # All of the clauses are met. See if the user buckets in
193
- bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
194
- weight = rule[:weight].to_f / 100000.0
195
- 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
196
470
  end
197
471
 
198
- 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)
199
474
 
200
- def get_variation(flag, index, reason)
201
- if index < 0 || index >= flag[:variations].length
202
- @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")
203
477
  return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
204
478
  end
205
- EvaluationDetail.new(flag[:variations][index], index, reason)
479
+ precomputed_results.for_variation(index, in_experiment)
206
480
  end
207
481
 
208
- def get_off_value(flag, reason)
209
- if flag[:offVariation].nil? # off variation unspecified - return default value
210
- return EvaluationDetail.new(nil, nil, reason)
211
- end
212
- get_variation(flag, flag[:offVariation], reason)
213
- 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?
214
493
 
215
- def get_value_for_variation_or_rollout(flag, vr, user, reason)
216
- index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
217
- #if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
218
- if in_experiment && reason.kind == :FALLTHROUGH
219
- reason = EvaluationReason::fallthrough(in_experiment)
220
- elsif in_experiment && reason.kind == :RULE_MATCH
221
- 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
222
502
  end
223
- if index.nil?
224
- @logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
225
- 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
226
521
  end
227
- return get_variation(flag, index, reason)
522
+
523
+ nil
228
524
  end
229
525
  end
230
526
  end