launchdarkly-server-sdk 6.3.1 → 6.4.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 +37 -0
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/evaluator.rb +42 -46
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
- data/lib/ldclient-rb/impl/event_types.rb +90 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +1 -1
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
- data/lib/ldclient-rb/impl/util.rb +58 -0
- data/lib/ldclient-rb/integrations/consul.rb +1 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/redis.rb +1 -1
- data/lib/ldclient-rb/ldclient.rb +106 -28
- data/lib/ldclient-rb/requestor.rb +2 -2
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +9 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +11 -9
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
@@ -1,9 +1,17 @@
|
|
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"
|
4
5
|
|
5
6
|
module LaunchDarkly
|
6
7
|
module Impl
|
8
|
+
# Used internally to record that we evaluated a prerequisite flag.
|
9
|
+
PrerequisiteEvalRecord = Struct.new(
|
10
|
+
:prereq_flag, # the prerequisite flag that we evaluated
|
11
|
+
:prereq_of_flag, # the flag that it was a prerequisite of
|
12
|
+
:detail # the EvaluationDetail representing the evaluation result
|
13
|
+
)
|
14
|
+
|
7
15
|
# Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
|
8
16
|
# if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
|
9
17
|
# is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
|
@@ -22,7 +30,7 @@ module LaunchDarkly
|
|
22
30
|
@get_big_segments_membership = get_big_segments_membership
|
23
31
|
@logger = logger
|
24
32
|
end
|
25
|
-
|
33
|
+
|
26
34
|
# Used internally to hold an evaluation result and additional state that may be accumulated during an
|
27
35
|
# evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
|
28
36
|
# trying to use a pure functional approach, and since we're not exposing this object to any application code
|
@@ -34,7 +42,7 @@ module LaunchDarkly
|
|
34
42
|
# evaluation.
|
35
43
|
EvalResult = Struct.new(
|
36
44
|
:detail, # the EvaluationDetail representing the evaluation result
|
37
|
-
:
|
45
|
+
:prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil
|
38
46
|
:big_segments_status,
|
39
47
|
:big_segments_membership
|
40
48
|
)
|
@@ -50,17 +58,15 @@ module LaunchDarkly
|
|
50
58
|
#
|
51
59
|
# @param flag [Object] the flag
|
52
60
|
# @param user [Object] the user properties
|
53
|
-
# @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
|
54
|
-
# evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
|
55
61
|
# @return [EvalResult] the evaluation result
|
56
|
-
def evaluate(flag, user
|
62
|
+
def evaluate(flag, user)
|
57
63
|
result = EvalResult.new
|
58
64
|
if user.nil? || user[:key].nil?
|
59
65
|
result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
|
60
66
|
return result
|
61
67
|
end
|
62
68
|
|
63
|
-
detail = eval_internal(flag, user, result
|
69
|
+
detail = eval_internal(flag, user, result)
|
64
70
|
if !result.big_segments_status.nil?
|
65
71
|
# If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
|
66
72
|
# some point and we will want to include the status in the evaluation reason.
|
@@ -80,21 +86,19 @@ module LaunchDarkly
|
|
80
86
|
|
81
87
|
private
|
82
88
|
|
83
|
-
def eval_internal(flag, user, state
|
89
|
+
def eval_internal(flag, user, state)
|
84
90
|
if !flag[:on]
|
85
|
-
return
|
91
|
+
return EvaluatorHelpers.off_result(flag)
|
86
92
|
end
|
87
93
|
|
88
|
-
|
89
|
-
if !
|
90
|
-
return get_off_value(flag, prereq_failure_reason)
|
91
|
-
end
|
94
|
+
prereq_failure_result = check_prerequisites(flag, user, state)
|
95
|
+
return prereq_failure_result if !prereq_failure_result.nil?
|
92
96
|
|
93
97
|
# Check user target matches
|
94
98
|
(flag[:targets] || []).each do |target|
|
95
99
|
(target[:values] || []).each do |value|
|
96
100
|
if value == user[:key]
|
97
|
-
return
|
101
|
+
return EvaluatorHelpers.target_match_result(target, flag)
|
98
102
|
end
|
99
103
|
end
|
100
104
|
end
|
@@ -106,19 +110,21 @@ module LaunchDarkly
|
|
106
110
|
if rule_match_user(rule, user, state)
|
107
111
|
reason = rule[:_reason] # try to use cached reason for this rule
|
108
112
|
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
109
|
-
return get_value_for_variation_or_rollout(flag, rule, user, reason
|
113
|
+
return get_value_for_variation_or_rollout(flag, rule, user, reason,
|
114
|
+
EvaluatorHelpers.rule_precomputed_results(rule))
|
110
115
|
end
|
111
116
|
end
|
112
117
|
|
113
118
|
# Check the fallthrough rule
|
114
119
|
if !flag[:fallthrough].nil?
|
115
|
-
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough
|
120
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough,
|
121
|
+
EvaluatorHelpers.fallthrough_precomputed_results(flag))
|
116
122
|
end
|
117
123
|
|
118
124
|
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
119
125
|
end
|
120
126
|
|
121
|
-
def check_prerequisites(flag, user, state
|
127
|
+
def check_prerequisites(flag, user, state)
|
122
128
|
(flag[:prerequisites] || []).each do |prerequisite|
|
123
129
|
prereq_ok = true
|
124
130
|
prereq_key = prerequisite[:key]
|
@@ -129,23 +135,22 @@ module LaunchDarkly
|
|
129
135
|
prereq_ok = false
|
130
136
|
else
|
131
137
|
begin
|
132
|
-
prereq_res = eval_internal(prereq_flag, user, state
|
138
|
+
prereq_res = eval_internal(prereq_flag, user, state)
|
133
139
|
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
134
140
|
# off variation was. But we still need to evaluate it in order to generate an event.
|
135
141
|
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
136
142
|
prereq_ok = false
|
137
143
|
end
|
138
|
-
|
139
|
-
state.
|
140
|
-
state.
|
144
|
+
prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
|
145
|
+
state.prereq_evals = [] if state.prereq_evals.nil?
|
146
|
+
state.prereq_evals.push(prereq_eval)
|
141
147
|
rescue => exn
|
142
148
|
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
143
149
|
prereq_ok = false
|
144
150
|
end
|
145
151
|
end
|
146
152
|
if !prereq_ok
|
147
|
-
|
148
|
-
return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
|
153
|
+
return EvaluatorHelpers.prerequisite_failed_result(prerequisite, flag)
|
149
154
|
end
|
150
155
|
end
|
151
156
|
nil
|
@@ -248,35 +253,26 @@ module LaunchDarkly
|
|
248
253
|
end
|
249
254
|
|
250
255
|
private
|
251
|
-
|
252
|
-
def
|
253
|
-
if index < 0 || index >= flag[:variations].length
|
254
|
-
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
255
|
-
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
256
|
-
end
|
257
|
-
EvaluationDetail.new(flag[:variations][index], index, reason)
|
258
|
-
end
|
259
|
-
|
260
|
-
def get_off_value(flag, reason)
|
261
|
-
if flag[:offVariation].nil? # off variation unspecified - return default value
|
262
|
-
return EvaluationDetail.new(nil, nil, reason)
|
263
|
-
end
|
264
|
-
get_variation(flag, flag[:offVariation], reason)
|
265
|
-
end
|
266
|
-
|
267
|
-
def get_value_for_variation_or_rollout(flag, vr, user, reason)
|
256
|
+
|
257
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason, precomputed_results)
|
268
258
|
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
269
|
-
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
270
|
-
if in_experiment && reason.kind == :FALLTHROUGH
|
271
|
-
reason = EvaluationReason::fallthrough(in_experiment)
|
272
|
-
elsif in_experiment && reason.kind == :RULE_MATCH
|
273
|
-
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
274
|
-
end
|
275
259
|
if index.nil?
|
276
260
|
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
277
261
|
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
278
262
|
end
|
279
|
-
|
263
|
+
if precomputed_results
|
264
|
+
return precomputed_results.for_variation(index, in_experiment)
|
265
|
+
else
|
266
|
+
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
267
|
+
if in_experiment
|
268
|
+
if reason.kind == :FALLTHROUGH
|
269
|
+
reason = EvaluationReason::fallthrough(in_experiment)
|
270
|
+
elsif reason.kind == :RULE_MATCH
|
271
|
+
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
return EvaluatorHelpers.evaluation_detail_for_variation(flag, index, reason)
|
275
|
+
end
|
280
276
|
end
|
281
277
|
end
|
282
278
|
end
|
@@ -0,0 +1,53 @@
|
|
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
|
+
def self.off_result(flag, logger = nil)
|
10
|
+
pre = flag[:_preprocessed]
|
11
|
+
pre ? pre.off_result : evaluation_detail_for_off_variation(flag, EvaluationReason::off, logger)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.target_match_result(target, flag, logger = nil)
|
15
|
+
pre = target[:_preprocessed]
|
16
|
+
pre ? pre.match_result : evaluation_detail_for_variation(
|
17
|
+
flag, target[:variation], EvaluationReason::target_match, logger)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.prerequisite_failed_result(prereq, flag, logger = nil)
|
21
|
+
pre = prereq[:_preprocessed]
|
22
|
+
pre ? pre.failed_result : evaluation_detail_for_off_variation(
|
23
|
+
flag, EvaluationReason::prerequisite_failed(prereq[:key]), logger
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.fallthrough_precomputed_results(flag)
|
28
|
+
pre = flag[:_preprocessed]
|
29
|
+
pre ? pre.fallthrough_factory : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.rule_precomputed_results(rule)
|
33
|
+
pre = rule[:_preprocessed]
|
34
|
+
pre ? pre.all_match_results : nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.evaluation_detail_for_off_variation(flag, reason, logger = nil)
|
38
|
+
index = flag[:offVariation]
|
39
|
+
index.nil? ? EvaluationDetail.new(nil, nil, reason) : evaluation_detail_for_variation(flag, index, reason, logger)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.evaluation_detail_for_variation(flag, index, reason, logger = nil)
|
43
|
+
vars = flag[:variations] || []
|
44
|
+
if index < 0 || index >= vars.length
|
45
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index") unless logger.nil?
|
46
|
+
EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
47
|
+
else
|
48
|
+
EvaluationDetail.new(vars[index], index, reason)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -89,7 +89,7 @@ module LaunchDarkly
|
|
89
89
|
|
90
90
|
private
|
91
91
|
|
92
|
-
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
92
|
+
BUILTINS = Set[:key, :secondary, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
93
|
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
94
|
|
95
95
|
private_constant :BUILTINS
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "ldclient-rb/impl/event_types"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
EventSummary = Struct.new(:start_date, :end_date, :counters)
|
6
|
+
|
7
|
+
EventSummaryFlagInfo = Struct.new(:default, :versions)
|
8
|
+
|
9
|
+
EventSummaryFlagVariationCounter = Struct.new(:value, :count)
|
10
|
+
|
11
|
+
# Manages the state of summarizable information for the EventProcessor, including the
|
12
|
+
# event counters and user deduplication. Note that the methods of this class are
|
13
|
+
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
|
14
|
+
# synchronization across both the summarizer and the event queue.
|
15
|
+
class EventSummarizer
|
16
|
+
class Counter
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
clear
|
21
|
+
end
|
22
|
+
|
23
|
+
# Adds this event to our counters, if it is a type of event we need to count.
|
24
|
+
def summarize_event(event)
|
25
|
+
return if !event.is_a?(LaunchDarkly::Impl::EvalEvent)
|
26
|
+
|
27
|
+
counters_for_flag = @counters[event.key]
|
28
|
+
if counters_for_flag.nil?
|
29
|
+
counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new)
|
30
|
+
@counters[event.key] = counters_for_flag
|
31
|
+
end
|
32
|
+
counters_for_flag_version = counters_for_flag.versions[event.version]
|
33
|
+
if counters_for_flag_version.nil?
|
34
|
+
counters_for_flag_version = Hash.new
|
35
|
+
counters_for_flag.versions[event.version] = counters_for_flag_version
|
36
|
+
end
|
37
|
+
variation_counter = counters_for_flag_version[event.variation]
|
38
|
+
if variation_counter.nil?
|
39
|
+
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
|
40
|
+
else
|
41
|
+
variation_counter.count = variation_counter.count + 1
|
42
|
+
end
|
43
|
+
time = event.timestamp
|
44
|
+
if !time.nil?
|
45
|
+
@start_date = time if @start_date == 0 || time < @start_date
|
46
|
+
@end_date = time if time > @end_date
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a snapshot of the current summarized event data, and resets this state.
|
51
|
+
def snapshot
|
52
|
+
ret = EventSummary.new(@start_date, @end_date, @counters)
|
53
|
+
ret
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
@start_date = 0
|
58
|
+
@end_date = 0
|
59
|
+
@counters = {}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
class Event
|
4
|
+
def initialize(timestamp, user)
|
5
|
+
@timestamp = timestamp
|
6
|
+
@user = user
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :timestamp
|
10
|
+
attr_reader :kind
|
11
|
+
attr_reader :user
|
12
|
+
end
|
13
|
+
|
14
|
+
class EvalEvent < Event
|
15
|
+
def initialize(timestamp, user, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
|
16
|
+
track_events = false, debug_until = nil, prereq_of = nil)
|
17
|
+
super(timestamp, user)
|
18
|
+
@key = key
|
19
|
+
@version = version
|
20
|
+
@variation = variation
|
21
|
+
@value = value
|
22
|
+
@reason = reason
|
23
|
+
@default = default
|
24
|
+
# avoid setting rarely-used attributes if they have no value - this saves a little space per instance
|
25
|
+
@track_events = track_events if track_events
|
26
|
+
@debug_until = debug_until if debug_until
|
27
|
+
@prereq_of = prereq_of if prereq_of
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :key
|
31
|
+
attr_reader :version
|
32
|
+
attr_reader :variation
|
33
|
+
attr_reader :value
|
34
|
+
attr_reader :reason
|
35
|
+
attr_reader :default
|
36
|
+
attr_reader :track_events
|
37
|
+
attr_reader :debug_until
|
38
|
+
attr_reader :prereq_of
|
39
|
+
end
|
40
|
+
|
41
|
+
class IdentifyEvent < Event
|
42
|
+
def initialize(timestamp, user)
|
43
|
+
super(timestamp, user)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class CustomEvent < Event
|
48
|
+
def initialize(timestamp, user, key, data = nil, metric_value = nil)
|
49
|
+
super(timestamp, user)
|
50
|
+
@key = key
|
51
|
+
@data = data if !data.nil?
|
52
|
+
@metric_value = metric_value if !metric_value.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :key
|
56
|
+
attr_reader :data
|
57
|
+
attr_reader :metric_value
|
58
|
+
end
|
59
|
+
|
60
|
+
class AliasEvent < Event
|
61
|
+
def initialize(timestamp, key, context_kind, previous_key, previous_context_kind)
|
62
|
+
super(timestamp, nil)
|
63
|
+
@key = key
|
64
|
+
@context_kind = context_kind
|
65
|
+
@previous_key = previous_key
|
66
|
+
@previous_context_kind = previous_context_kind
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_reader :key
|
70
|
+
attr_reader :context_kind
|
71
|
+
attr_reader :previous_key
|
72
|
+
attr_reader :previous_context_kind
|
73
|
+
end
|
74
|
+
|
75
|
+
class IndexEvent < Event
|
76
|
+
def initialize(timestamp, user)
|
77
|
+
super(timestamp, user)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class DebugEvent < Event
|
82
|
+
def initialize(eval_event)
|
83
|
+
super(eval_event.timestamp, eval_event.user)
|
84
|
+
@eval_event = eval_event
|
85
|
+
end
|
86
|
+
|
87
|
+
attr_reader :eval_event
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -35,7 +35,7 @@ module LaunchDarkly
|
|
35
35
|
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
36
36
|
end
|
37
37
|
|
38
|
-
@logger.info("
|
38
|
+
@logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
|
39
39
|
end
|
40
40
|
|
41
41
|
def stop
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require "ldclient-rb/impl/evaluator_helpers"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module DataModelPreprocessing
|
6
|
+
#
|
7
|
+
# Container for a precomputed result that includes a specific variation index and value, an
|
8
|
+
# evaluation reason, and optionally an alternate evaluation reason that corresponds to the
|
9
|
+
# "in experiment" state.
|
10
|
+
#
|
11
|
+
class EvalResultsForSingleVariation
|
12
|
+
def initialize(value, variation_index, regular_reason, in_experiment_reason = nil)
|
13
|
+
@regular_result = EvaluationDetail.new(value, variation_index, regular_reason)
|
14
|
+
@in_experiment_result = in_experiment_reason ?
|
15
|
+
EvaluationDetail.new(value, variation_index, in_experiment_reason) :
|
16
|
+
@regular_result
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param in_experiment [Boolean] indicates whether we want the result to include
|
20
|
+
# "inExperiment: true" in the reason or not
|
21
|
+
# @return [EvaluationDetail]
|
22
|
+
def get_result(in_experiment = false)
|
23
|
+
in_experiment ? @in_experiment_result : @regular_result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Container for a set of precomputed results, one for each possible flag variation.
|
29
|
+
#
|
30
|
+
class EvalResultFactoryMultiVariations
|
31
|
+
def initialize(variation_factories)
|
32
|
+
@factories = variation_factories
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param index [Integer] the variation index
|
36
|
+
# @param in_experiment [Boolean] indicates whether we want the result to include
|
37
|
+
# "inExperiment: true" in the reason or not
|
38
|
+
def for_variation(index, in_experiment)
|
39
|
+
if index < 0 || index >= @factories.length
|
40
|
+
EvaluationDetail.new(nil, nil, EvaluationReason.error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
41
|
+
else
|
42
|
+
@factories[index].get_result(in_experiment)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Base class for all of the preprocessed data classes we embed in our data model. Using this class
|
48
|
+
# ensures that none of its properties will be included in JSON representations. It also overrides
|
49
|
+
# == to say that it is always equal with another instance of the same class; equality tests on
|
50
|
+
# this class are only ever done in test code, and we want the contents of these classes to be
|
51
|
+
# ignored in test code unless we are looking at specific attributes.
|
52
|
+
class PreprocessedDataBase
|
53
|
+
def as_json(*)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_json(*a)
|
58
|
+
"null"
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
other.class == self.class
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class FlagPreprocessed < PreprocessedDataBase
|
67
|
+
def initialize(off_result, fallthrough_factory)
|
68
|
+
@off_result = off_result
|
69
|
+
@fallthrough_factory = fallthrough_factory
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [EvalResultsForSingleVariation]
|
73
|
+
attr_reader :off_result
|
74
|
+
# @return [EvalResultFactoryMultiVariations]
|
75
|
+
attr_reader :fallthrough_factory
|
76
|
+
end
|
77
|
+
|
78
|
+
class PrerequisitePreprocessed < PreprocessedDataBase
|
79
|
+
def initialize(failed_result)
|
80
|
+
@failed_result = failed_result
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [EvalResultsForSingleVariation]
|
84
|
+
attr_reader :failed_result
|
85
|
+
end
|
86
|
+
|
87
|
+
class TargetPreprocessed < PreprocessedDataBase
|
88
|
+
def initialize(match_result)
|
89
|
+
@match_result = match_result
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [EvalResultsForSingleVariation]
|
93
|
+
attr_reader :match_result
|
94
|
+
end
|
95
|
+
|
96
|
+
class FlagRulePreprocessed < PreprocessedDataBase
|
97
|
+
def initialize(all_match_results)
|
98
|
+
@all_match_results = all_match_results
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [EvalResultsForSingleVariation]
|
102
|
+
attr_reader :all_match_results
|
103
|
+
end
|
104
|
+
|
105
|
+
class Preprocessor
|
106
|
+
def initialize(logger = nil)
|
107
|
+
@logger = logger
|
108
|
+
end
|
109
|
+
|
110
|
+
def preprocess_item!(kind, item)
|
111
|
+
if kind.eql? FEATURES
|
112
|
+
preprocess_flag!(item)
|
113
|
+
elsif kind.eql? SEGMENTS
|
114
|
+
preprocess_segment!(item)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def preprocess_all_items!(kind, items_map)
|
119
|
+
return items_map if !items_map
|
120
|
+
items_map.each do |key, item|
|
121
|
+
preprocess_item!(kind, item)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def preprocess_flag!(flag)
|
126
|
+
flag[:_preprocessed] = FlagPreprocessed.new(
|
127
|
+
EvaluatorHelpers.off_result(flag),
|
128
|
+
precompute_multi_variation_results(flag, EvaluationReason::fallthrough(false), EvaluationReason::fallthrough(true))
|
129
|
+
)
|
130
|
+
(flag[:prerequisites] || []).each do |prereq|
|
131
|
+
preprocess_prerequisite!(prereq, flag)
|
132
|
+
end
|
133
|
+
(flag[:targets] || []).each do |target|
|
134
|
+
preprocess_target!(target, flag)
|
135
|
+
end
|
136
|
+
rules = flag[:rules]
|
137
|
+
(rules || []).each_index do |index|
|
138
|
+
preprocess_flag_rule!(rules[index], index, flag)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def preprocess_segment!(segment)
|
143
|
+
# nothing to do for segments currently
|
144
|
+
end
|
145
|
+
|
146
|
+
private def preprocess_prerequisite!(prereq, flag)
|
147
|
+
prereq[:_preprocessed] = PrerequisitePreprocessed.new(
|
148
|
+
EvaluatorHelpers.prerequisite_failed_result(prereq, flag, @logger)
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
private def preprocess_target!(target, flag)
|
153
|
+
target[:_preprocessed] = TargetPreprocessed.new(
|
154
|
+
EvaluatorHelpers.target_match_result(target, flag, @logger)
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
private def preprocess_flag_rule!(rule, index, flag)
|
159
|
+
match_reason = EvaluationReason::rule_match(index, rule[:id])
|
160
|
+
match_reason_in_experiment = EvaluationReason::rule_match(index, rule[:id], true)
|
161
|
+
rule[:_preprocessed] = FlagRulePreprocessed.new(
|
162
|
+
precompute_multi_variation_results(flag, match_reason, match_reason_in_experiment)
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
private def precompute_multi_variation_results(flag, regular_reason, in_experiment_reason)
|
167
|
+
factories = []
|
168
|
+
vars = flag[:variations] || []
|
169
|
+
vars.each_index do |index|
|
170
|
+
factories << EvalResultsForSingleVariation.new(vars[index], index, regular_reason, in_experiment_reason)
|
171
|
+
end
|
172
|
+
EvalResultFactoryMultiVariations.new(factories)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -1,13 +1,14 @@
|
|
1
|
+
require "ldclient-rb/impl/model/preprocessed_data"
|
1
2
|
|
2
3
|
module LaunchDarkly
|
3
4
|
module Impl
|
4
5
|
module Model
|
5
6
|
# Abstraction of deserializing a feature flag or segment that was read from a data store or
|
6
7
|
# received from LaunchDarkly.
|
7
|
-
def self.deserialize(kind, json)
|
8
|
+
def self.deserialize(kind, json, logger = nil)
|
8
9
|
return nil if json.nil?
|
9
10
|
item = JSON.parse(json, symbolize_names: true)
|
10
|
-
|
11
|
+
DataModelPreprocessing::Preprocessor.new(logger).preprocess_item!(kind, item)
|
11
12
|
item
|
12
13
|
end
|
13
14
|
|
@@ -18,45 +19,14 @@ module LaunchDarkly
|
|
18
19
|
end
|
19
20
|
|
20
21
|
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
21
|
-
def self.make_all_store_data(received_data)
|
22
|
+
def self.make_all_store_data(received_data, logger = nil)
|
23
|
+
preprocessor = DataModelPreprocessing::Preprocessor.new(logger)
|
22
24
|
flags = received_data[:flags]
|
23
|
-
|
25
|
+
preprocessor.preprocess_all_items!(FEATURES, flags)
|
24
26
|
segments = received_data[:segments]
|
25
|
-
|
27
|
+
preprocessor.preprocess_all_items!(SEGMENTS, segments)
|
26
28
|
{ FEATURES => flags, SEGMENTS => segments }
|
27
29
|
end
|
28
|
-
|
29
|
-
# Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
|
30
|
-
# or read it from a persistent data store). This allows us to precompute some derived attributes that
|
31
|
-
# will never change during the lifetime of that item.
|
32
|
-
def self.postprocess_item_after_deserializing!(kind, item)
|
33
|
-
return if !item
|
34
|
-
# Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
|
35
|
-
# to the "kind" object or the item class.
|
36
|
-
if kind.eql? FEATURES
|
37
|
-
# For feature flags, we precompute all possible parameterized EvaluationReason instances.
|
38
|
-
prereqs = item[:prerequisites]
|
39
|
-
if !prereqs.nil?
|
40
|
-
prereqs.each do |prereq|
|
41
|
-
prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
|
42
|
-
end
|
43
|
-
end
|
44
|
-
rules = item[:rules]
|
45
|
-
if !rules.nil?
|
46
|
-
rules.each_index do |i|
|
47
|
-
rule = rules[i]
|
48
|
-
rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.postprocess_items_after_deserializing!(kind, items_map)
|
55
|
-
return items_map if !items_map
|
56
|
-
items_map.each do |key, item|
|
57
|
-
postprocess_item_after_deserializing!(kind, item)
|
58
|
-
end
|
59
|
-
end
|
60
30
|
end
|
61
31
|
end
|
62
32
|
end
|