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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. 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
- def self.deserialize(kind, json)
8
- return nil if json.nil?
9
- item = JSON.parse(json, symbolize_names: true)
10
- postprocess_item_after_deserializing!(kind, item)
11
- item
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
- flags = received_data[:flags]
23
- postprocess_items_after_deserializing!(FEATURES, flags)
24
- segments = received_data[:segments]
25
- postprocess_items_after_deserializing!(SEGMENTS, segments)
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
- if @start_delay
20
- sleep(@start_delay)
21
- end
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