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.
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