launchdarkly-server-sdk 8.11.3-java → 8.12.0-java
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/lib/ldclient-rb/config.rb +25 -28
- data/lib/ldclient-rb/data_system.rb +22 -38
- data/lib/ldclient-rb/events.rb +1 -1
- data/lib/ldclient-rb/impl/data_source/requestor.rb +9 -2
- data/lib/ldclient-rb/impl/data_source/status_provider.rb +5 -0
- data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +1 -1
- data/lib/ldclient-rb/impl/data_store/status_provider.rb +0 -6
- data/lib/ldclient-rb/impl/data_store/store.rb +2 -2
- data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +81 -80
- data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
- data/lib/ldclient-rb/impl/data_system/polling.rb +59 -32
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +8 -8
- data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
- data/lib/ldclient-rb/impl/event_sender.rb +11 -4
- data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +4 -2
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +13 -7
- data/lib/ldclient-rb/integrations/file_data.rb +67 -0
- data/lib/ldclient-rb/integrations/test_data_v2.rb +27 -21
- data/lib/ldclient-rb/interfaces/data_system.rb +46 -97
- data/lib/ldclient-rb/interfaces/feature_store.rb +2 -2
- data/lib/ldclient-rb/ldclient.rb +16 -6
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +1 -0
- metadata +8 -4
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ldclient-rb/impl/util'
|
|
4
|
+
require 'ldclient-rb/interfaces/data_system'
|
|
5
|
+
require 'ldclient-rb/util'
|
|
6
|
+
|
|
7
|
+
require 'concurrent/atomics'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'yaml'
|
|
10
|
+
require 'pathname'
|
|
11
|
+
require 'thread'
|
|
12
|
+
|
|
13
|
+
module LaunchDarkly
|
|
14
|
+
module Impl
|
|
15
|
+
module Integrations
|
|
16
|
+
#
|
|
17
|
+
# Internal implementation of both Initializer and Synchronizer protocols for file-based data.
|
|
18
|
+
#
|
|
19
|
+
# This component reads feature flag and segment data from local files and provides them
|
|
20
|
+
# via the FDv2 protocol interfaces. Each instance implements both Initializer and Synchronizer
|
|
21
|
+
# protocols:
|
|
22
|
+
# - As an Initializer: reads files once and returns initial data
|
|
23
|
+
# - As a Synchronizer: watches for file changes and yields updates
|
|
24
|
+
#
|
|
25
|
+
# The files use the same format as the v1 file data source, supporting flags, flagValues,
|
|
26
|
+
# and segments in JSON or YAML format.
|
|
27
|
+
#
|
|
28
|
+
class FileDataSourceV2
|
|
29
|
+
include LaunchDarkly::Interfaces::DataSystem::Initializer
|
|
30
|
+
include LaunchDarkly::Interfaces::DataSystem::Synchronizer
|
|
31
|
+
|
|
32
|
+
# To avoid pulling in 'listen' and its transitive dependencies for people who aren't using the
|
|
33
|
+
# file data source or who don't need auto-updating, we only enable auto-update if the 'listen'
|
|
34
|
+
# gem has been provided by the host app.
|
|
35
|
+
@@have_listen = false
|
|
36
|
+
begin
|
|
37
|
+
require 'listen'
|
|
38
|
+
@@have_listen = true
|
|
39
|
+
rescue LoadError
|
|
40
|
+
# Ignored
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# Initialize the file data source.
|
|
45
|
+
#
|
|
46
|
+
# @param logger [Logger] the logger
|
|
47
|
+
# @param paths [Array<String>, String] file paths to load (or a single path string)
|
|
48
|
+
# @param poll_interval [Float] seconds between polling checks when watching files (default: 1)
|
|
49
|
+
#
|
|
50
|
+
def initialize(logger, paths:, poll_interval: 1)
|
|
51
|
+
@logger = logger
|
|
52
|
+
@paths = paths.is_a?(Array) ? paths : [paths]
|
|
53
|
+
@poll_interval = poll_interval
|
|
54
|
+
|
|
55
|
+
@closed = false
|
|
56
|
+
@update_queue = Queue.new
|
|
57
|
+
@lock = Mutex.new
|
|
58
|
+
@listener = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# Return the name of this data source.
|
|
63
|
+
#
|
|
64
|
+
# @return [String]
|
|
65
|
+
#
|
|
66
|
+
def name
|
|
67
|
+
'FileDataV2'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# Implementation of the Initializer.fetch method.
|
|
72
|
+
#
|
|
73
|
+
# Reads all configured files once and returns their contents as a Basis.
|
|
74
|
+
#
|
|
75
|
+
# @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for file data)
|
|
76
|
+
# @return [LaunchDarkly::Result] A Result containing either a Basis or an error message
|
|
77
|
+
#
|
|
78
|
+
def fetch(selector_store)
|
|
79
|
+
@lock.synchronize do
|
|
80
|
+
if @closed
|
|
81
|
+
return LaunchDarkly::Result.fail('FileDataV2 source has been closed')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
result = load_all_to_changeset
|
|
85
|
+
return result unless result.success?
|
|
86
|
+
|
|
87
|
+
change_set = result.value
|
|
88
|
+
basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
|
|
89
|
+
change_set: change_set,
|
|
90
|
+
persist: false,
|
|
91
|
+
environment_id: nil
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
LaunchDarkly::Result.success(basis)
|
|
95
|
+
end
|
|
96
|
+
rescue => e
|
|
97
|
+
@logger.error { "[LDClient] Error fetching file data: #{e.message}" }
|
|
98
|
+
LaunchDarkly::Result.fail("Error fetching file data: #{e.message}", e)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# Implementation of the Synchronizer.sync method.
|
|
103
|
+
#
|
|
104
|
+
# Yields initial data from files, then continues to watch for file changes
|
|
105
|
+
# and yields updates when files are modified.
|
|
106
|
+
#
|
|
107
|
+
# @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for file data)
|
|
108
|
+
# @yield [LaunchDarkly::Interfaces::DataSystem::Update] Yields Update objects as synchronization progresses
|
|
109
|
+
# @return [void]
|
|
110
|
+
#
|
|
111
|
+
def sync(selector_store)
|
|
112
|
+
# First yield initial data
|
|
113
|
+
initial_result = fetch(selector_store)
|
|
114
|
+
unless initial_result.success?
|
|
115
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
116
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
117
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
118
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
|
119
|
+
0,
|
|
120
|
+
initial_result.error,
|
|
121
|
+
Time.now
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
128
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
129
|
+
change_set: initial_result.value.change_set
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Start watching for file changes
|
|
133
|
+
@lock.synchronize do
|
|
134
|
+
@listener = start_listener unless @closed
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
until @closed
|
|
138
|
+
begin
|
|
139
|
+
update = @update_queue.pop
|
|
140
|
+
|
|
141
|
+
# stop() pushes nil to wake us up when shutting down
|
|
142
|
+
break if update.nil?
|
|
143
|
+
|
|
144
|
+
yield update
|
|
145
|
+
rescue => e
|
|
146
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
147
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
148
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
149
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
|
|
150
|
+
0,
|
|
151
|
+
"Error in file data synchronizer: #{e.message}",
|
|
152
|
+
Time.now
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
break
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
# Stop the data source and clean up resources.
|
|
162
|
+
#
|
|
163
|
+
# @return [void]
|
|
164
|
+
#
|
|
165
|
+
def stop
|
|
166
|
+
@lock.synchronize do
|
|
167
|
+
return if @closed
|
|
168
|
+
@closed = true
|
|
169
|
+
|
|
170
|
+
listener = @listener
|
|
171
|
+
@listener = nil
|
|
172
|
+
|
|
173
|
+
listener&.stop
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Signal shutdown to sync generator
|
|
177
|
+
@update_queue.push(nil)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
#
|
|
181
|
+
# Load all files and build a changeset.
|
|
182
|
+
#
|
|
183
|
+
# @return [LaunchDarkly::Result] A Result containing either a ChangeSet or an error message
|
|
184
|
+
#
|
|
185
|
+
private def load_all_to_changeset
|
|
186
|
+
flags_dict = {}
|
|
187
|
+
segments_dict = {}
|
|
188
|
+
|
|
189
|
+
@paths.each do |path|
|
|
190
|
+
begin
|
|
191
|
+
load_file(path, flags_dict, segments_dict)
|
|
192
|
+
rescue => e
|
|
193
|
+
Impl::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", e)
|
|
194
|
+
return LaunchDarkly::Result.fail("Unable to load flag data from \"#{path}\": #{e.message}", e)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
199
|
+
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
|
|
200
|
+
|
|
201
|
+
flags_dict.each do |key, flag_data|
|
|
202
|
+
builder.add_put(
|
|
203
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
|
|
204
|
+
key,
|
|
205
|
+
flag_data[:version] || 1,
|
|
206
|
+
flag_data
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
segments_dict.each do |key, segment_data|
|
|
211
|
+
builder.add_put(
|
|
212
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
|
|
213
|
+
key,
|
|
214
|
+
segment_data[:version] || 1,
|
|
215
|
+
segment_data
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Use no_selector since we don't have versioning information from files
|
|
220
|
+
change_set = builder.finish(LaunchDarkly::Interfaces::DataSystem::Selector.no_selector)
|
|
221
|
+
|
|
222
|
+
LaunchDarkly::Result.success(change_set)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
#
|
|
226
|
+
# Load a single file and add its contents to the provided dictionaries.
|
|
227
|
+
#
|
|
228
|
+
# @param path [String] path to the file
|
|
229
|
+
# @param flags_dict [Hash] dictionary to add flags to
|
|
230
|
+
# @param segments_dict [Hash] dictionary to add segments to
|
|
231
|
+
#
|
|
232
|
+
private def load_file(path, flags_dict, segments_dict)
|
|
233
|
+
parsed = parse_content(File.read(path))
|
|
234
|
+
|
|
235
|
+
(parsed[:flags] || {}).each do |key, flag|
|
|
236
|
+
flag[:version] ||= 1
|
|
237
|
+
add_item(flags_dict, 'flags', flag)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
(parsed[:flagValues] || {}).each do |key, value|
|
|
241
|
+
add_item(flags_dict, 'flags', make_flag_with_value(key.to_s, value))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
(parsed[:segments] || {}).each do |key, segment|
|
|
245
|
+
segment[:version] ||= 1
|
|
246
|
+
add_item(segments_dict, 'segments', segment)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#
|
|
251
|
+
# Parse file content as JSON or YAML.
|
|
252
|
+
#
|
|
253
|
+
# @param content [String] file content string
|
|
254
|
+
# @return [Hash] parsed dictionary with symbolized keys
|
|
255
|
+
#
|
|
256
|
+
private def parse_content(content)
|
|
257
|
+
# Ruby's YAML parser correctly handles JSON as well
|
|
258
|
+
symbolize_all_keys(YAML.safe_load(content)) || {}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
#
|
|
262
|
+
# Recursively symbolize all keys in a hash or array.
|
|
263
|
+
#
|
|
264
|
+
# @param value [Object] the value to symbolize
|
|
265
|
+
# @return [Object] the value with all keys symbolized
|
|
266
|
+
private def symbolize_all_keys(value)
|
|
267
|
+
if value.is_a?(Hash)
|
|
268
|
+
value.map { |k, v| [k.to_sym, symbolize_all_keys(v)] }.to_h
|
|
269
|
+
elsif value.is_a?(Array)
|
|
270
|
+
value.map { |v| symbolize_all_keys(v) }
|
|
271
|
+
else
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
#
|
|
277
|
+
# Add an item to a dictionary, checking for duplicates.
|
|
278
|
+
#
|
|
279
|
+
# @param items_dict [Hash] dictionary to add to
|
|
280
|
+
# @param kind_name [String] name of the kind (for error messages)
|
|
281
|
+
# @param item [Hash] item to add
|
|
282
|
+
#
|
|
283
|
+
private def add_item(items_dict, kind_name, item)
|
|
284
|
+
key = item[:key].to_sym
|
|
285
|
+
if items_dict[key].nil?
|
|
286
|
+
items_dict[key] = item
|
|
287
|
+
else
|
|
288
|
+
raise ArgumentError, "In #{kind_name}, key \"#{item[:key]}\" was used more than once"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
#
|
|
293
|
+
# Create a simple flag configuration from a key-value pair.
|
|
294
|
+
#
|
|
295
|
+
# @param key [String] flag key
|
|
296
|
+
# @param value [Object] flag value
|
|
297
|
+
# @return [Hash] flag dictionary
|
|
298
|
+
#
|
|
299
|
+
private def make_flag_with_value(key, value)
|
|
300
|
+
{
|
|
301
|
+
key: key,
|
|
302
|
+
on: true,
|
|
303
|
+
version: 1,
|
|
304
|
+
fallthrough: { variation: 0 },
|
|
305
|
+
variations: [value],
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
#
|
|
310
|
+
# Callback invoked when files change.
|
|
311
|
+
#
|
|
312
|
+
# Reloads all files and queues an update.
|
|
313
|
+
#
|
|
314
|
+
private def on_file_change
|
|
315
|
+
@lock.synchronize do
|
|
316
|
+
return if @closed
|
|
317
|
+
|
|
318
|
+
begin
|
|
319
|
+
result = load_all_to_changeset
|
|
320
|
+
|
|
321
|
+
if result.success?
|
|
322
|
+
update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
323
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
324
|
+
change_set: result.value
|
|
325
|
+
)
|
|
326
|
+
@update_queue.push(update)
|
|
327
|
+
else
|
|
328
|
+
error_update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
329
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
330
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
331
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
|
332
|
+
0,
|
|
333
|
+
result.error,
|
|
334
|
+
Time.now
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
@update_queue.push(error_update)
|
|
338
|
+
end
|
|
339
|
+
rescue => e
|
|
340
|
+
@logger.error { "[LDClient] Error processing file change: #{e.message}" }
|
|
341
|
+
error_update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
342
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
343
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
344
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
|
|
345
|
+
0,
|
|
346
|
+
"Error processing file change: #{e.message}",
|
|
347
|
+
Time.now
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
@update_queue.push(error_update)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
#
|
|
356
|
+
# Start watching files for changes.
|
|
357
|
+
#
|
|
358
|
+
# @return [Object] auto-updater instance
|
|
359
|
+
#
|
|
360
|
+
private def start_listener
|
|
361
|
+
resolved_paths = @paths.map do |p|
|
|
362
|
+
begin
|
|
363
|
+
Pathname.new(File.absolute_path(p)).realpath.to_s
|
|
364
|
+
rescue
|
|
365
|
+
@logger.warn { "[LDClient] Cannot watch for changes to data file \"#{p}\" because it is an invalid path" }
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
end.compact
|
|
369
|
+
|
|
370
|
+
if @@have_listen
|
|
371
|
+
start_listener_with_listen_gem(resolved_paths)
|
|
372
|
+
else
|
|
373
|
+
FileDataSourcePollerV2.new(resolved_paths, @poll_interval, method(:on_file_change), @logger)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
#
|
|
378
|
+
# Start listening for file changes using the listen gem.
|
|
379
|
+
#
|
|
380
|
+
# @param resolved_paths [Array<String>] resolved file paths to watch
|
|
381
|
+
# @return [Listen::Listener] the listener instance
|
|
382
|
+
#
|
|
383
|
+
private def start_listener_with_listen_gem(resolved_paths)
|
|
384
|
+
path_set = resolved_paths.to_set
|
|
385
|
+
dir_paths = resolved_paths.map { |p| File.dirname(p) }.uniq
|
|
386
|
+
opts = { latency: @poll_interval }
|
|
387
|
+
l = Listen.to(*dir_paths, **opts) do |modified, added, removed|
|
|
388
|
+
paths = modified + added + removed
|
|
389
|
+
if paths.any? { |p| path_set.include?(p) }
|
|
390
|
+
on_file_change
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
l.start
|
|
394
|
+
l
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
#
|
|
399
|
+
# Used internally by FileDataSourceV2 to track data file changes if the 'listen' gem is not available.
|
|
400
|
+
#
|
|
401
|
+
class FileDataSourcePollerV2
|
|
402
|
+
#
|
|
403
|
+
# Initialize the file data poller.
|
|
404
|
+
#
|
|
405
|
+
# @param resolved_paths [Array<String>] resolved file paths to watch
|
|
406
|
+
# @param interval [Float] polling interval in seconds
|
|
407
|
+
# @param on_change_callback [Proc] callback to invoke when files change
|
|
408
|
+
# @param logger [Logger] the logger
|
|
409
|
+
#
|
|
410
|
+
def initialize(resolved_paths, interval, on_change_callback, logger)
|
|
411
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
|
412
|
+
@on_change = on_change_callback
|
|
413
|
+
@logger = logger
|
|
414
|
+
|
|
415
|
+
get_file_times = proc do
|
|
416
|
+
ret = {}
|
|
417
|
+
resolved_paths.each do |path|
|
|
418
|
+
begin
|
|
419
|
+
ret[path] = File.mtime(path)
|
|
420
|
+
rescue Errno::ENOENT
|
|
421
|
+
ret[path] = nil
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
ret
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
last_times = get_file_times.call
|
|
428
|
+
@thread = Thread.new do
|
|
429
|
+
loop do
|
|
430
|
+
sleep interval
|
|
431
|
+
break if @stopped.value
|
|
432
|
+
|
|
433
|
+
begin
|
|
434
|
+
new_times = get_file_times.call
|
|
435
|
+
changed = false
|
|
436
|
+
last_times.each do |path, old_time|
|
|
437
|
+
new_time = new_times[path]
|
|
438
|
+
if !new_time.nil? && new_time != old_time
|
|
439
|
+
changed = true
|
|
440
|
+
break
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
last_times = new_times
|
|
444
|
+
@on_change.call if changed
|
|
445
|
+
rescue => e
|
|
446
|
+
Impl::Util.log_exception(@logger, "Unexpected exception in FileDataSourcePollerV2", e)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
@thread.name = "LD/FileDataSourceV2"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def stop
|
|
454
|
+
@stopped.make_true
|
|
455
|
+
@thread.run # wakes it up if it's sleeping
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
@@ -192,9 +192,10 @@ module LaunchDarkly
|
|
|
192
192
|
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES)
|
|
193
193
|
|
|
194
194
|
# Add the updated flag
|
|
195
|
+
flag_key = flag_data[:key].to_sym
|
|
195
196
|
builder.add_put(
|
|
196
197
|
LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
|
|
197
|
-
|
|
198
|
+
flag_key,
|
|
198
199
|
flag_data[:version] || 1,
|
|
199
200
|
flag_data
|
|
200
201
|
)
|
|
@@ -247,9 +248,10 @@ module LaunchDarkly
|
|
|
247
248
|
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES)
|
|
248
249
|
|
|
249
250
|
# Add the updated segment
|
|
251
|
+
segment_key = segment_data[:key].to_sym
|
|
250
252
|
builder.add_put(
|
|
251
253
|
LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
|
|
252
|
-
|
|
254
|
+
segment_key,
|
|
253
255
|
segment_data[:version] || 1,
|
|
254
256
|
segment_data
|
|
255
257
|
)
|
|
@@ -35,7 +35,7 @@ module LaunchDarkly
|
|
|
35
35
|
items_out = {}
|
|
36
36
|
until remaining_items.empty?
|
|
37
37
|
# pick a random item that hasn't been updated yet
|
|
38
|
-
|
|
38
|
+
_, item = remaining_items.first
|
|
39
39
|
self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out)
|
|
40
40
|
end
|
|
41
41
|
items_out
|
|
@@ -118,12 +118,18 @@ module LaunchDarkly
|
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
#
|
|
122
|
+
# Creates a new persistent HTTP client with the given configuration.
|
|
123
|
+
#
|
|
124
|
+
# @param http_config [LaunchDarkly::Impl::DataSystem::HttpConfigOptions] HTTP connection settings
|
|
125
|
+
# @return [HTTP::Client]
|
|
126
|
+
#
|
|
127
|
+
def self.new_http_client(http_config)
|
|
122
128
|
http_client_options = {}
|
|
123
|
-
if
|
|
124
|
-
http_client_options["socket_class"] =
|
|
129
|
+
if http_config.socket_factory
|
|
130
|
+
http_client_options["socket_class"] = http_config.socket_factory
|
|
125
131
|
end
|
|
126
|
-
proxy = URI.parse(
|
|
132
|
+
proxy = URI.parse(http_config.base_uri).find_proxy
|
|
127
133
|
unless proxy.nil?
|
|
128
134
|
http_client_options["proxy"] = {
|
|
129
135
|
proxy_address: proxy.host,
|
|
@@ -134,10 +140,10 @@ module LaunchDarkly
|
|
|
134
140
|
end
|
|
135
141
|
HTTP::Client.new(http_client_options)
|
|
136
142
|
.timeout({
|
|
137
|
-
read:
|
|
138
|
-
connect:
|
|
143
|
+
read: http_config.read_timeout,
|
|
144
|
+
connect: http_config.connect_timeout,
|
|
139
145
|
})
|
|
140
|
-
.persistent(
|
|
146
|
+
.persistent(http_config.base_uri)
|
|
141
147
|
end
|
|
142
148
|
|
|
143
149
|
def self.log_exception(logger, message, exc)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'ldclient-rb/impl/integrations/file_data_source'
|
|
2
|
+
require 'ldclient-rb/impl/integrations/file_data_source_v2'
|
|
2
3
|
|
|
3
4
|
module LaunchDarkly
|
|
4
5
|
module Integrations
|
|
@@ -103,6 +104,72 @@ module LaunchDarkly
|
|
|
103
104
|
lambda { |sdk_key, config|
|
|
104
105
|
Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.data_source_update_sink, config.logger, options) }
|
|
105
106
|
end
|
|
107
|
+
|
|
108
|
+
#
|
|
109
|
+
# Returns a builder for the FDv2-compatible file data source.
|
|
110
|
+
#
|
|
111
|
+
# This method is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
|
|
112
|
+
# It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
|
|
113
|
+
#
|
|
114
|
+
# This method returns a builder proc that can be used with the FDv2 data system
|
|
115
|
+
# configuration as both an Initializer and a Synchronizer. When used as an Initializer
|
|
116
|
+
# (via `fetch`), it reads files once. When used as a Synchronizer (via `sync`), it
|
|
117
|
+
# watches for file changes and automatically updates when files are modified.
|
|
118
|
+
#
|
|
119
|
+
# @param options [Hash] the configuration options
|
|
120
|
+
# @option options [Array<String>, String] :paths The paths of the source files for loading flag data. These
|
|
121
|
+
# may be absolute paths or relative to the current working directory. (Required)
|
|
122
|
+
# @option options [Float] :poll_interval The minimum interval, in seconds, between checks for
|
|
123
|
+
# file modifications - used only if the native file-watching mechanism from 'listen' is not
|
|
124
|
+
# being used. The default value is 1 second.
|
|
125
|
+
# @return [FileDataSourceV2Builder] a builder that can be used as an FDv2 initializer or synchronizer
|
|
126
|
+
#
|
|
127
|
+
# @example Using as an initializer
|
|
128
|
+
# file_source = LaunchDarkly::Integrations::FileData.data_source_v2(paths: ['flags.json'])
|
|
129
|
+
# data_system_config = LaunchDarkly::DataSystemConfig.new(
|
|
130
|
+
# initializers: [file_source]
|
|
131
|
+
# )
|
|
132
|
+
# config = LaunchDarkly::Config.new(data_system: data_system_config)
|
|
133
|
+
#
|
|
134
|
+
# @example Using as a synchronizer
|
|
135
|
+
# file_source = LaunchDarkly::Integrations::FileData.data_source_v2(paths: ['flags.json'])
|
|
136
|
+
# data_system_config = LaunchDarkly::DataSystemConfig.new(
|
|
137
|
+
# synchronizers: [file_source]
|
|
138
|
+
# )
|
|
139
|
+
# config = LaunchDarkly::Config.new(data_system: data_system_config)
|
|
140
|
+
#
|
|
141
|
+
# @example Using as both initializer and synchronizer
|
|
142
|
+
# file_source = LaunchDarkly::Integrations::FileData.data_source_v2(paths: ['flags.json'])
|
|
143
|
+
# data_system_config = LaunchDarkly::DataSystemConfig.new(
|
|
144
|
+
# initializers: [file_source],
|
|
145
|
+
# synchronizers: [file_source]
|
|
146
|
+
# )
|
|
147
|
+
# config = LaunchDarkly::Config.new(data_system: data_system_config)
|
|
148
|
+
#
|
|
149
|
+
def self.data_source_v2(options = {})
|
|
150
|
+
paths = options[:paths] || []
|
|
151
|
+
poll_interval = options[:poll_interval] || 1
|
|
152
|
+
|
|
153
|
+
FileDataSourceV2Builder.new(paths, poll_interval)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
#
|
|
158
|
+
# Builder for FileDataSourceV2.
|
|
159
|
+
#
|
|
160
|
+
class FileDataSourceV2Builder
|
|
161
|
+
def initialize(paths, poll_interval)
|
|
162
|
+
@paths = paths
|
|
163
|
+
@poll_interval = poll_interval
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build(_sdk_key, config)
|
|
167
|
+
Impl::Integrations::FileDataSourceV2.new(
|
|
168
|
+
config.logger,
|
|
169
|
+
paths: @paths,
|
|
170
|
+
poll_interval: @poll_interval
|
|
171
|
+
)
|
|
172
|
+
end
|
|
106
173
|
end
|
|
107
174
|
end
|
|
108
175
|
end
|