launchdarkly-server-sdk 6.2.5 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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