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