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.
- checksums.yaml +7 -0
- data/LICENSE.txt +13 -0
- data/README.md +61 -0
- data/lib/launchdarkly-server-sdk.rb +1 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +658 -0
- data/lib/ldclient-rb/context.rb +565 -0
- data/lib/ldclient-rb/evaluation_detail.rb +387 -0
- data/lib/ldclient-rb/events.rb +642 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/flags_state.rb +88 -0
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +166 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +109 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +129 -0
- data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
- data/lib/ldclient-rb/impl/evaluator.rb +539 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +86 -0
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +131 -0
- data/lib/ldclient-rb/impl/event_sender.rb +100 -0
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +170 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +300 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +229 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +306 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +254 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +72 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +46 -0
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +141 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/impl/util.rb +95 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations/consul.rb +45 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +92 -0
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +98 -0
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +663 -0
- data/lib/ldclient-rb/integrations/test_data.rb +213 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +246 -0
- data/lib/ldclient-rb/integrations.rb +6 -0
- data/lib/ldclient-rb/interfaces.rb +974 -0
- data/lib/ldclient-rb/ldclient.rb +822 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +102 -0
- data/lib/ldclient-rb/reference.rb +295 -0
- data/lib/ldclient-rb/requestor.rb +102 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +196 -0
- data/lib/ldclient-rb/util.rb +132 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/lib/ldclient-rb.rb +27 -0
- 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
|