launchdarkly-server-sdk 6.4.0 → 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.
- checksums.yaml +4 -4
- data/lib/ldclient-rb/config.rb +102 -56
- data/lib/ldclient-rb/context.rb +487 -0
- data/lib/ldclient-rb/evaluation_detail.rb +20 -20
- data/lib/ldclient-rb/events.rb +77 -132
- data/lib/ldclient-rb/flags_state.rb +4 -4
- data/lib/ldclient-rb/impl/big_segments.rb +17 -17
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +379 -131
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +28 -31
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +6 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
- data/lib/ldclient-rb/impl/event_types.rb +18 -30
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
- data/lib/ldclient-rb/impl/model/clause.rb +39 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +8 -121
- data/lib/ldclient-rb/impl/model/segment.rb +126 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +52 -12
- data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +2 -2
- data/lib/ldclient-rb/in_memory_store.rb +2 -2
- data/lib/ldclient-rb/integrations/consul.rb +1 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/file_data.rb +3 -3
- data/lib/ldclient-rb/integrations/redis.rb +4 -4
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
- data/lib/ldclient-rb/integrations/test_data.rb +16 -12
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
- data/lib/ldclient-rb/interfaces.rb +14 -14
- data/lib/ldclient-rb/ldclient.rb +94 -144
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +2 -2
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +5 -5
- data/lib/ldclient-rb/stream.rb +7 -8
- data/lib/ldclient-rb/util.rb +4 -19
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +2 -3
- metadata +34 -17
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -2,6 +2,8 @@ require "ldclient-rb/evaluation_detail"
|
|
2
2
|
require "ldclient-rb/impl/evaluator_bucketing"
|
3
3
|
require "ldclient-rb/impl/evaluator_helpers"
|
4
4
|
require "ldclient-rb/impl/evaluator_operators"
|
5
|
+
require "ldclient-rb/impl/model/feature_flag"
|
6
|
+
require "ldclient-rb/impl/model/segment"
|
5
7
|
|
6
8
|
module LaunchDarkly
|
7
9
|
module Impl
|
@@ -12,6 +14,81 @@ module LaunchDarkly
|
|
12
14
|
:detail # the EvaluationDetail representing the evaluation result
|
13
15
|
)
|
14
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
|
+
|
15
92
|
# Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
|
16
93
|
# if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
|
17
94
|
# is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
|
@@ -22,7 +99,7 @@ module LaunchDarkly
|
|
22
99
|
# @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
|
23
100
|
# currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
|
24
101
|
# flag data - or nil if the flag is unknown or deleted
|
25
|
-
# @param get_segment [Function] similar to `get_flag`, but is used to query a
|
102
|
+
# @param get_segment [Function] similar to `get_flag`, but is used to query a context segment.
|
26
103
|
# @param logger [Logger] the client's logger
|
27
104
|
def initialize(get_flag, get_segment, get_big_segments_membership, logger)
|
28
105
|
@get_flag = get_flag
|
@@ -30,15 +107,15 @@ module LaunchDarkly
|
|
30
107
|
@get_big_segments_membership = get_big_segments_membership
|
31
108
|
@logger = logger
|
32
109
|
end
|
33
|
-
|
110
|
+
|
34
111
|
# Used internally to hold an evaluation result and additional state that may be accumulated during an
|
35
112
|
# evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
|
36
113
|
# trying to use a pure functional approach, and since we're not exposing this object to any application code
|
37
114
|
# or retaining it anywhere, we don't have to be quite as strict about immutability.
|
38
115
|
#
|
39
116
|
# The big_segments_status and big_segments_membership properties are not used by the caller; they are used
|
40
|
-
# during an evaluation to cache the result of any Big Segments query that we've done for this
|
41
|
-
# we don't want to do multiple queries for the same
|
117
|
+
# during an evaluation to cache the result of any Big Segments query that we've done for this context, because
|
118
|
+
# we don't want to do multiple queries for the same context if multiple Big Segments are referenced in the same
|
42
119
|
# evaluation.
|
43
120
|
EvalResult = Struct.new(
|
44
121
|
:detail, # the EvaluationDetail representing the evaluation result
|
@@ -56,223 +133,394 @@ module LaunchDarkly
|
|
56
133
|
# any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
|
57
134
|
# default value. Error conditions produce a result with a nil value and an error reason, not an exception.
|
58
135
|
#
|
59
|
-
# @param flag [
|
60
|
-
# @param
|
61
|
-
# @return [EvalResult] the evaluation result
|
62
|
-
def evaluate(flag,
|
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
|
+
|
63
142
|
result = EvalResult.new
|
64
|
-
|
65
|
-
|
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))
|
66
152
|
return result
|
67
153
|
end
|
68
|
-
|
69
|
-
|
70
|
-
if !result.big_segments_status.nil?
|
154
|
+
|
155
|
+
unless result.big_segments_status.nil?
|
71
156
|
# If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
|
72
157
|
# some point and we will want to include the status in the evaluation reason.
|
73
158
|
detail = EvaluationDetail.new(detail.value, detail.variation_index,
|
74
159
|
detail.reason.with_big_segments_status(result.big_segments_status))
|
75
160
|
end
|
76
161
|
result.detail = detail
|
77
|
-
|
162
|
+
result
|
78
163
|
end
|
79
164
|
|
165
|
+
# @param segment [LaunchDarkly::Impl::Model::Segment]
|
80
166
|
def self.make_big_segment_ref(segment) # method is visible for testing
|
81
167
|
# The format of Big Segment references is independent of what store implementation is being
|
82
168
|
# used; the store implementation receives only this string and does not know the details of
|
83
169
|
# the data model. The Relay Proxy will use the same format when writing to the store.
|
84
|
-
"#{segment
|
170
|
+
"#{segment.key}.g#{segment.generation}"
|
85
171
|
end
|
86
172
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
92
181
|
end
|
93
182
|
|
94
|
-
prereq_failure_result = check_prerequisites(flag,
|
95
|
-
return prereq_failure_result
|
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?
|
96
189
|
|
97
|
-
# Check user target matches
|
98
|
-
(flag[:targets] || []).each do |target|
|
99
|
-
(target[:values] || []).each do |value|
|
100
|
-
if value == user[:key]
|
101
|
-
return EvaluatorHelpers.target_match_result(target, flag)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
190
|
# Check custom rules
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
if rule_match_user(rule, user, state)
|
111
|
-
reason = rule[:_reason] # try to use cached reason for this rule
|
112
|
-
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
113
|
-
return get_value_for_variation_or_rollout(flag, rule, user, reason,
|
114
|
-
EvaluatorHelpers.rule_precomputed_results(rule))
|
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)
|
115
194
|
end
|
116
195
|
end
|
117
196
|
|
118
197
|
# Check the fallthrough rule
|
119
|
-
|
120
|
-
return get_value_for_variation_or_rollout(flag, flag
|
121
|
-
EvaluatorHelpers.fallthrough_precomputed_results(flag))
|
198
|
+
unless flag.fallthrough.nil?
|
199
|
+
return get_value_for_variation_or_rollout(flag, flag.fallthrough, context, flag.fallthrough_results)
|
122
200
|
end
|
123
201
|
|
124
|
-
|
202
|
+
EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
125
203
|
end
|
126
204
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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?
|
132
212
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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)
|
139
233
|
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
140
234
|
# off variation was. But we still need to evaluate it in order to generate an event.
|
141
|
-
if !prereq_flag
|
235
|
+
if !prereq_flag.on || prereq_res.variation_index != prerequisite.variation
|
142
236
|
prereq_ok = false
|
143
237
|
end
|
144
238
|
prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
150
245
|
end
|
151
246
|
end
|
152
|
-
|
153
|
-
|
154
|
-
end
|
247
|
+
ensure
|
248
|
+
state.prereq_stack.pop
|
155
249
|
end
|
250
|
+
|
156
251
|
nil
|
157
252
|
end
|
158
253
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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)
|
164
262
|
end
|
165
263
|
|
166
|
-
|
264
|
+
true
|
167
265
|
end
|
168
266
|
|
169
|
-
|
170
|
-
|
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,
|
171
274
|
# and possibly negate
|
172
|
-
if clause
|
173
|
-
result =
|
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
|
+
|
174
283
|
segment = @get_segment.call(v)
|
175
|
-
!segment.nil? &&
|
284
|
+
!segment.nil? && segment_match_context(segment, context, eval_result, state)
|
176
285
|
}
|
177
|
-
clause
|
286
|
+
clause.negate ? !result : result
|
178
287
|
else
|
179
|
-
|
288
|
+
clause_match_context_no_segments(clause, context)
|
180
289
|
end
|
181
290
|
end
|
182
291
|
|
183
|
-
|
184
|
-
|
185
|
-
|
292
|
+
# @param clause [LaunchDarkly::Impl::Model::Clause]
|
293
|
+
# @param context_value [any]
|
294
|
+
# @return [Boolean]
|
295
|
+
private def match_any_clause_value(clause, context_value)
|
296
|
+
op = clause.op
|
297
|
+
clause.values.any? { |cv| EvaluatorOperators.apply(op, context_value, cv) }
|
298
|
+
end
|
299
|
+
|
300
|
+
# @param clause [LaunchDarkly::Impl::Model::Clause]
|
301
|
+
# @param context [LaunchDarkly::LDContext]
|
302
|
+
# @return [Boolean]
|
303
|
+
private def clause_match_by_kind(clause, context)
|
304
|
+
# If attribute is "kind", then we treat operator and values as a match
|
305
|
+
# expression against a list of all individual kinds in the context.
|
306
|
+
# That is, for a multi-kind context with kinds of "org" and "user", it
|
307
|
+
# is a match if either of those strings is a match with Operator and
|
308
|
+
# Values.
|
186
309
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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) }
|
191
340
|
else
|
192
|
-
|
341
|
+
match_any_clause_value(clause, context_val)
|
193
342
|
end
|
194
|
-
clause
|
343
|
+
clause.negate ? !result : result
|
195
344
|
end
|
196
345
|
|
197
|
-
|
198
|
-
|
199
|
-
|
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)
|
200
355
|
end
|
201
356
|
|
202
|
-
|
203
|
-
|
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
|
204
364
|
# Big segment queries can only be done if the generation is known. If it's unset,
|
205
365
|
# that probably means the data store was populated by an older SDK that doesn't know
|
206
366
|
# about the generation property and therefore dropped it from the JSON data. We'll treat
|
207
367
|
# that as a "not configured" condition.
|
208
|
-
|
368
|
+
eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
209
369
|
return false
|
210
370
|
end
|
211
|
-
|
212
|
-
|
371
|
+
|
372
|
+
matched_context = context.individual_context(segment.unbounded_context_kind)
|
373
|
+
return false if matched_context.nil?
|
374
|
+
|
375
|
+
membership = eval_result.big_segments_membership.nil? ? nil : eval_result.big_segments_membership[matched_context.key]
|
376
|
+
|
377
|
+
if membership.nil?
|
378
|
+
# Note that this query is just by key; the context kind doesn't matter because any given
|
379
|
+
# Big Segment can only reference one context kind. So if segment A for the "user" kind
|
380
|
+
# includes a "user" context with key X, and segment B for the "org" kind includes an "org"
|
381
|
+
# context with the same key X, it is fine to say that the membership for key X is
|
382
|
+
# segment A and segment B-- there is no ambiguity.
|
383
|
+
result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(matched_context.key)
|
213
384
|
if result
|
214
|
-
|
215
|
-
|
385
|
+
eval_result.big_segments_status = result.status
|
386
|
+
|
387
|
+
membership = result.membership
|
388
|
+
eval_result.big_segments_membership = {} if eval_result.big_segments_membership.nil?
|
389
|
+
eval_result.big_segments_membership[matched_context.key] = membership
|
216
390
|
else
|
217
|
-
|
218
|
-
state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
391
|
+
eval_result.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
219
392
|
end
|
220
393
|
end
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
394
|
+
|
395
|
+
membership_result = nil
|
396
|
+
unless membership.nil?
|
397
|
+
segment_ref = Evaluator.make_big_segment_ref(segment)
|
398
|
+
membership_result = membership.nil? ? nil : membership[segment_ref]
|
399
|
+
end
|
400
|
+
|
401
|
+
return membership_result unless membership_result.nil?
|
402
|
+
simple_segment_match_context(segment, context, false, eval_result, state)
|
226
403
|
end
|
227
404
|
|
228
|
-
|
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)
|
229
411
|
if use_includes_and_excludes
|
230
|
-
|
231
|
-
|
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
|
232
431
|
end
|
233
432
|
|
234
|
-
|
235
|
-
|
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
|
236
442
|
end
|
237
443
|
|
238
|
-
|
444
|
+
false
|
239
445
|
end
|
240
446
|
|
241
|
-
|
242
|
-
|
243
|
-
|
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)
|
244
456
|
end
|
245
457
|
|
246
458
|
# If the weight is absent, this rule matches
|
247
|
-
return true
|
248
|
-
|
459
|
+
return true unless rule.weight
|
460
|
+
|
249
461
|
# All of the clauses are met. See if the user buckets in
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
253
470
|
end
|
254
471
|
|
255
|
-
private
|
256
|
-
|
257
|
-
|
258
|
-
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
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)
|
474
|
+
|
259
475
|
if index.nil?
|
260
|
-
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag
|
476
|
+
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag.key}\": variation/rollout object with no variation or rollout")
|
261
477
|
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
262
478
|
end
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
479
|
+
precomputed_results.for_variation(index, in_experiment)
|
480
|
+
end
|
481
|
+
|
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?
|
493
|
+
|
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
|
502
|
+
end
|
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
|
272
517
|
end
|
518
|
+
elsif EvaluatorHelpers.context_key_in_target_list(context, target.kind, target.values)
|
519
|
+
return target.match_result
|
273
520
|
end
|
274
|
-
return EvaluatorHelpers.evaluation_detail_for_variation(flag, index, reason)
|
275
521
|
end
|
522
|
+
|
523
|
+
nil
|
276
524
|
end
|
277
525
|
end
|
278
526
|
end
|