launchdarkly-server-sdk 6.2.5 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/lib/ldclient-rb/config.rb +203 -43
  4. data/lib/ldclient-rb/context.rb +487 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +85 -26
  6. data/lib/ldclient-rb/events.rb +185 -146
  7. data/lib/ldclient-rb/flags_state.rb +25 -14
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/context.rb +96 -0
  10. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  11. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  12. data/lib/ldclient-rb/impl/evaluator.rb +428 -132
  13. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  14. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  15. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  16. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  17. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  18. data/lib/ldclient-rb/impl/event_types.rb +78 -0
  19. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  20. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +92 -28
  21. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  22. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +165 -32
  23. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  24. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  25. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  26. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  27. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  28. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  29. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  30. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  32. data/lib/ldclient-rb/impl/util.rb +62 -1
  33. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  34. data/lib/ldclient-rb/integrations/consul.rb +9 -2
  35. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  36. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +43 -3
  38. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +594 -0
  39. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  40. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +14 -9
  41. data/lib/ldclient-rb/integrations.rb +2 -51
  42. data/lib/ldclient-rb/interfaces.rb +151 -1
  43. data/lib/ldclient-rb/ldclient.rb +175 -133
  44. data/lib/ldclient-rb/memoized_value.rb +1 -1
  45. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  46. data/lib/ldclient-rb/polling.rb +22 -41
  47. data/lib/ldclient-rb/reference.rb +274 -0
  48. data/lib/ldclient-rb/requestor.rb +7 -7
  49. data/lib/ldclient-rb/stream.rb +9 -9
  50. data/lib/ldclient-rb/util.rb +11 -17
  51. data/lib/ldclient-rb/version.rb +1 -1
  52. data/lib/ldclient-rb.rb +2 -4
  53. metadata +49 -23
  54. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  55. data/lib/ldclient-rb/file_data_source.rb +0 -314
  56. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  57. data/lib/ldclient-rb/newrelic.rb +0 -17
  58. data/lib/ldclient-rb/redis_store.rb +0 -88
  59. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -0,0 +1,40 @@
