launchdarkly-server-sdk 6.3.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|