launchdarkly-server-sdk 8.11.2-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 +69 -9
- data/lib/ldclient-rb/context.rb +1 -1
- data/lib/ldclient-rb/data_system.rb +227 -0
- data/lib/ldclient-rb/events.rb +34 -19
- data/lib/ldclient-rb/flags_state.rb +1 -1
- data/lib/ldclient-rb/impl/big_segments.rb +4 -4
- data/lib/ldclient-rb/impl/cache_store.rb +44 -0
- data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
- data/lib/ldclient-rb/impl/data_source/requestor.rb +113 -0
- data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -0
- data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
- data/lib/ldclient-rb/impl/data_source.rb +3 -3
- data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
- data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
- data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
- data/lib/ldclient-rb/impl/data_store/status_provider.rb +76 -0
- data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
- data/lib/ldclient-rb/impl/data_store.rb +11 -97
- data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
- data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
- data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
- data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
- data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
- data/lib/ldclient-rb/impl/evaluator.rb +3 -2
- data/lib/ldclient-rb/impl/event_sender.rb +14 -6
- data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- 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 +290 -0
- data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
- data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
- data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
- data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
- data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
- data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +71 -0
- data/lib/ldclient-rb/impl.rb +1 -2
- data/lib/ldclient-rb/in_memory_store.rb +1 -18
- data/lib/ldclient-rb/integrations/file_data.rb +67 -0
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
- data/lib/ldclient-rb/integrations/test_data.rb +11 -11
- data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
- data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
- data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
- data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
- data/lib/ldclient-rb/ldclient.rb +66 -132
- data/lib/ldclient-rb/util.rb +11 -70
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +9 -17
- metadata +41 -19
- data/lib/ldclient-rb/cache_store.rb +0 -45
- data/lib/ldclient-rb/expiring_cache.rb +0 -77
- data/lib/ldclient-rb/memoized_value.rb +0 -32
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
- data/lib/ldclient-rb/polling.rb +0 -102
- data/lib/ldclient-rb/requestor.rb +0 -102
- data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
- data/lib/ldclient-rb/stream.rb +0 -197
|
@@ -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
|