1
+ require 'concurrent/atomics'
2
+ require 'ldclient-rb/interfaces'
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ module Integrations
7
+ module TestData
8
+ # @private
9
+ class TestDataSource
10
+ include LaunchDarkly::Interfaces::DataSource
11
+
12
+ def initialize(feature_store, test_data)
13
+ @feature_store = feature_store
14
+ @test_data = test_data
15
+ end
16
+
17
+ def initialized?
18
+ true
19
+ end
20
+
21
+ def start
22
+ ready = Concurrent::Event.new
23
+ ready.set
24
+ init_data = @test_data.make_init_data
25
+ @feature_store.init(init_data)
26
+ ready
27
+ end
28
+
29
+ def stop
30
+ @test_data.closed_instance(self)
31
+ end
32
+
33
+ def upsert(kind, item)
34
+ @feature_store.upsert(kind, item)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+
2
+ # See serialization.rb for implementation notes on the data model classes.
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ module Model
7
+ class Clause
8
+ def initialize(data, logger)
9
+ @data = data
10
+ @context_kind = data[:contextKind]
11
+ @attribute = (@context_kind.nil? || @context_kind.empty?) ? Reference.create_literal(data[:attribute]) : Reference.create(data[:attribute])
12
+ unless logger.nil? || @attribute.error.nil?
13
+ logger.error("[LDClient] Data inconsistency in feature flag: #{@attribute.error}")
14
+ end
15
+ @op = data[:op].to_sym
16
+ @values = data[:values] || []
17
+ @negate = !!data[:negate]
18
+ end
19
+
20
+ # @return [Hash]
21
+ attr_reader :data
22
+ # @return [String|nil]
23
+ attr_reader :context_kind
24
+ # @return [LaunchDarkly::Reference]
25
+ attr_reader :attribute
26
+ # @return [Symbol]
27
+ attr_reader :op
28
+ # @return [Array]
29
+ attr_reader :values
30
+ # @return [Boolean]
31
+ attr_reader :negate
32
+
33
+ def as_json
34
+ @data
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,213 @@
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
+ module LaunchDarkly
8
+ module Impl
9
+ module Model
10
+ class FeatureFlag
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
+ @data = data
16
+ @key = data[:key]
17
+ @version = data[:version]
18
+ @deleted = !!data[:deleted]
19
+ return if @deleted
20
+ @variations = data[:variations] || []
21
+ @on = !!data[:on]
22
+ fallthrough = data[:fallthrough] || {}
23
+ @fallthrough = VariationOrRollout.new(fallthrough[:variation], fallthrough[:rollout])
24
+ @off_variation = data[:offVariation]
25
+ @prerequisites = (data[:prerequisites] || []).map do |prereq_data|
26
+ Prerequisite.new(prereq_data, self, logger)
27
+ end
28
+ @targets = (data[:targets] || []).map do |target_data|
29
+ Target.new(target_data, self, logger)
30
+ end
31
+ @context_targets = (data[:contextTargets] || []).map do |target_data|
32
+ Target.new(target_data, self, logger)
33
+ end
34
+ @rules = (data[:rules] || []).map.with_index do |rule_data, index|
35
+ FlagRule.new(rule_data, index, self, logger)
36
+ end
37
+ @salt = data[:salt]
38
+ @off_result = EvaluatorHelpers.evaluation_detail_for_off_variation(self, EvaluationReason::off, logger)
39
+ @fallthrough_results = Preprocessor.precompute_multi_variation_results(self,
40
+ EvaluationReason::fallthrough(false), EvaluationReason::fallthrough(true))
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]
52
+ attr_reader :variations
53
+ # @return [Boolean]
54
+ attr_reader :on
55
+ # @return [Integer|nil]
56
+ attr_reader :off_variation
57
+ # @return [LaunchDarkly::Impl::Model::VariationOrRollout]
58
+ attr_reader :fallthrough
59
+ # @return [LaunchDarkly::EvaluationDetail]
60
+ attr_reader :off_result
61
+ # @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
62
+ attr_reader :fallthrough_results
63
+ # @return [Array<LaunchDarkly::Impl::Model::Prerequisite>]
64
+ attr_reader :prerequisites
65
+ # @return [Array<LaunchDarkly::Impl::Model::Target>]
66
+ attr_reader :targets
67
+ # @return [Array<LaunchDarkly::Impl::Model::Target>]
68
+ attr_reader :context_targets
69
+ # @return [Array<LaunchDarkly::Impl::Model::FlagRule>]
70
+ attr_reader :rules
71
+ # @return [String]
72
+ attr_reader :salt
73
+
74
+ # This method allows us to read properties of the object as if it's just a hash. Currently this is
75
+ # necessary because some data store logic is still written to expect hashes; we can remove it once
76
+ # we migrate entirely to using attributes of the class.
77
+ def [](key)
78
+ @data[key]
79
+ end
80
+
81
+ def ==(other)
82
+ other.is_a?(FeatureFlag) && other.data == self.data
83
+ end
84
+
85
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
86
+ @data
87
+ end
88
+
89
+ # Same as as_json, but converts the JSON structure into a string.
90
+ def to_json(*a)
91
+ as_json.to_json(a)
92
+ end
93
+ end
94
+
95
+ class Prerequisite
96
+ def initialize(data, flag, logger)
97
+ @data = data
98
+ @key = data[:key]
99
+ @variation = data[:variation]
100
+ @failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
101
+ EvaluationReason::prerequisite_failed(@key), logger)
102
+ end
103
+
104
+ # @return [Hash]
105
+ attr_reader :data
106
+ # @return [String]
107
+ attr_reader :key
108
+ # @return [Integer]
109
+ attr_reader :variation
110
+ # @return [LaunchDarkly::EvaluationDetail]
111
+ attr_reader :failure_result
112
+ end
113
+
114
+ class Target
115
+ def initialize(data, flag, logger)
116
+ @kind = data[:contextKind] || LDContext::KIND_DEFAULT
117
+ @data = data
118
+ @values = Set.new(data[:values] || [])
119
+ @variation = data[:variation]
120
+ @match_result = EvaluatorHelpers.evaluation_detail_for_variation(flag,
121
+ data[:variation], EvaluationReason::target_match, logger)
122
+ end
123
+
124
+ # @return [String]
125
+ attr_reader :kind
126
+ # @return [Hash]
127
+ attr_reader :data
128
+ # @return [Set]
129
+ attr_reader :values
130
+ # @return [Integer]
131
+ attr_reader :variation
132
+ # @return [LaunchDarkly::EvaluationDetail]
133
+ attr_reader :match_result
134
+ end
135
+
136
+ class FlagRule
137
+ def initialize(data, rule_index, flag, logger)
138
+ @data = data
139
+ @clauses = (data[:clauses] || []).map do |clause_data|
140
+ Clause.new(clause_data, logger)
141
+ end
142
+ @variation_or_rollout = VariationOrRollout.new(data[:variation], data[:rollout])
143
+ rule_id = data[:id]
144
+ match_reason = EvaluationReason::rule_match(rule_index, rule_id)
145
+ match_reason_in_experiment = EvaluationReason::rule_match(rule_index, rule_id, true)
146
+ @match_results = Preprocessor.precompute_multi_variation_results(flag, match_reason, match_reason_in_experiment)
147
+ end
148
+
149
+ # @return [Hash]
150
+ attr_reader :data
151
+ # @return [Array<LaunchDarkly::Impl::Model::Clause>]
152
+ attr_reader :clauses
153
+ # @return [LaunchDarkly::Impl::Model::EvalResultFactoryMultiVariations]
154
+ attr_reader :match_results
155
+ # @return [LaunchDarkly::Impl::Model::VariationOrRollout]
156
+ attr_reader :variation_or_rollout
157
+ end
158
+
159
+ class VariationOrRollout
160
+ def initialize(variation, rollout_data)
161
+ @variation = variation
162
+ @rollout = rollout_data.nil? ? nil : Rollout.new(rollout_data)
163
+ end
164
+
165
+ # @return [Integer|nil]
166
+ attr_reader :variation
167
+ # @return [Rollout|nil] currently we do not have a model class for the rollout
168
+ attr_reader :rollout
169
+ end
170
+
171
+ class Rollout
172
+ def initialize(data)
173
+ @context_kind = data[:contextKind]
174
+ @variations = (data[:variations] || []).map { |v| WeightedVariation.new(v) }
175
+ @bucket_by = data[:bucketBy]
176
+ @kind = data[:kind]
177
+ @is_experiment = @kind == "experiment"
178
+ @seed = data[:seed]
179
+ end
180
+
181
+ # @return [String|nil]
182
+ attr_reader :context_kind
183
+ # @return [Array<WeightedVariation>]
184
+ attr_reader :variations
185
+ # @return [String|nil]
186
+ attr_reader :bucket_by
187
+ # @return [String|nil]
188
+ attr_reader :kind
189
+ # @return [Boolean]
190
+ attr_reader :is_experiment
191
+ # @return [Integer|nil]
192
+ attr_reader :seed
193
+ end
194
+
195
+ class WeightedVariation
196
+ def initialize(data)
197
+ @variation = data[:variation]
198
+ @weight = data[:weight]
199
+ @untracked = !!data[:untracked]
200
+ end
201
+
202
+ # @return [Integer]
203
+ attr_reader :variation
204
+ # @return [Integer]
205
+ attr_reader :weight
206
+ # @return [Boolean]
207
+ attr_reader :untracked
208
+ end
209
+
210
+ # Clause is defined in its own file because clauses are used by both flags and segments
211
+ end
212
+ end
213
+ 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,126 @@
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
+ @data = data
16
+ @key = data[:key]
17
+ @version = data[:version]
18
+ @deleted = !!data[:deleted]
19
+ return if @deleted
20
+ @included = data[:included] || []
21
+ @excluded = data[:excluded] || []
22
+ @included_contexts = (data[:includedContexts] || []).map do |target_data|
23
+ SegmentTarget.new(target_data)
24
+ end
25
+ @excluded_contexts = (data[:excludedContexts] || []).map do |target_data|
26
+ SegmentTarget.new(target_data)
27
+ end
28
+ @rules = (data[:rules] || []).map do |rule_data|
29
+ SegmentRule.new(rule_data, logger)
30
+ end
31
+ @unbounded = !!data[:unbounded]
32
+ @unbounded_context_kind = data[:unboundedContextKind] || LDContext::KIND_DEFAULT
33
+ @generation = data[:generation]
34
+ @salt = data[:salt]
35
+ end
36
+
37
+ # @return [Hash]
38
+ attr_reader :data
39
+ # @return [String]
40
+ attr_reader :key
41
+ # @return [Integer]
42
+ attr_reader :version
43
+ # @return [Boolean]
44
+ attr_reader :deleted
45
+ # @return [Array<String>]
46
+ attr_reader :included
47
+ # @return [Array<String>]
48
+ attr_reader :excluded
49
+ # @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
50
+ attr_reader :included_contexts
51
+ # @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
52
+ attr_reader :excluded_contexts
53
+ # @return [Array<SegmentRule>]
54
+ attr_reader :rules
55
+ # @return [Boolean]
56
+ attr_reader :unbounded
57
+ # @return [String]
58
+ attr_reader :unbounded_context_kind
59
+ # @return [Integer|nil]
60
+ attr_reader :generation
61
+ # @return [String]
62
+ attr_reader :salt
63
+
64
+ # This method allows us to read properties of the object as if it's just a hash. Currently this is
65
+ # necessary because some data store logic is still written to expect hashes; we can remove it once
66
+ # we migrate entirely to using attributes of the class.
67
+ def [](key)
68
+ @data[key]
69
+ end
70
+
71
+ def ==(other)
72
+ other.is_a?(Segment) && other.data == self.data
73
+ end
74
+
75
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
76
+ @data
77
+ end
78
+
79
+ # Same as as_json, but converts the JSON structure into a string.
80
+ def to_json(*a)
81
+ as_json.to_json(a)
82
+ end
83
+ end
84
+
85
+ class SegmentTarget
86
+ def initialize(data)
87
+ @data = data
88
+ @context_kind = data[:contextKind]
89
+ @values = Set.new(data[:values] || [])
90
+ end
91
+
92
+ # @return [Hash]
93
+ attr_reader :data
94
+ # @return [String]
95
+ attr_reader :context_kind
96
+ # @return [Set]
97
+ attr_reader :values
98
+ end
99
+
100
+ class SegmentRule
101
+ def initialize(data, logger)
102
+ @data = data
103
+ @clauses = (data[:clauses] || []).map do |clause_data|
104
+ Clause.new(clause_data, logger)
105
+ end
106
+ @weight = data[:weight]
107
+ @bucket_by = data[:bucketBy]
108
+ @rollout_context_kind = data[:rolloutContextKind]
109
+ end
110
+
111
+ # @return [Hash]
112
+ attr_reader :data
113
+ # @return [Array<LaunchDarkly::Impl::Model::Clause>]
114
+ attr_reader :clauses
115
+ # @return [Integer|nil]
116
+ attr_reader :weight
117
+ # @return [String|nil]
118
+ attr_reader :bucket_by
119
+ # @return [String|nil]
120
+ attr_reader :rollout_context_kind
121
+ end
122
+
123
+ # Clause is defined in its own file because clauses are used by both flags and segments
124
+ end
125
+ end
126
+ 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 warnings 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
@@ -0,0 +1,47 @@
1
+ require "ldclient-rb/util"
2
+
3
+ require "concurrent/atomics"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ class RepeatingTask
8
+ def initialize(interval, start_delay, task, logger)
9
+ @interval = interval
10
+ @start_delay = start_delay
11
+ @task = task
12
+ @logger = logger
13
+ @stopped = Concurrent::AtomicBoolean.new(false)
14
+ @worker = nil
15
+ end
16
+
17
+ def start
18
+ @worker = Thread.new do
19
+ if @start_delay
20
+ sleep(@start_delay)
21
+ end
22
+ until @stopped.value do
23
+ started_at = Time.now
24
+ begin
25
+ @task.call
26
+ rescue => e
27
+ LaunchDarkly::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
28
+ end
29
+ delta = @interval - (Time.now - started_at)
30
+ if delta > 0
31
+ sleep(delta)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def stop
38
+ if @stopped.make_true
39
+ if @worker && @worker.alive? && @worker != Thread.current
40
+ @worker.run # causes the thread to wake up if it's currently in a sleep
41
+ @worker.join
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -33,7 +33,7 @@ module LaunchDarkly
33
33
  return input if dependency_fn.nil? || input.empty?
