launchdarkly-server-sdk 6.3.0 → 8.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 +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
|