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.
- 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
@@ -4,10 +4,7 @@ module LaunchDarkly
|
|
4
4
|
module Impl
|
5
5
|
module Integrations
|
6
6
|
module DynamoDB
|
7
|
-
|
8
|
-
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
9
|
-
#
|
10
|
-
class DynamoDBFeatureStoreCore
|
7
|
+
class DynamoDBStoreImplBase
|
11
8
|
begin
|
12
9
|
require "aws-sdk-dynamodb"
|
13
10
|
AWS_SDK_ENABLED = true
|
@@ -23,16 +20,13 @@ module LaunchDarkly
|
|
23
20
|
PARTITION_KEY = "namespace"
|
24
21
|
SORT_KEY = "key"
|
25
22
|
|
26
|
-
VERSION_ATTRIBUTE = "version"
|
27
|
-
ITEM_JSON_ATTRIBUTE = "item"
|
28
|
-
|
29
23
|
def initialize(table_name, opts)
|
30
|
-
|
31
|
-
raise RuntimeError.new("can't use
|
24
|
+
unless AWS_SDK_ENABLED
|
25
|
+
raise RuntimeError.new("can't use #{description} without the aws-sdk or aws-sdk-dynamodb gem")
|
32
26
|
end
|
33
27
|
|
34
28
|
@table_name = table_name
|
35
|
-
@prefix = opts[:prefix]
|
29
|
+
@prefix = opts[:prefix] ? (opts[:prefix] + ":") : ""
|
36
30
|
@logger = opts[:logger] || Config.default_logger
|
37
31
|
|
38
32
|
if !opts[:existing_client].nil?
|
@@ -41,7 +35,31 @@ module LaunchDarkly
|
|
41
35
|
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
42
36
|
end
|
43
37
|
|
44
|
-
@logger.info("
|
38
|
+
@logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop
|
42
|
+
# AWS client doesn't seem to have a close method
|
43
|
+
end
|
44
|
+
|
45
|
+
protected def description
|
46
|
+
"DynamoDB"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Internal implementation of the DynamoDB feature store, intended to be used with CachingStoreWrapper.
|
52
|
+
#
|
53
|
+
class DynamoDBFeatureStoreCore < DynamoDBStoreImplBase
|
54
|
+
VERSION_ATTRIBUTE = "version"
|
55
|
+
ITEM_JSON_ATTRIBUTE = "item"
|
56
|
+
|
57
|
+
def initialize(table_name, opts)
|
58
|
+
super(table_name, opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
def description
|
62
|
+
"DynamoDBFeatureStore"
|
45
63
|
end
|
46
64
|
|
47
65
|
def init_internal(all_data)
|
@@ -65,7 +83,7 @@ module LaunchDarkly
|
|
65
83
|
del_item = make_keys_hash(tuple[0], tuple[1])
|
66
84
|
requests.push({ delete_request: { key: del_item } })
|
67
85
|
end
|
68
|
-
|
86
|
+
|
69
87
|
# Now set the special key that we check in initialized_internal?
|
70
88
|
inited_item = make_keys_hash(inited_key, inited_key)
|
71
89
|
requests.push({ put_request: { item: inited_item } })
|
@@ -105,11 +123,11 @@ module LaunchDarkly
|
|
105
123
|
expression_attribute_names: {
|
106
124
|
"#namespace" => PARTITION_KEY,
|
107
125
|
"#key" => SORT_KEY,
|
108
|
-
"#version" => VERSION_ATTRIBUTE
|
126
|
+
"#version" => VERSION_ATTRIBUTE,
|
109
127
|
},
|
110
128
|
expression_attribute_values: {
|
111
|
-
":version" => new_item[:version]
|
112
|
-
}
|
129
|
+
":version" => new_item[:version],
|
130
|
+
},
|
113
131
|
})
|
114
132
|
new_item
|
115
133
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
@@ -124,14 +142,10 @@ module LaunchDarkly
|
|
124
142
|
!resp.item.nil? && resp.item.length > 0
|
125
143
|
end
|
126
144
|
|
127
|
-
def stop
|
128
|
-
# AWS client doesn't seem to have a close method
|
129
|
-
end
|
130
|
-
|
131
145
|
private
|
132
146
|
|
133
147
|
def prefixed_namespace(base_str)
|
134
|
-
|
148
|
+
@prefix + base_str
|
135
149
|
end
|
136
150
|
|
137
151
|
def namespace_for_kind(kind)
|
@@ -145,7 +159,7 @@ module LaunchDarkly
|
|
145
159
|
def make_keys_hash(namespace, key)
|
146
160
|
{
|
147
161
|
PARTITION_KEY => namespace,
|
148
|
-
SORT_KEY => key
|
162
|
+
SORT_KEY => key,
|
149
163
|
}
|
150
164
|
end
|
151
165
|
|
@@ -156,16 +170,16 @@ module LaunchDarkly
|
|
156
170
|
key_conditions: {
|
157
171
|
PARTITION_KEY => {
|
158
172
|
comparison_operator: "EQ",
|
159
|
-
attribute_value_list: [ namespace_for_kind(kind) ]
|
160
|
-
}
|
161
|
-
}
|
173
|
+
attribute_value_list: [ namespace_for_kind(kind) ],
|
174
|
+
},
|
175
|
+
},
|
162
176
|
}
|
163
177
|
end
|
164
178
|
|
165
179
|
def get_item_by_keys(namespace, key)
|
166
180
|
@client.get_item({
|
167
181
|
table_name: @table_name,
|
168
|
-
key: make_keys_hash(namespace, key)
|
182
|
+
key: make_keys_hash(namespace, key),
|
169
183
|
})
|
170
184
|
end
|
171
185
|
|
@@ -176,8 +190,8 @@ module LaunchDarkly
|
|
176
190
|
projection_expression: "#namespace, #key",
|
177
191
|
expression_attribute_names: {
|
178
192
|
"#namespace" => PARTITION_KEY,
|
179
|
-
"#key" => SORT_KEY
|
180
|
-
}
|
193
|
+
"#key" => SORT_KEY,
|
194
|
+
},
|
181
195
|
})
|
182
196
|
while true
|
183
197
|
resp = @client.query(req)
|
@@ -196,7 +210,7 @@ module LaunchDarkly
|
|
196
210
|
def marshal_item(kind, item)
|
197
211
|
make_keys_hash(namespace_for_kind(kind), item[:key]).merge({
|
198
212
|
VERSION_ATTRIBUTE => item[:version],
|
199
|
-
ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item)
|
213
|
+
ITEM_JSON_ATTRIBUTE => Model.serialize(kind, item),
|
200
214
|
})
|
201
215
|
end
|
202
216
|
|
@@ -208,6 +222,56 @@ module LaunchDarkly
|
|
208
222
|
end
|
209
223
|
end
|
210
224
|
|
225
|
+
class DynamoDBBigSegmentStore < DynamoDBStoreImplBase
|
226
|
+
KEY_METADATA = 'big_segments_metadata'
|
227
|
+
KEY_CONTEXT_DATA = 'big_segments_user'
|
228
|
+
ATTR_SYNC_TIME = 'synchronizedOn'
|
229
|
+
ATTR_INCLUDED = 'included'
|
230
|
+
ATTR_EXCLUDED = 'excluded'
|
231
|
+
|
232
|
+
def initialize(table_name, opts)
|
233
|
+
super(table_name, opts)
|
234
|
+
end
|
235
|
+
|
236
|
+
def description
|
237
|
+
"DynamoDBBigSegmentStore"
|
238
|
+
end
|
239
|
+
|
240
|
+
def get_metadata
|
241
|
+
key = @prefix + KEY_METADATA
|
242
|
+
data = @client.get_item(
|
243
|
+
table_name: @table_name,
|
244
|
+
key: {
|
245
|
+
PARTITION_KEY => key,
|
246
|
+
SORT_KEY => key,
|
247
|
+
}
|
248
|
+
)
|
249
|
+
timestamp = data.item && data.item[ATTR_SYNC_TIME] ?
|
250
|
+
data.item[ATTR_SYNC_TIME] : nil
|
251
|
+
LaunchDarkly::Interfaces::BigSegmentStoreMetadata.new(timestamp)
|
252
|
+
end
|
253
|
+
|
254
|
+
def get_membership(context_hash)
|
255
|
+
data = @client.get_item(
|
256
|
+
table_name: @table_name,
|
257
|
+
key: {
|
258
|
+
PARTITION_KEY => @prefix + KEY_CONTEXT_DATA,
|
259
|
+
SORT_KEY => context_hash,
|
260
|
+
})
|
261
|
+
return nil unless data.item
|
262
|
+
excluded_refs = data.item[ATTR_EXCLUDED] || []
|
263
|
+
included_refs = data.item[ATTR_INCLUDED] || []
|
264
|
+
if excluded_refs.empty? && included_refs.empty?
|
265
|
+
nil
|
266
|
+
else
|
267
|
+
membership = {}
|
268
|
+
excluded_refs.each { |ref| membership[ref] = false }
|
269
|
+
included_refs.each { |ref| membership[ref] = true }
|
270
|
+
membership
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
211
275
|
class DynamoDBUtil
|
212
276
|
#
|
213
277
|
# Calls client.batch_write_item as many times as necessary to submit all of the given requests.
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'ldclient-rb/in_memory_store'
|
2
|
+
require 'ldclient-rb/util'
|
3
|
+
|
4
|
+
require 'concurrent/atomics'
|
5
|
+
require 'json'
|
6
|
+
require 'yaml'
|
7
|
+
require 'pathname'
|
8
|
+
|
9
|
+
module LaunchDarkly
|
10
|
+
module Impl
|
11
|
+
module Integrations
|
12
|
+
class FileDataSourceImpl
|
13
|
+
# To avoid pulling in 'listen' and its transitive dependencies for people who aren't using the
|
14
|
+
# file data source or who don't need auto-updating, we only enable auto-update if the 'listen'
|
15
|
+
# gem has been provided by the host app.
|
16
|
+
@@have_listen = false
|
17
|
+
begin
|
18
|
+
require 'listen'
|
19
|
+
@@have_listen = true
|
20
|
+
rescue LoadError
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(feature_store, logger, options={})
|
24
|
+
@feature_store = feature_store
|
25
|
+
@logger = logger
|
26
|
+
@paths = options[:paths] || []
|
27
|
+
if @paths.is_a? String
|
28
|
+
@paths = [ @paths ]
|
29
|
+
end
|
30
|
+
@auto_update = options[:auto_update]
|
31
|
+
if @auto_update && @@have_listen && !options[:force_polling] # force_polling is used only for tests
|
32
|
+
# We have seen unreliable behavior in the 'listen' gem in JRuby 9.1 (https://github.com/guard/listen/issues/449).
|
33
|
+
# Therefore, on that platform we'll fall back to file polling instead.
|
34
|
+
if defined?(JRUBY_VERSION) && JRUBY_VERSION.start_with?("9.1.")
|
35
|
+
@use_listen = false
|
36
|
+
else
|
37
|
+
@use_listen = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@poll_interval = options[:poll_interval] || 1
|
41
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
42
|
+
@ready = Concurrent::Event.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialized?
|
46
|
+
@initialized.value
|
47
|
+
end
|
48
|
+
|
49
|
+
def start
|
50
|
+
ready = Concurrent::Event.new
|
51
|
+
|
52
|
+
# We will return immediately regardless of whether the file load succeeded or failed -
|
53
|
+
# the difference can be detected by checking "initialized?"
|
54
|
+
ready.set
|
55
|
+
|
56
|
+
load_all
|
57
|
+
|
58
|
+
if @auto_update
|
59
|
+
# If we're going to watch files, then the start event will be set the first time we get
|
60
|
+
# a successful load.
|
61
|
+
@listener = start_listener
|
62
|
+
end
|
63
|
+
|
64
|
+
ready
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop
|
68
|
+
@listener.stop unless @listener.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def load_all
|
74
|
+
all_data = {
|
75
|
+
FEATURES => {},
|
76
|
+
SEGMENTS => {},
|
77
|
+
}
|
78
|
+
@paths.each do |path|
|
79
|
+
begin
|
80
|
+
load_file(path, all_data)
|
81
|
+
rescue => exn
|
82
|
+
LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
|
83
|
+
return
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@feature_store.init(all_data)
|
87
|
+
@initialized.make_true
|
88
|
+
end
|
89
|
+
|
90
|
+
def load_file(path, all_data)
|
91
|
+
parsed = parse_content(IO.read(path))
|
92
|
+
(parsed[:flags] || {}).each do |key, flag|
|
93
|
+
add_item(all_data, FEATURES, flag)
|
94
|
+
end
|
95
|
+
(parsed[:flagValues] || {}).each do |key, value|
|
96
|
+
add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value))
|
97
|
+
end
|
98
|
+
(parsed[:segments] || {}).each do |key, segment|
|
99
|
+
add_item(all_data, SEGMENTS, segment)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_content(content)
|
104
|
+
# We can use the Ruby YAML parser for both YAML and JSON (JSON is a subset of YAML and while
|
105
|
+
# not all YAML parsers handle it correctly, we have verified that the Ruby one does, at least
|
106
|
+
# for all the samples of actual flag data that we've tested).
|
107
|
+
symbolize_all_keys(YAML.safe_load(content))
|
108
|
+
end
|
109
|
+
|
110
|
+
def symbolize_all_keys(value)
|
111
|
+
# This is necessary because YAML.load doesn't have an option for parsing keys as symbols, and
|
112
|
+
# the SDK expects all objects to be formatted that way.
|
113
|
+
if value.is_a?(Hash)
|
114
|
+
value.map{ |k, v| [k.to_sym, symbolize_all_keys(v)] }.to_h
|
115
|
+
elsif value.is_a?(Array)
|
116
|
+
value.map{ |v| symbolize_all_keys(v) }
|
117
|
+
else
|
118
|
+
value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_item(all_data, kind, item)
|
123
|
+
items = all_data[kind]
|
124
|
+
raise ArgumentError, "Received unknown item kind #{kind[:namespace]} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
|
125
|
+
key = item[:key].to_sym
|
126
|
+
unless items[key].nil?
|
127
|
+
raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once"
|
128
|
+
end
|
129
|
+
items[key] = Model.deserialize(kind, item)
|
130
|
+
end
|
131
|
+
|
132
|
+
def make_flag_with_value(key, value)
|
133
|
+
{
|
134
|
+
key: key,
|
135
|
+
on: true,
|
136
|
+
fallthrough: { variation: 0 },
|
137
|
+
variations: [ value ],
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def start_listener
|
142
|
+
resolved_paths = @paths.map { |p| Pathname.new(File.absolute_path(p)).realpath.to_s }
|
143
|
+
if @use_listen
|
144
|
+
start_listener_with_listen_gem(resolved_paths)
|
145
|
+
else
|
146
|
+
FileDataSourcePoller.new(resolved_paths, @poll_interval, self.method(:load_all), @logger)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def start_listener_with_listen_gem(resolved_paths)
|
151
|
+
path_set = resolved_paths.to_set
|
152
|
+
dir_paths = resolved_paths.map{ |p| File.dirname(p) }.uniq
|
153
|
+
opts = { latency: @poll_interval }
|
154
|
+
l = Listen.to(*dir_paths, opts) do |modified, added, removed|
|
155
|
+
paths = modified + added + removed
|
156
|
+
if paths.any? { |p| path_set.include?(p) }
|
157
|
+
load_all
|
158
|
+
end
|
159
|
+
end
|
160
|
+
l.start
|
161
|
+
l
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# Used internally by FileDataSource to track data file changes if the 'listen' gem is not available.
|
166
|
+
#
|
167
|
+
class FileDataSourcePoller
|
168
|
+
def initialize(resolved_paths, interval, reloader, logger)
|
169
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
170
|
+
get_file_times = Proc.new do
|
171
|
+
ret = {}
|
172
|
+
resolved_paths.each do |path|
|
173
|
+
begin
|
174
|
+
ret[path] = File.mtime(path)
|
175
|
+
rescue Errno::ENOENT
|
176
|
+
ret[path] = nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
ret
|
180
|
+
end
|
181
|
+
last_times = get_file_times.call
|
182
|
+
@thread = Thread.new do
|
183
|
+
while true
|
184
|
+
sleep interval
|
185
|
+
break if @stopped.value
|
186
|
+
begin
|
187
|
+
new_times = get_file_times.call
|
188
|
+
changed = false
|
189
|
+
last_times.each do |path, old_time|
|
190
|
+
new_time = new_times[path]
|
191
|
+
if !new_time.nil? && new_time != old_time
|
192
|
+
changed = true
|
193
|
+
break
|
194
|
+
end
|
195
|
+
end
|
196
|
+
reloader.call if changed
|
197
|
+
rescue => exn
|
198
|
+
LaunchDarkly::Util.log_exception(logger, "Unexpected exception in FileDataSourcePoller", exn)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def stop
|
205
|
+
@stopped.make_true
|
206
|
+
@thread.run # wakes it up if it's sleeping
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -6,9 +6,87 @@ module LaunchDarkly
|
|
6
6
|
module Integrations
|
7
7
|
module Redis
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# An implementation of the LaunchDarkly client's feature store that uses a Redis
|
10
|
+
# instance. This object holds feature flags and related data received from the
|
11
|
+
# streaming API. Feature data can also be further cached in memory to reduce overhead
|
12
|
+
# of calls to Redis.
|
13
|
+
#
|
14
|
+
# To use this class, you must first have the `redis` and `connection-pool` gems
|
15
|
+
# installed. Then, create an instance and store it in the `feature_store` property
|
16
|
+
# of your client configuration.
|
10
17
|
#
|
11
|
-
class
|
18
|
+
class RedisFeatureStore
|
19
|
+
include LaunchDarkly::Interfaces::FeatureStore
|
20
|
+
|
21
|
+
# Note that this class is now just a facade around CachingStoreWrapper, which is in turn delegating
|
22
|
+
# to RedisFeatureStoreCore where the actual database logic is. This class was retained for historical
|
23
|
+
# reasons, so that existing code can still call RedisFeatureStore.new. In the future, we will migrate
|
24
|
+
# away from exposing these concrete classes and use factory methods instead.
|
25
|
+
|
26
|
+
#
|
27
|
+
# Constructor for a RedisFeatureStore instance.
|
28
|
+
#
|
29
|
+
# @param opts [Hash] the configuration options
|
30
|
+
# @option opts [String] :redis_url URL of the Redis instance (shortcut for omitting redis_opts)
|
31
|
+
# @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just redis_url)
|
32
|
+
# @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
|
33
|
+
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
|
34
|
+
# @option opts [Integer] :max_connections size of the Redis connection pool
|
35
|
+
# @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
|
36
|
+
# @option opts [Integer] :capacity maximum number of feature flags (or related objects) to cache locally
|
37
|
+
# @option opts [Object] :pool custom connection pool, if desired
|
38
|
+
# @option opts [Boolean] :pool_shutdown_on_close whether calling `close` should shutdown the custom connection pool.
|
39
|
+
#
|
40
|
+
def initialize(opts = {})
|
41
|
+
core = RedisFeatureStoreCore.new(opts)
|
42
|
+
@wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
47
|
+
# running at `localhost` with its default port.
|
48
|
+
#
|
49
|
+
def self.default_redis_url
|
50
|
+
LaunchDarkly::Integrations::Redis::default_redis_url
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Default value for the `prefix` constructor parameter.
|
55
|
+
#
|
56
|
+
def self.default_prefix
|
57
|
+
LaunchDarkly::Integrations::Redis::default_prefix
|
58
|
+
end
|
59
|
+
|
60
|
+
def get(kind, key)
|
61
|
+
@wrapper.get(kind, key)
|
62
|
+
end
|
63
|
+
|
64
|
+
def all(kind)
|
65
|
+
@wrapper.all(kind)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(kind, key, version)
|
69
|
+
@wrapper.delete(kind, key, version)
|
70
|
+
end
|
71
|
+
|
72
|
+
def init(all_data)
|
73
|
+
@wrapper.init(all_data)
|
74
|
+
end
|
75
|
+
|
76
|
+
def upsert(kind, item)
|
77
|
+
@wrapper.upsert(kind, item)
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialized?
|
81
|
+
@wrapper.initialized?
|
82
|
+
end
|
83
|
+
|
84
|
+
def stop
|
85
|
+
@wrapper.stop
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class RedisStoreImplBase
|
12
90
|
begin
|
13
91
|
require "redis"
|
14
92
|
require "connection_pool"
|
@@ -18,35 +96,68 @@ module LaunchDarkly
|
|
18
96
|
end
|
19
97
|
|
20
98
|
def initialize(opts)
|
21
|
-
|
22
|
-
raise RuntimeError.new("can't use
|
99
|
+
unless REDIS_ENABLED
|
100
|
+
raise RuntimeError.new("can't use #{description} because one of these gems is missing: redis, connection_pool")
|
23
101
|
end
|
24
102
|
|
25
|
-
@
|
26
|
-
|
27
|
-
@redis_opts[:url] = opts[:redis_url]
|
28
|
-
end
|
29
|
-
if !@redis_opts.include?(:url)
|
30
|
-
@redis_opts[:url] = LaunchDarkly::Integrations::Redis::default_redis_url
|
31
|
-
end
|
32
|
-
max_connections = opts[:max_connections] || 16
|
33
|
-
@pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
|
34
|
-
::Redis.new(@redis_opts)
|
35
|
-
end
|
103
|
+
@pool = create_redis_pool(opts)
|
104
|
+
|
36
105
|
# shutdown pool on close unless the client passed a custom pool and specified not to shutdown
|
37
106
|
@pool_shutdown_on_close = (!opts[:pool] || opts.fetch(:pool_shutdown_on_close, true))
|
107
|
+
|
38
108
|
@prefix = opts[:prefix] || LaunchDarkly::Integrations::Redis::default_prefix
|
39
109
|
@logger = opts[:logger] || Config.default_logger
|
40
110
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
41
111
|
|
42
|
-
@stopped = Concurrent::AtomicBoolean.new(
|
112
|
+
@stopped = Concurrent::AtomicBoolean.new()
|
43
113
|
|
44
114
|
with_connection do |redis|
|
45
|
-
@logger.info("
|
46
|
-
and prefix: #{@prefix}")
|
115
|
+
@logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
|
47
116
|
end
|
48
117
|
end
|
49
118
|
|
119
|
+
def stop
|
120
|
+
if @stopped.make_true
|
121
|
+
return unless @pool_shutdown_on_close
|
122
|
+
@pool.shutdown { |redis| redis.close }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
protected def description
|
127
|
+
"Redis"
|
128
|
+
end
|
129
|
+
|
130
|
+
protected def with_connection
|
131
|
+
@pool.with { |redis| yield(redis) }
|
132
|
+
end
|
133
|
+
|
134
|
+
private def create_redis_pool(opts)
|
135
|
+
redis_opts = opts[:redis_opts] ? opts[:redis_opts].clone : Hash.new
|
136
|
+
if opts[:redis_url]
|
137
|
+
redis_opts[:url] = opts[:redis_url]
|
138
|
+
end
|
139
|
+
unless redis_opts.include?(:url)
|
140
|
+
redis_opts[:url] = LaunchDarkly::Integrations::Redis::default_redis_url
|
141
|
+
end
|
142
|
+
max_connections = opts[:max_connections] || 16
|
143
|
+
opts[:pool] || ConnectionPool.new(size: max_connections) { ::Redis.new(redis_opts) }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Internal implementation of the Redis feature store, intended to be used with CachingStoreWrapper.
|
149
|
+
#
|
150
|
+
class RedisFeatureStoreCore < RedisStoreImplBase
|
151
|
+
def initialize(opts)
|
152
|
+
super(opts)
|
153
|
+
|
154
|
+
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
155
|
+
end
|
156
|
+
|
157
|
+
def description
|
158
|
+
"RedisFeatureStore"
|
159
|
+
end
|
160
|
+
|
50
161
|
def init_internal(all_data)
|
51
162
|
count = 0
|
52
163
|
with_connection do |redis|
|
@@ -103,8 +214,8 @@ module LaunchDarkly
|
|
103
214
|
else
|
104
215
|
final_item = old_item
|
105
216
|
action = new_item[:deleted] ? "delete" : "update"
|
106
|
-
|
107
|
-
|
217
|
+
# rubocop:disable Layout/LineLength
|
218
|
+
@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]}" }
|
108
219
|
end
|
109
220
|
redis.unwatch
|
110
221
|
end
|
@@ -117,17 +228,10 @@ module LaunchDarkly
|
|
117
228
|
with_connection { |redis| redis.exists?(inited_key) }
|
118
229
|
end
|
119
230
|
|
120
|
-
def stop
|
121
|
-
if @stopped.make_true
|
122
|
-
return unless @pool_shutdown_on_close
|
123
|
-
@pool.shutdown { |redis| redis.close }
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
231
|
private
|
128
232
|
|
129
233
|
def before_update_transaction(base_key, key)
|
130
|
-
@test_hook.before_update_transaction(base_key, key)
|
234
|
+
@test_hook.before_update_transaction(base_key, key) unless @test_hook.nil?
|
131
235
|
end
|
132
236
|
|
133
237
|
def items_key(kind)
|
@@ -142,14 +246,43 @@ module LaunchDarkly
|
|
142
246
|
@prefix + ":$inited"
|
143
247
|
end
|
144
248
|
|
145
|
-
def with_connection
|
146
|
-
@pool.with { |redis| yield(redis) }
|
147
|
-
end
|
148
|
-
|
149
249
|
def get_redis(redis, kind, key)
|
150
250
|
Model.deserialize(kind, redis.hget(items_key(kind), key))
|
151
251
|
end
|
152
252
|
end
|
253
|
+
|
254
|
+
#
|
255
|
+
# Internal implementation of the Redis big segment store.
|
256
|
+
#
|
257
|
+
class RedisBigSegmentStore < RedisStoreImplBase
|
258
|
+
KEY_LAST_UP_TO_DATE = ':big_segments_synchronized_on'
|
259
|
+
KEY_CONTEXT_INCLUDE = ':big_segment_include:'
|
260
|
+
KEY_CONTEXT_EXCLUDE = ':big_segment_exclude:'
|
261
|
+
|
262
|
+
def description
|
263
|
+
"RedisBigSegmentStore"
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_metadata
|
267
|
+
value = with_connection { |redis| redis.get(@prefix + KEY_LAST_UP_TO_DATE) }
|
268
|
+
Interfaces::BigSegmentStoreMetadata.new(value.nil? ? nil : value.to_i)
|
269
|
+
end
|
270
|
+
|
271
|
+
def get_membership(context_hash)
|
272
|
+
with_connection do |redis|
|
273
|
+
included_refs = redis.smembers(@prefix + KEY_CONTEXT_INCLUDE + context_hash)
|
274
|
+
excluded_refs = redis.smembers(@prefix + KEY_CONTEXT_EXCLUDE + context_hash)
|
275
|
+
if !included_refs && !excluded_refs
|
276
|
+
nil
|
277
|
+
else
|
278
|
+
membership = {}
|
279
|
+
excluded_refs.each { |ref| membership[ref] = false }
|
280
|
+
included_refs.each { |ref| membership[ref] = true }
|
281
|
+
membership
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
153
286
|
end
|
154
287
|
end
|
155
288
|
end
|