launchdarkly-server-sdk 6.3.0 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -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/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- 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 +7 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- 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 +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- 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
@@ -16,14 +16,14 @@ module LaunchDarkly
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def initialize(opts)
|
19
|
-
|
19
|
+
unless CONSUL_ENABLED
|
20
20
|
raise RuntimeError.new("can't use Consul feature store without the 'diplomat' gem")
|
21
21
|
end
|
22
22
|
|
23
23
|
@prefix = (opts[:prefix] || LaunchDarkly::Integrations::Consul.default_prefix) + '/'
|
24
24
|
@logger = opts[:logger] || Config.default_logger
|
25
|
-
Diplomat.configuration = opts[:consul_config]
|
26
|
-
Diplomat.configuration.url = opts[:url]
|
25
|
+
Diplomat.configuration = opts[:consul_config] unless opts[:consul_config].nil?
|
26
|
+
Diplomat.configuration.url = opts[:url] unless opts[:url].nil?
|
27
27
|
@logger.info("ConsulFeatureStore: using Consul host at #{Diplomat.configuration.url}")
|
28
28
|
end
|
29
29
|
|
@@ -51,10 +51,10 @@ module LaunchDarkly
|
|
51
51
|
unused_old_keys.each do |key|
|
52
52
|
ops.push({ 'KV' => { 'Verb' => 'delete', 'Key' => key } })
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
# Now set the special key that we check in initialized_internal?
|
56
56
|
ops.push({ 'KV' => { 'Verb' => 'set', 'Key' => inited_key, 'Value' => '' } })
|
57
|
-
|
57
|
+
|
58
58
|
ConsulUtil.batch_operations(ops)
|
59
59
|
|
60
60
|
@logger.info { "Initialized database with #{num_items} items" }
|
@@ -70,7 +70,7 @@ module LaunchDarkly
|
|
70
70
|
results = Diplomat::Kv.get(kind_key(kind), { recurse: true }, :return)
|
71
71
|
(results == "" ? [] : results).each do |result|
|
72
72
|
value = result[:value]
|
73
|
-
|
73
|
+
unless value.nil?
|
74
74
|
item = Model.deserialize(kind, value)
|
75
75
|
items_out[item[:key].to_sym] = item
|
76
76
|
end
|
@@ -119,6 +119,18 @@ module LaunchDarkly
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
+
def available?
|
123
|
+
# Most implementations use the initialized_internal? method as a
|
124
|
+
# proxy for this check. However, since `initialized_internal?`
|
125
|
+
# catches a KeyNotFound exception, and that exception can be raised
|
126
|
+
# when the server goes away, we have to modify our behavior
|
127
|
+
# slightly.
|
128
|
+
Diplomat::Kv.get(inited_key, {}, :return, :return)
|
129
|
+
true
|
130
|
+
rescue
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
122
134
|
def stop
|
123
135
|
# There's no Consul client instance to dispose of
|
124
136
|
end
|
@@ -132,7 +144,7 @@ module LaunchDarkly
|
|
132
144
|
def kind_key(kind)
|
133
145
|
@prefix + kind[:namespace] + '/'
|
134
146
|
end
|
135
|
-
|
147
|
+
|
136
148
|
def inited_key
|
137
149
|
@prefix + '$inited'
|
138
150
|
end
|
@@ -16,28 +16,28 @@ module LaunchDarkly
|
|
16
16
|
AWS_SDK_ENABLED = false
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
PARTITION_KEY = "namespace"
|
21
21
|
SORT_KEY = "key"
|
22
22
|
|
23
23
|
def initialize(table_name, opts)
|
24
|
-
|
24
|
+
unless AWS_SDK_ENABLED
|
25
25
|
raise RuntimeError.new("can't use #{description} without the aws-sdk or aws-sdk-dynamodb gem")
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
@table_name = table_name
|
29
29
|
@prefix = opts[:prefix] ? (opts[:prefix] + ":") : ""
|
30
30
|
@logger = opts[:logger] || Config.default_logger
|
31
|
-
|
31
|
+
|
32
32
|
if !opts[:existing_client].nil?
|
33
33
|
@client = opts[:existing_client]
|
34
34
|
else
|
35
35
|
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
36
36
|
end
|
37
|
-
|
38
|
-
@logger.info("
|
37
|
+
|
38
|
+
@logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def stop
|
42
42
|
# AWS client doesn't seem to have a close method
|
43
43
|
end
|
@@ -46,7 +46,7 @@ module LaunchDarkly
|
|
46
46
|
"DynamoDB"
|
47
47
|
end
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
#
|
51
51
|
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
52
52
|
#
|
@@ -62,6 +62,14 @@ module LaunchDarkly
|
|
62
62
|
"DynamoDBFeatureStore"
|
63
63
|
end
|
64
64
|
|
65
|
+
def available?
|
66
|
+
resp = get_item_by_keys(inited_key, inited_key)
|
67
|
+
!resp.item.nil? && resp.item.length > 0
|
68
|
+
true
|
69
|
+
rescue
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
65
73
|
def init_internal(all_data)
|
66
74
|
# Start by reading the existing keys; we will later delete any of these that weren't in all_data.
|
67
75
|
unused_old_keys = read_existing_keys(all_data.keys)
|
@@ -83,7 +91,7 @@ module LaunchDarkly
|
|
83
91
|
del_item = make_keys_hash(tuple[0], tuple[1])
|
84
92
|
requests.push({ delete_request: { key: del_item } })
|
85
93
|
end
|
86
|
-
|
94
|
+
|
87
95
|
# Now set the special key that we check in initialized_internal?
|
88
96
|
inited_item = make_keys_hash(inited_key, inited_key)
|
89
97
|
requests.push({ put_request: { item: inited_item } })
|
@@ -123,11 +131,11 @@ module LaunchDarkly
|
|
123
131
|
expression_attribute_names: {
|
124
132
|
"#namespace" => PARTITION_KEY,
|
125
133
|
"#key" => SORT_KEY,
|
126
|
-
"#version" => VERSION_ATTRIBUTE
|
134
|
+
"#version" => VERSION_ATTRIBUTE,
|
127
135
|
},
|
128
136
|
expression_attribute_values: {
|
129
|
-
":version" => new_item[:version]
|
130
|
-
}
|
137
|
+
":version" => new_item[:version],
|
138
|
+
},
|
131
139
|
})
|
132
140
|
new_item
|
133
141
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
@@ -159,7 +167,7 @@ module LaunchDarkly
|
|
159
167
|
def make_keys_hash(namespace, key)
|
160
168
|
{
|
161
169
|
PARTITION_KEY => namespace,
|
162
|
-
SORT_KEY => key
|
170
|
+
SORT_KEY => key,
|
163
171
|
}
|
164
172
|
end
|
165
173
|
|
@@ -170,16 +178,16 @@ module LaunchDarkly
|
|
170
178
|
key_conditions: {
|
171
179
|
PARTITION_KEY => {
|
172
180
|
comparison_operator: "EQ",
|
173
|
-
attribute_value_list: [ namespace_for_kind(kind) ]
|
174
|
-
}
|
175
|
-
}
|
181
|
+
attribute_value_list: [ namespace_for_kind(kind) ],
|
182
|
+
},
|
183
|
+
},
|
176
184
|
}
|
177
185
|
end
|
178
186
|
|
179
187
|
def get_item_by_keys(namespace, key)
|
180
188
|
@client.get_item({
|
181
189
|
table_name: @table_name,
|
182
|
-
key: make_keys_hash(namespace, key)
|
190
|
+
key: make_keys_hash(namespace, key),
|
183
191
|
})
|
184
192
|
end
|
185
193
|
|
@@ -190,8 +198,8 @@ module LaunchDarkly
|
|
190
198
|
projection_expression: "#namespace, #key",
|
191
199
|
expression_attribute_names: {
|
192
200
|
"#namespace" => PARTITION_KEY,
|
193
|
-
"#key" => SORT_KEY
|
194
|
-
}
|
201
|
+
"#key" => SORT_KEY,
|
202
|
+
},
|
195
203
|
})
|
196
204
|
while true
|
197
205
|
resp = @client.query(req)
|
@@ -210,7 +218,7 @@ module LaunchDarkly
|
|
210
218
|
def marshal_item(kind, item)
|
211
219
|
make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
|
212
220
|
VERSION_ATTRIBUTE => item[:version],
|
213
|
-
ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item)
|
221
|
+
ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item),
|
214
222
|
})
|
215
223
|
end
|
216
224
|
|
@@ -223,11 +231,11 @@ module LaunchDarkly
|
|
223
231
|
end
|
224
232
|
|
225
233
|
class DynamoDBBigSegmentStore < DynamoDBStoreImplBase
|
226
|
-
KEY_METADATA = 'big_segments_metadata'
|
227
|
-
|
228
|
-
ATTR_SYNC_TIME = 'synchronizedOn'
|
229
|
-
ATTR_INCLUDED = 'included'
|
230
|
-
ATTR_EXCLUDED = 'excluded'
|
234
|
+
KEY_METADATA = 'big_segments_metadata'
|
235
|
+
KEY_CONTEXT_DATA = 'big_segments_user'
|
236
|
+
ATTR_SYNC_TIME = 'synchronizedOn'
|
237
|
+
ATTR_INCLUDED = 'included'
|
238
|
+
ATTR_EXCLUDED = 'excluded'
|
231
239
|
|
232
240
|
def initialize(table_name, opts)
|
233
241
|
super(table_name, opts)
|
@@ -243,7 +251,7 @@ module LaunchDarkly
|
|
243
251
|
table_name: @table_name,
|
244
252
|
key: {
|
245
253
|
PARTITION_KEY => key,
|
246
|
-
SORT_KEY => key
|
254
|
+
SORT_KEY => key,
|
247
255
|
}
|
248
256
|
)
|
249
257
|
timestamp = data.item && data.item[ATTR_SYNC_TIME] ?
|
@@ -251,14 +259,14 @@ module LaunchDarkly
|
|
251
259
|
LaunchDarkly::Interfaces::BigSegmentStoreMetadata.new(timestamp)
|
252
260
|
end
|
253
261
|
|
254
|
-
def get_membership(
|
262
|
+
def get_membership(context_hash)
|
255
263
|
data = @client.get_item(
|
256
264
|
table_name: @table_name,
|
257
265
|
key: {
|
258
|
-
PARTITION_KEY => @prefix +
|
259
|
-
SORT_KEY =>
|
266
|
+
PARTITION_KEY => @prefix + KEY_CONTEXT_DATA,
|
267
|
+
SORT_KEY => context_hash,
|
260
268
|
})
|
261
|
-
return nil
|
269
|
+
return nil unless data.item
|
262
270
|
excluded_refs = data.item[ATTR_EXCLUDED] || []
|
263
271
|
included_refs = data.item[ATTR_INCLUDED] || []
|
264
272
|
if excluded_refs.empty? && included_refs.empty?
|
@@ -18,10 +18,18 @@ module LaunchDarkly
|
|
18
18
|
require 'listen'
|
19
19
|
@@have_listen = true
|
20
20
|
rescue LoadError
|
21
|
+
# Ignored
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
#
|
25
|
+
# @param data_store [LaunchDarkly::Interfaces::FeatureStore]
|
26
|
+
# @param data_source_update_sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil] Might be nil for backwards compatibility reasons.
|
27
|
+
# @param logger [Logger]
|
28
|
+
# @param options [Hash]
|
29
|
+
#
|
30
|
+
def initialize(data_store, data_source_update_sink, logger, options={})
|
31
|
+
@data_store = data_source_update_sink || data_store
|
32
|
+
@data_source_update_sink = data_source_update_sink
|
25
33
|
@logger = logger
|
26
34
|
@paths = options[:paths] || []
|
27
35
|
if @paths.is_a? String
|
@@ -48,7 +56,7 @@ module LaunchDarkly
|
|
48
56
|
|
49
57
|
def start
|
50
58
|
ready = Concurrent::Event.new
|
51
|
-
|
59
|
+
|
52
60
|
# We will return immediately regardless of whether the file load succeeded or failed -
|
53
61
|
# the difference can be detected by checking "initialized?"
|
54
62
|
ready.set
|
@@ -63,9 +71,9 @@ module LaunchDarkly
|
|
63
71
|
|
64
72
|
ready
|
65
73
|
end
|
66
|
-
|
74
|
+
|
67
75
|
def stop
|
68
|
-
@listener.stop
|
76
|
+
@listener.stop unless @listener.nil?
|
69
77
|
end
|
70
78
|
|
71
79
|
private
|
@@ -73,17 +81,22 @@ module LaunchDarkly
|
|
73
81
|
def load_all
|
74
82
|
all_data = {
|
75
83
|
FEATURES => {},
|
76
|
-
SEGMENTS => {}
|
84
|
+
SEGMENTS => {},
|
77
85
|
}
|
78
86
|
@paths.each do |path|
|
79
87
|
begin
|
80
88
|
load_file(path, all_data)
|
81
89
|
rescue => exn
|
82
90
|
LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
|
91
|
+
@data_source_update_sink&.update_status(
|
92
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
93
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA, 0, exn.to_s, Time.now)
|
94
|
+
)
|
83
95
|
return
|
84
96
|
end
|
85
97
|
end
|
86
|
-
@
|
98
|
+
@data_store.init(all_data)
|
99
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
87
100
|
@initialized.make_true
|
88
101
|
end
|
89
102
|
|
@@ -121,12 +134,12 @@ module LaunchDarkly
|
|
121
134
|
|
122
135
|
def add_item(all_data, kind, item)
|
123
136
|
items = all_data[kind]
|
124
|
-
raise ArgumentError, "Received unknown item kind #{kind} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
|
137
|
+
raise ArgumentError, "Received unknown item kind #{kind[:namespace]} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
|
125
138
|
key = item[:key].to_sym
|
126
|
-
|
139
|
+
unless items[key].nil?
|
127
140
|
raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once"
|
128
141
|
end
|
129
|
-
items[key] = item
|
142
|
+
items[key] = Model.deserialize(kind, item)
|
130
143
|
end
|
131
144
|
|
132
145
|
def make_flag_with_value(key, value)
|
@@ -134,7 +147,7 @@ module LaunchDarkly
|
|
134
147
|
key: key,
|
135
148
|
on: true,
|
136
149
|
fallthrough: { variation: 0 },
|
137
|
-
variations: [ value ]
|
150
|
+
variations: [ value ],
|
138
151
|
}
|
139
152
|
end
|
140
153
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "ldclient-rb/interfaces"
|
1
2
|
require "concurrent/atomics"
|
2
3
|
require "json"
|
3
4
|
|
@@ -5,6 +6,95 @@ module LaunchDarkly
|
|
5
6
|
module Impl
|
6
7
|
module Integrations
|
7
8
|
module Redis
|
9
|
+
#
|
10
|
+
# An implementation of the LaunchDarkly client's feature store that uses a Redis
|
11
|
+
# instance. This object holds feature flags and related data received from the
|
12
|
+
# streaming API. Feature data can also be further cached in memory to reduce overhead
|
13
|
+
# of calls to Redis.
|
14
|
+
#
|
15
|
+
# To use this class, you must first have the `redis` and `connection-pool` gems
|
16
|
+
# installed. Then, create an instance and store it in the `feature_store` property
|
17
|
+
# of your client configuration.
|
18
|
+
#
|
19
|
+
class RedisFeatureStore
|
20
|
+
include LaunchDarkly::Interfaces::FeatureStore
|
21
|
+
|
22
|
+
# Note that this class is now just a facade around CachingStoreWrapper, which is in turn delegating
|
23
|
+
# to RedisFeatureStoreCore where the actual database logic is. This class was retained for historical
|
24
|
+
# reasons, so that existing code can still call RedisFeatureStore.new. In the future, we will migrate
|
25
|
+
# away from exposing these concrete classes and use factory methods instead.
|
26
|
+
|
27
|
+
#
|
28
|
+
# Constructor for a RedisFeatureStore instance.
|
29
|
+
#
|
30
|
+
# @param opts [Hash] the configuration options
|
31
|
+
# @option opts [String] :redis_url URL of the Redis instance (shortcut for omitting redis_opts)
|
32
|
+
# @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just redis_url)
|
33
|
+
# @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
|
34
|
+
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
|
35
|
+
# @option opts [Integer] :max_connections size of the Redis connection pool
|
36
|
+
# @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
|
37
|
+
# @option opts [Integer] :capacity maximum number of feature flags (or related objects) to cache locally
|
38
|
+
# @option opts [Object] :pool custom connection pool, if desired
|
39
|
+
# @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool.
|
40
|
+
#
|
41
|
+
def initialize(opts = {})
|
42
|
+
core = RedisFeatureStoreCore.new(opts)
|
43
|
+
@wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
def monitoring_enabled?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def available?
|
51
|
+
@wrapper.available?
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
56
|
+
# running at `localhost` with its default port.
|
57
|
+
#
|
58
|
+
def self.default_redis_url
|
59
|
+
LaunchDarkly::Integrations::Redis::default_redis_url
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Default value for the `prefix` constructor parameter.
|
64
|
+
#
|
65
|
+
def self.default_prefix
|
66
|
+
LaunchDarkly::Integrations::Redis::default_prefix
|
67
|
+
end
|
68
|
+
|
69
|
+
def get(kind, key)
|
70
|
+
@wrapper.get(kind, key)
|
71
|
+
end
|
72
|
+
|
73
|
+
def all(kind)
|
74
|
+
@wrapper.all(kind)
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete(kind, key, version)
|
78
|
+
@wrapper.delete(kind, key, version)
|
79
|
+
end
|
80
|
+
|
81
|
+
def init(all_data)
|
82
|
+
@wrapper.init(all_data)
|
83
|
+
end
|
84
|
+
|
85
|
+
def upsert(kind, item)
|
86
|
+
@wrapper.upsert(kind, item)
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialized?
|
90
|
+
@wrapper.initialized?
|
91
|
+
end
|
92
|
+
|
93
|
+
def stop
|
94
|
+
@wrapper.stop
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
8
98
|
class RedisStoreImplBase
|
9
99
|
begin
|
10
100
|
require "redis"
|
@@ -15,7 +105,7 @@ module LaunchDarkly
|
|
15
105
|
end
|
16
106
|
|
17
107
|
def initialize(opts)
|
18
|
-
|
108
|
+
unless REDIS_ENABLED
|
19
109
|
raise RuntimeError.new("can't use #{description} because one of these gems is missing: redis, connection_pool")
|
20
110
|
end
|
21
111
|
|
@@ -28,7 +118,7 @@ module LaunchDarkly
|
|
28
118
|
@logger = opts[:logger] || Config.default_logger
|
29
119
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
30
120
|
|
31
|
-
@stopped = Concurrent::AtomicBoolean.new
|
121
|
+
@stopped = Concurrent::AtomicBoolean.new
|
32
122
|
|
33
123
|
with_connection do |redis|
|
34
124
|
@logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
|
@@ -55,13 +145,11 @@ module LaunchDarkly
|
|
55
145
|
if opts[:redis_url]
|
56
146
|
redis_opts[:url] = opts[:redis_url]
|
57
147
|
end
|
58
|
-
|
148
|
+
unless redis_opts.include?(:url)
|
59
149
|
redis_opts[:url] = LaunchDarkly::Integrations::Redis::default_redis_url
|
60
150
|
end
|
61
151
|
max_connections = opts[:max_connections] || 16
|
62
|
-
|
63
|
-
::Redis.new(redis_opts)
|
64
|
-
end
|
152
|
+
opts[:pool] || ConnectionPool.new(size: max_connections) { ::Redis.new(redis_opts) }
|
65
153
|
end
|
66
154
|
end
|
67
155
|
|
@@ -75,6 +163,14 @@ module LaunchDarkly
|
|
75
163
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
76
164
|
end
|
77
165
|
|
166
|
+
def available?
|
167
|
+
# We don't care what the status is, only that we can connect
|
168
|
+
initialized_internal?
|
169
|
+
true
|
170
|
+
rescue
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
78
174
|
def description
|
79
175
|
"RedisFeatureStore"
|
80
176
|
end
|
@@ -135,6 +231,7 @@ module LaunchDarkly
|
|
135
231
|
else
|
136
232
|
final_item = old_item
|
137
233
|
action = new_item[:deleted] ? "delete" : "update"
|
234
|
+
# rubocop:disable Layout/LineLength
|
138
235
|
@logger.warn { "RedisFeatureStore: attempted to #{action} #{key} version: #{old_item[:version]} in '#{kind[:namespace]}' with a version that is the same or older: #{new_item[:version]}" }
|
139
236
|
end
|
140
237
|
redis.unwatch
|
@@ -151,7 +248,7 @@ module LaunchDarkly
|
|
151
248
|
private
|
152
249
|
|
153
250
|
def before_update_transaction(base_key, key)
|
154
|
-
@test_hook.before_update_transaction(base_key, key)
|
251
|
+
@test_hook.before_update_transaction(base_key, key) unless @test_hook.nil?
|
155
252
|
end
|
156
253
|
|
157
254
|
def items_key(kind)
|
@@ -176,8 +273,8 @@ module LaunchDarkly
|
|
176
273
|
#
|
177
274
|
class RedisBigSegmentStore < RedisStoreImplBase
|
178
275
|
KEY_LAST_UP_TO_DATE = ':big_segments_synchronized_on'
|
179
|
-
|
180
|
-
|
276
|
+
KEY_CONTEXT_INCLUDE = ':big_segment_include:'
|
277
|
+
KEY_CONTEXT_EXCLUDE = ':big_segment_exclude:'
|
181
278
|
|
182
279
|
def description
|
183
280
|
"RedisBigSegmentStore"
|
@@ -188,10 +285,10 @@ module LaunchDarkly
|
|
188
285
|
Interfaces::BigSegmentStoreMetadata.new(value.nil? ? nil : value.to_i)
|
189
286
|
end
|
190
287
|
|
191
|
-
def get_membership(
|
288
|
+
def get_membership(context_hash)
|
192
289
|
with_connection do |redis|
|
193
|
-
included_refs = redis.smembers(@prefix +
|
194
|
-
excluded_refs = redis.smembers(@prefix +
|
290
|
+
included_refs = redis.smembers(@prefix + KEY_CONTEXT_INCLUDE + context_hash)
|
291
|
+
excluded_refs = redis.smembers(@prefix + KEY_CONTEXT_EXCLUDE + context_hash)
|
195
292
|
if !included_refs && !excluded_refs
|
196
293
|
nil
|
197
294
|
else
|