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
@@ -0,0 +1,255 @@
|
|
1
|
+
require "ldclient-rb/impl/evaluator_helpers"
|
2
|
+
require "ldclient-rb/impl/model/clause"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
# See serialization.rb for implementation notes on the data model classes.
|
6
|
+
|
7
|
+
def check_variation_range(flag, errors_out, variation, description)
|
8
|
+
unless flag.nil? || errors_out.nil? || variation.nil?
|
9
|
+
if variation < 0 || variation >= flag.variations.length
|
10
|
+
errors_out << "#{description} has invalid variation index"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module LaunchDarkly
|
16
|
+
module Impl
|
17
|
+
module Model
|
18
|
+
class FeatureFlag
|
19
|
+
# @param data [Hash]
|
20
|
+
# @param logger [Logger|nil]
|
21
|
+
def initialize(data, logger = nil)
|
22
|
+
raise ArgumentError, "expected hash but got #{data.class}" unless data.is_a?(Hash)
|
23
|
+
errors = []
|
24
|
+
@data = data
|
25
|
+
@key = data[:key]
|
26
|
+
@version = data[:version]
|
27
|
+
@deleted = !!data[:deleted]
|
28
|
+
return if @deleted
|
29
|
+
migration_settings = data[:migration] || {}
|
30
|
+
@migration_settings = MigrationSettings.new(migration_settings[:checkRatio])
|
31
|
+
@sampling_ratio = data[:samplingRatio]
|
32
|
+
@exclude_from_summaries = !!data[:excludeFromSummaries]
|
33
|
+
@variations = data[:variations] || []
|
34
|
+
@on = !!data[:on]
|
35
|
+
fallthrough = data[:fallthrough] || {}
|
36
|
+
@fallthrough = VariationOrRollout.new(fallthrough[:variation], fallthrough[:rollout], self, errors, "fallthrough")
|
37
|
+
@off_variation = data[:offVariation]
|
38
|
+
check_variation_range(self, errors, @off_variation, "off variation")
|
39
|
+
@prerequisites = (data[:prerequisites] || []).map do |prereq_data|
|
40
|
+
Prerequisite.new(prereq_data, self, errors)
|
41
|
+
end
|
42
|
+
@targets = (data[:targets] || []).map do |target_data|
|
43
|
+
Target.new(target_data, self, errors)
|
44
|
+
end
|
45
|
+
@context_targets = (data[:contextTargets] || []).map do |target_data|
|
46
|
+
Target.new(target_data, self, errors)
|
47
|
+
end
|
48
|
+
@rules = (data[:rules] || []).map.with_index do |rule_data, index|
|
49
|
+
FlagRule.new(rule_data, index, self, errors)
|
50
|
+
end
|
51
|
+
@salt = data[:salt]
|
52
|
+
@off_result = EvaluatorHelpers.evaluation_detail_for_off_variation(self, EvaluationReason::off)
|
53
|
+
@fallthrough_results = Preprocessor.precompute_multi_variation_results(self,
|
54
|
+
EvaluationReason::fallthrough(false), EvaluationReason::fallthrough(true))
|
55
|
+
unless logger.nil?
|
56
|
+
errors.each do |message|
|
57
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{@key}\": #{message}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Hash]
|
63
|
+
attr_reader :data
|
64
|
+
# @return [String]
|
65
|
+
attr_reader :key
|
66
|
+
# @return [Integer]
|
67
|
+
attr_reader :version
|
68
|
+
# @return [Boolean]
|
69
|
+
attr_reader :deleted
|
70
|
+
# @return [MigrationSettings, nil]
|
71
|
+
attr_reader :migration_settings
|
72
|
+
# @return [Integer, nil]
|
73
|
+
attr_reader :sampling_ratio
|
74
|
+
# @return [Boolean, nil]
|
75
|
+
attr_reader :exclude_from_summaries
|
76
|
+
# @return [Array]
|
77
|
+
attr_reader :variations
|
78
|
+
# @return [Boolean]
|
79
|
+
attr_reader :on
|
80
|
+
# @return [Integer|nil]
|
81
|
+
attr_reader :off_variation
|
82
|
+
# @return [LaunchDarkly::Impl::Model::VariationOrRollout]
|
83
|
+
attr_reader :fallthrough
|
84
|
+
# @return [LaunchDarkly::EvaluationDetail]
|
85
|
+
attr_reader :off_result
|
86
|
+
# @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
|
87
|
+
attr_reader :fallthrough_results
|
88
|
+
# @return [Array<LaunchDarkly::Impl::Model::Prerequisite>]
|
89
|
+
attr_reader :prerequisites
|
90
|
+
# @return [Array<LaunchDarkly::Impl::Model::Target>]
|
91
|
+
attr_reader :targets
|
92
|
+
# @return [Array<LaunchDarkly::Impl::Model::Target>]
|
93
|
+
attr_reader :context_targets
|
94
|
+
# @return [Array<LaunchDarkly::Impl::Model::FlagRule>]
|
95
|
+
attr_reader :rules
|
96
|
+
# @return [String]
|
97
|
+
attr_reader :salt
|
98
|
+
|
99
|
+
# This method allows us to read properties of the object as if it's just a hash. Currently this is
|
100
|
+
# necessary because some data store logic is still written to expect hashes; we can remove it once
|
101
|
+
# we migrate entirely to using attributes of the class.
|
102
|
+
def [](key)
|
103
|
+
@data[key]
|
104
|
+
end
|
105
|
+
|
106
|
+
def ==(other)
|
107
|
+
other.is_a?(FeatureFlag) && other.data == self.data
|
108
|
+
end
|
109
|
+
|
110
|
+
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
|
111
|
+
@data
|
112
|
+
end
|
113
|
+
|
114
|
+
# Same as as_json, but converts the JSON structure into a string.
|
115
|
+
def to_json(*a)
|
116
|
+
as_json.to_json(*a)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Prerequisite
|
121
|
+
def initialize(data, flag, errors_out = nil)
|
122
|
+
@data = data
|
123
|
+
@key = data[:key]
|
124
|
+
@variation = data[:variation]
|
125
|
+
@failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
|
126
|
+
EvaluationReason::prerequisite_failed(@key))
|
127
|
+
check_variation_range(flag, errors_out, @variation, "prerequisite")
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Hash]
|
131
|
+
attr_reader :data
|
132
|
+
# @return [String]
|
133
|
+
attr_reader :key
|
134
|
+
# @return [Integer]
|
135
|
+
attr_reader :variation
|
136
|
+
# @return [LaunchDarkly::EvaluationDetail]
|
137
|
+
attr_reader :failure_result
|
138
|
+
end
|
139
|
+
|
140
|
+
class Target
|
141
|
+
def initialize(data, flag, errors_out = nil)
|
142
|
+
@kind = data[:contextKind] || LDContext::KIND_DEFAULT
|
143
|
+
@data = data
|
144
|
+
@values = Set.new(data[:values] || [])
|
145
|
+
@variation = data[:variation]
|
146
|
+
@match_result = EvaluatorHelpers.evaluation_detail_for_variation(flag,
|
147
|
+
data[:variation], EvaluationReason::target_match)
|
148
|
+
check_variation_range(flag, errors_out, @variation, "target")
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [String]
|
152
|
+
attr_reader :kind
|
153
|
+
# @return [Hash]
|
154
|
+
attr_reader :data
|
155
|
+
# @return [Set]
|
156
|
+
attr_reader :values
|
157
|
+
# @return [Integer]
|
158
|
+
attr_reader :variation
|
159
|
+
# @return [LaunchDarkly::EvaluationDetail]
|
160
|
+
attr_reader :match_result
|
161
|
+
end
|
162
|
+
|
163
|
+
class FlagRule
|
164
|
+
def initialize(data, rule_index, flag, errors_out = nil)
|
165
|
+
@data = data
|
166
|
+
@clauses = (data[:clauses] || []).map do |clause_data|
|
167
|
+
Clause.new(clause_data, errors_out)
|
168
|
+
end
|
169
|
+
@variation_or_rollout = VariationOrRollout.new(data[:variation], data[:rollout], flag, errors_out, 'rule')
|
170
|
+
rule_id = data[:id]
|
171
|
+
match_reason = EvaluationReason::rule_match(rule_index, rule_id)
|
172
|
+
match_reason_in_experiment = EvaluationReason::rule_match(rule_index, rule_id, true)
|
173
|
+
@match_results = Preprocessor.precompute_multi_variation_results(flag, match_reason, match_reason_in_experiment)
|
174
|
+
end
|
175
|
+
|
176
|
+
# @return [Hash]
|
177
|
+
attr_reader :data
|
178
|
+
# @return [Array<LaunchDarkly::Impl::Model::Clause>]
|
179
|
+
attr_reader :clauses
|
180
|
+
# @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
|
181
|
+
attr_reader :match_results
|
182
|
+
# @return [LaunchDarkly::Impl::Model::VariationOrRollout]
|
183
|
+
attr_reader :variation_or_rollout
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
class MigrationSettings
|
188
|
+
#
|
189
|
+
# @param check_ratio [Int, nil]
|
190
|
+
#
|
191
|
+
def initialize(check_ratio)
|
192
|
+
@check_ratio = check_ratio
|
193
|
+
end
|
194
|
+
|
195
|
+
# @return [Integer, nil]
|
196
|
+
attr_reader :check_ratio
|
197
|
+
end
|
198
|
+
|
199
|
+
class VariationOrRollout
|
200
|
+
def initialize(variation, rollout_data, flag = nil, errors_out = nil, description = nil)
|
201
|
+
@variation = variation
|
202
|
+
check_variation_range(flag, errors_out, variation, description)
|
203
|
+
@rollout = rollout_data.nil? ? nil : Rollout.new(rollout_data, flag, errors_out, description)
|
204
|
+
end
|
205
|
+
|
206
|
+
# @return [Integer|nil]
|
207
|
+
attr_reader :variation
|
208
|
+
# @return [Rollout|nil] currently we do not have a model class for the rollout
|
209
|
+
attr_reader :rollout
|
210
|
+
end
|
211
|
+
|
212
|
+
class Rollout
|
213
|
+
def initialize(data, flag = nil, errors_out = nil, description = nil)
|
214
|
+
@context_kind = data[:contextKind]
|
215
|
+
@variations = (data[:variations] || []).map { |v| WeightedVariation.new(v, flag, errors_out, description) }
|
216
|
+
@bucket_by = data[:bucketBy]
|
217
|
+
@kind = data[:kind]
|
218
|
+
@is_experiment = @kind == "experiment"
|
219
|
+
@seed = data[:seed]
|
220
|
+
end
|
221
|
+
|
222
|
+
# @return [String|nil]
|
223
|
+
attr_reader :context_kind
|
224
|
+
# @return [Array<WeightedVariation>]
|
225
|
+
attr_reader :variations
|
226
|
+
# @return [String|nil]
|
227
|
+
attr_reader :bucket_by
|
228
|
+
# @return [String|nil]
|
229
|
+
attr_reader :kind
|
230
|
+
# @return [Boolean]
|
231
|
+
attr_reader :is_experiment
|
232
|
+
# @return [Integer|nil]
|
233
|
+
attr_reader :seed
|
234
|
+
end
|
235
|
+
|
236
|
+
class WeightedVariation
|
237
|
+
def initialize(data, flag = nil, errors_out = nil, description = nil)
|
238
|
+
@variation = data[:variation]
|
239
|
+
@weight = data[:weight]
|
240
|
+
@untracked = !!data[:untracked]
|
241
|
+
check_variation_range(flag, errors_out, @variation, description)
|
242
|
+
end
|
243
|
+
|
244
|
+
# @return [Integer]
|
245
|
+
attr_reader :variation
|
246
|
+
# @return [Integer]
|
247
|
+
attr_reader :weight
|
248
|
+
# @return [Boolean]
|
249
|
+
attr_reader :untracked
|
250
|
+
end
|
251
|
+
|
252
|
+
# Clause is defined in its own file because clauses are used by both flags and segments
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "ldclient-rb/impl/evaluator_helpers"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module Model
|
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 [LaunchDarkly::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
|
+
# @return [LaunchDarkly::EvaluationDetail]
|
39
|
+
def for_variation(index, in_experiment)
|
40
|
+
if index < 0 || index >= @factories.length
|
41
|
+
EvaluationDetail.new(nil, nil, EvaluationReason.error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
42
|
+
else
|
43
|
+
@factories[index].get_result(in_experiment)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Preprocessor
|
49
|
+
# @param flag [LaunchDarkly::Impl::Model::FeatureFlag]
|
50
|
+
# @param regular_reason [LaunchDarkly::EvaluationReason]
|
51
|
+
# @param in_experiment_reason [LaunchDarkly::EvaluationReason]
|
52
|
+
# @return [EvalResultFactoryMultiVariations]
|
53
|
+
def self.precompute_multi_variation_results(flag, regular_reason, in_experiment_reason)
|
54
|
+
factories = []
|
55
|
+
vars = flag[:variations] || []
|
56
|
+
vars.each_index do |index|
|
57
|
+
factories << EvalResultsForSingleVariation.new(vars[index], index, regular_reason, in_experiment_reason)
|
58
|
+
end
|
59
|
+
EvalResultFactoryMultiVariations.new(factories)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require "ldclient-rb/impl/model/clause"
|
2
|
+
require "ldclient-rb/impl/model/preprocessed_data"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
# See serialization.rb for implementation notes on the data model classes.
|
6
|
+
|
7
|
+
module LaunchDarkly
|
8
|
+
module Impl
|
9
|
+
module Model
|
10
|
+
class Segment
|
11
|
+
# @param data [Hash]
|
12
|
+
# @param logger [Logger|nil]
|
13
|
+
def initialize(data, logger = nil)
|
14
|
+
raise ArgumentError, "expected hash but got #{data.class}" unless data.is_a?(Hash)
|
15
|
+
errors = []
|
16
|
+
@data = data
|
17
|
+
@key = data[:key]
|
18
|
+
@version = data[:version]
|
19
|
+
@deleted = !!data[:deleted]
|
20
|
+
return if @deleted
|
21
|
+
@included = data[:included] || []
|
22
|
+
@excluded = data[:excluded] || []
|
23
|
+
@included_contexts = (data[:includedContexts] || []).map do |target_data|
|
24
|
+
SegmentTarget.new(target_data)
|
25
|
+
end
|
26
|
+
@excluded_contexts = (data[:excludedContexts] || []).map do |target_data|
|
27
|
+
SegmentTarget.new(target_data)
|
28
|
+
end
|
29
|
+
@rules = (data[:rules] || []).map do |rule_data|
|
30
|
+
SegmentRule.new(rule_data, errors)
|
31
|
+
end
|
32
|
+
@unbounded = !!data[:unbounded]
|
33
|
+
@unbounded_context_kind = data[:unboundedContextKind] || LDContext::KIND_DEFAULT
|
34
|
+
@generation = data[:generation]
|
35
|
+
@salt = data[:salt]
|
36
|
+
unless logger.nil?
|
37
|
+
errors.each do |message|
|
38
|
+
logger.error("[LDClient] Data inconsistency in segment \"#{@key}\": #{message}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash]
|
44
|
+
attr_reader :data
|
45
|
+
# @return [String]
|
46
|
+
attr_reader :key
|
47
|
+
# @return [Integer]
|
48
|
+
attr_reader :version
|
49
|
+
# @return [Boolean]
|
50
|
+
attr_reader :deleted
|
51
|
+
# @return [Array<String>]
|
52
|
+
attr_reader :included
|
53
|
+
# @return [Array<String>]
|
54
|
+
attr_reader :excluded
|
55
|
+
# @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
|
56
|
+
attr_reader :included_contexts
|
57
|
+
# @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
|
58
|
+
attr_reader :excluded_contexts
|
59
|
+
# @return [Array<SegmentRule>]
|
60
|
+
attr_reader :rules
|
61
|
+
# @return [Boolean]
|
62
|
+
attr_reader :unbounded
|
63
|
+
# @return [String]
|
64
|
+
attr_reader :unbounded_context_kind
|
65
|
+
# @return [Integer|nil]
|
66
|
+
attr_reader :generation
|
67
|
+
# @return [String]
|
68
|
+
attr_reader :salt
|
69
|
+
|
70
|
+
# This method allows us to read properties of the object as if it's just a hash. Currently this is
|
71
|
+
# necessary because some data store logic is still written to expect hashes; we can remove it once
|
72
|
+
# we migrate entirely to using attributes of the class.
|
73
|
+
def [](key)
|
74
|
+
@data[key]
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==(other)
|
78
|
+
other.is_a?(Segment) && other.data == self.data
|
79
|
+
end
|
80
|
+
|
81
|
+
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
|
82
|
+
@data
|
83
|
+
end
|
84
|
+
|
85
|
+
# Same as as_json, but converts the JSON structure into a string.
|
86
|
+
def to_json(*a)
|
87
|
+
as_json.to_json(*a)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class SegmentTarget
|
92
|
+
def initialize(data)
|
93
|
+
@data = data
|
94
|
+
@context_kind = data[:contextKind]
|
95
|
+
@values = Set.new(data[:values] || [])
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Hash]
|
99
|
+
attr_reader :data
|
100
|
+
# @return [String]
|
101
|
+
attr_reader :context_kind
|
102
|
+
# @return [Set]
|
103
|
+
attr_reader :values
|
104
|
+
end
|
105
|
+
|
106
|
+
class SegmentRule
|
107
|
+
def initialize(data, errors_out = nil)
|
108
|
+
@data = data
|
109
|
+
@clauses = (data[:clauses] || []).map do |clause_data|
|
110
|
+
Clause.new(clause_data, errors_out)
|
111
|
+
end
|
112
|
+
@weight = data[:weight]
|
113
|
+
@bucket_by = data[:bucketBy]
|
114
|
+
@rollout_context_kind = data[:rolloutContextKind]
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Hash]
|
118
|
+
attr_reader :data
|
119
|
+
# @return [Array<LaunchDarkly::Impl::Model::Clause>]
|
120
|
+
attr_reader :clauses
|
121
|
+
# @return [Integer|nil]
|
122
|
+
attr_reader :weight
|
123
|
+
# @return [String|nil]
|
124
|
+
attr_reader :bucket_by
|
125
|
+
# @return [String|nil]
|
126
|
+
attr_reader :rollout_context_kind
|
127
|
+
end
|
128
|
+
|
129
|
+
# Clause is defined in its own file because clauses are used by both flags and segments
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -1,61 +1,71 @@
|
|
1
|
+
require "ldclient-rb/impl/model/feature_flag"
|
2
|
+
require "ldclient-rb/impl/model/preprocessed_data"
|
3
|
+
require "ldclient-rb/impl/model/segment"
|
4
|
+
|
5
|
+
# General implementation notes about the data model classes in LaunchDarkly::Impl::Model--
|
6
|
+
#
|
7
|
+
# As soon as we receive flag/segment JSON data from LaunchDarkly (or, read it from a database), we
|
8
|
+
# transform it into the model classes FeatureFlag, Segment, etc. The constructor of each of these
|
9
|
+
# classes takes a hash (the parsed JSON), and transforms it into an internal representation that
|
10
|
+
# is more efficient for evaluations.
|
11
|
+
#
|
12
|
+
# Validation works as follows:
|
13
|
+
# - A property value that is of the correct type, but is invalid for other reasons (for example,
|
14
|
+
# if a flag rule refers to variation index 5, but there are only 2 variations in the flag), does
|
15
|
+
# not prevent the flag from being parsed and stored. It does cause a warning to be logged, if a
|
16
|
+
# logger was passed to the constructor.
|
17
|
+
# - If a value is completely invalid for the schema, the constructor may throw an
|
18
|
+
# exception, causing the whole data set to be rejected. This is consistent with the behavior of
|
19
|
+
# the strongly-typed SDKs.
|
20
|
+
#
|
21
|
+
# Currently, the model classes also retain the original hash of the parsed JSON. This is because
|
22
|
+
# we may need to re-serialize them to JSON, and building the JSON on the fly would be very
|
23
|
+
# inefficient, so each model class has a to_json method that just returns the same Hash. If we
|
24
|
+
# are able in the future to either use a custom streaming serializer, or pass the JSON data
|
25
|
+
# straight through from LaunchDarkly to a database instead of re-serializing, we could stop
|
26
|
+
# retaining this data.
|
1
27
|
|
2
28
|
module LaunchDarkly
|
3
29
|
module Impl
|
4
30
|
module Model
|
5
31
|
# Abstraction of deserializing a feature flag or segment that was read from a data store or
|
6
32
|
# received from LaunchDarkly.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
33
|
+
#
|
34
|
+
# SDK code outside of Impl::Model should use this method instead of calling the model class
|
35
|
+
# constructors directly, so as not to rely on implementation details.
|
36
|
+
#
|
37
|
+
# @param kind [Hash] normally either FEATURES or SEGMENTS
|
38
|
+
# @param input [object] a JSON string or a parsed hash (or a data model object, in which case
|
39
|
+
# we'll just return the original object)
|
40
|
+
# @param logger [Logger|nil] logs errors if there are any data validation problems
|
41
|
+
# @return [Object] the flag or segment (or, for an unknown data kind, the data as a hash)
|
42
|
+
def self.deserialize(kind, input, logger = nil)
|
43
|
+
return nil if input.nil?
|
44
|
+
return input if !input.is_a?(String) && !input.is_a?(Hash)
|
45
|
+
data = input.is_a?(Hash) ? input : JSON.parse(input, symbolize_names: true)
|
46
|
+
case kind
|
47
|
+
when FEATURES
|
48
|
+
FeatureFlag.new(data, logger)
|
49
|
+
when SEGMENTS
|
50
|
+
Segment.new(data, logger)
|
51
|
+
else
|
52
|
+
data
|
53
|
+
end
|
12
54
|
end
|
13
55
|
|
14
56
|
# Abstraction of serializing a feature flag or segment that will be written to a data store.
|
15
|
-
# Currently we just call to_json
|
57
|
+
# Currently we just call to_json, but SDK code outside of Impl::Model should use this method
|
58
|
+
# instead of to_json, so as not to rely on implementation details.
|
16
59
|
def self.serialize(kind, item)
|
17
60
|
item.to_json
|
18
61
|
end
|
19
62
|
|
20
63
|
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
21
|
-
def self.make_all_store_data(received_data)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
{ FEATURES => flags, SEGMENTS => segments }
|
27
|
-
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
|
64
|
+
def self.make_all_store_data(received_data, logger = nil)
|
65
|
+
{
|
66
|
+
FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) },
|
67
|
+
SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) },
|
68
|
+
}
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
@@ -16,10 +16,9 @@ module LaunchDarkly
|
|
16
16
|
|
17
17
|
def start
|
18
18
|
@worker = Thread.new do
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
while !@stopped.value do
|
19
|
+
sleep(@start_delay) unless @start_delay.nil? || @start_delay == 0
|
20
|
+
|
21
|
+
until @stopped.value do
|
23
22
|
started_at = Time.now
|
24
23
|
begin
|
25
24
|
@task.call
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
class Sampler
|
4
|
+
#
|
5
|
+
# @param random [Random]
|
6
|
+
#
|
7
|
+
def initialize(random)
|
8
|
+
@random = random
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param ratio [Int]
|
13
|
+
#
|
14
|
+
# @return [Boolean]
|
15
|
+
#
|
16
|
+
def sample(ratio)
|
17
|
+
return false unless ratio.is_a? Integer
|
18
|
+
return false if ratio <= 0
|
19
|
+
return true if ratio == 1
|
20
|
+
|
21
|
+
@random.rand(1.0) < 1.0 / ratio
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|