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