launchdarkly-server-sdk 8.8.3-java

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