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