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.
- checksums.yaml +4 -4
- data/README.md +1 -2
- data/lib/ldclient-rb/config.rb +203 -43
- data/lib/ldclient-rb/context.rb +487 -0
- data/lib/ldclient-rb/evaluation_detail.rb +85 -26
- data/lib/ldclient-rb/events.rb +185 -146
- data/lib/ldclient-rb/flags_state.rb +25 -14
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +428 -132
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +6 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +78 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +92 -28
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +165 -32
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/model/clause.rb +39 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +126 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +62 -1
- data/lib/ldclient-rb/in_memory_store.rb +2 -2
- data/lib/ldclient-rb/integrations/consul.rb +9 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +43 -3
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +594 -0
- data/lib/ldclient-rb/integrations/test_data.rb +213 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +14 -9
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +151 -1
- data/lib/ldclient-rb/ldclient.rb +175 -133
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +7 -7
- data/lib/ldclient-rb/stream.rb +9 -9
- data/lib/ldclient-rb/util.rb +11 -17
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +2 -4
- metadata +49 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -314
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -0,0 +1,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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
33
|
+
#
|
34
|
+
# SDK code outside of Impl::Model should use this method instead of calling the model class
|
35
|
+
# constructors directly, so as not to rely on implementation details.
|
36
|
+
#
|
37
|
+
# @param kind [Hash] normally either FEATURES or SEGMENTS
|
38
|
+
# @param input [object] a JSON string or a parsed hash (or a data model object, in which case
|
39
|
+
# we'll just return the original object)
|
40
|
+
# @param logger [Logger|nil] logs 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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
{ FEATURES => flags, SEGMENTS => segments }
|
27
|
-
end
|
28
|
-
|
29
|
-
# Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
|
30
|
-
# or read it from a persistent data store). This allows us to precompute some derived attributes that
|
31
|
-
# will never change during the lifetime of that item.
|
32
|
-
def self.postprocess_item_after_deserializing!(kind, item)
|
33
|
-
return if !item
|
34
|
-
# Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
|
35
|
-
# to the "kind" object or the item class.
|
36
|
-
if kind.eql? FEATURES
|
37
|
-
# For feature flags, we precompute all possible parameterized EvaluationReason instances.
|
38
|
-
prereqs = item[:prerequisites]
|
39
|
-
if !prereqs.nil?
|
40
|
-
prereqs.each do |prereq|
|
41
|
-
prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
|
42
|
-
end
|
43
|
-
end
|
44
|
-
rules = item[:rules]
|
45
|
-
if !rules.nil?
|
46
|
-
rules.each_index do |i|
|
47
|
-
rule = rules[i]
|
48
|
-
rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.postprocess_items_after_deserializing!(kind, items_map)
|
55
|
-
return items_map if !items_map
|
56
|
-
items_map.each do |key, item|
|
57
|
-
postprocess_item_after_deserializing!(kind, item)
|
58
|
-
end
|
64
|
+
def self.make_all_store_data(received_data, logger = nil)
|
65
|
+
{
|
66
|
+
FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) },
|
67
|
+
SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) },
|
68
|
+
}
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
@@ -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
|
-
|
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)
|
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) }
|
28
|
+
@pool.map { |instance| @instance_destructor.call(instance) } unless @instance_destructor.nil?
|
29
29
|
@pool.clear()
|
30
30
|
}
|
31
31
|
end
|