launchdarkly-server-sdk 6.2.5 → 7.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 (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