34
34
  remaining_items = input.clone
35
35
  items_out = {}
36
- while !remaining_items.empty?
36
+ until remaining_items.empty?
37
37
  # pick a random item that hasn't been updated yet
38
38
  key, item = remaining_items.first
39
39
  self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out)
@@ -46,7 +46,7 @@ module LaunchDarkly
46
46
  remaining_items.delete(item_key) # we won't need to visit this item again
47
47
  dependency_fn.call(item).each do |dep_key|
48
48
  dep_item = remaining_items[dep_key.to_sym]
49
- self.add_with_dependencies_first(dep_item, dependency_fn, remaining_items, items_out) if !dep_item.nil?
49
+ self.add_with_dependencies_first(dep_item, dependency_fn, remaining_items, items_out) unless dep_item.nil?
50
50
  end
51
51
  items_out[item_key] = item
52
52
  end
@@ -25,7 +25,7 @@ module LaunchDarkly
25
25
 
26
26
  def dispose_all
27
27
  @lock.synchronize {
28
- @pool.map { |instance| @instance_destructor.call(instance) } if !@instance_destructor.nil?
28
+ @pool.map { |instance| @instance_destructor.call(instance) } unless @instance_destructor.nil?
29
29
  @pool.clear()
30
30
  }
31
31
  end