launchdarkly-server-sdk 8.11.2 → 8.11.3

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +66 -3
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +243 -0
  5. data/lib/ldclient-rb/events.rb +34 -19
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +106 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +82 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  21. data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
  22. data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
  23. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  24. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  25. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  26. data/lib/ldclient-rb/impl/event_sender.rb +4 -3
  27. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  28. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  29. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
  30. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  31. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  32. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  33. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  34. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  35. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  36. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  37. data/lib/ldclient-rb/impl/util.rb +65 -0
  38. data/lib/ldclient-rb/impl.rb +1 -2
  39. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  40. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  41. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  42. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  43. data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
  44. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  45. data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
  46. data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
  47. data/lib/ldclient-rb/ldclient.rb +55 -131
  48. data/lib/ldclient-rb/util.rb +11 -70
  49. data/lib/ldclient-rb/version.rb +1 -1
  50. data/lib/ldclient-rb.rb +8 -17
  51. metadata +35 -17
  52. data/lib/ldclient-rb/cache_store.rb +0 -45
  53. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  54. data/lib/ldclient-rb/memoized_value.rb +0 -32
  55. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  56. data/lib/ldclient-rb/polling.rb +0 -102
  57. data/lib/ldclient-rb/requestor.rb +0 -102
  58. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  59. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require "ldclient-rb/config"
5
+ require "ldclient-rb/impl/data_system"
6
+ require "ldclient-rb/impl/data_store/store"
7
+ require "ldclient-rb/impl/data_store/feature_store_client_wrapper"
8
+ require "ldclient-rb/impl/data_source/status_provider"
9
+ require "ldclient-rb/impl/data_store/status_provider"
10
+ require "ldclient-rb/impl/broadcaster"
11
+ require "ldclient-rb/impl/repeating_task"
12
+ require "ldclient-rb/interfaces/data_system"
13
+
14
+ module LaunchDarkly
15
+ module Impl
16
+ module DataSystem
17
+ # FDv2 is an implementation of the DataSystem interface that uses the Flag Delivery V2 protocol
18
+ # for obtaining and keeping data up-to-date. Additionally, it operates with an optional persistent
19
+ # store in read-only or read/write mode.
20
+ class FDv2
21
+ include LaunchDarkly::Impl::DataSystem
22
+
23
+ # Initialize a new FDv2 data system.
24
+ #
25
+ # @param sdk_key [String] The SDK key
26
+ # @param config [LaunchDarkly::Config] Configuration for initializers and synchronizers
27
+ # @param data_system_config [LaunchDarkly::DataSystemConfig] FDv2 data system configuration
28
+ def initialize(sdk_key, config, data_system_config)
29
+ @sdk_key = sdk_key
30
+ @config = config
31
+ @data_system_config = data_system_config
32
+ @logger = config.logger
33
+ @primary_synchronizer_builder = data_system_config.primary_synchronizer
34
+ @secondary_synchronizer_builder = data_system_config.secondary_synchronizer
35
+ @fdv1_fallback_synchronizer_builder = data_system_config.fdv1_fallback_synchronizer
36
+ @disabled = @config.offline?
37
+
38
+ # Diagnostic accumulator provided by client for streaming metrics
39
+ @diagnostic_accumulator = nil
40
+
41
+ # Shared executor for all broadcasters
42
+ @shared_executor = Concurrent::SingleThreadExecutor.new
43
+
44
+ # Set up event listeners
45
+ @flag_change_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
46
+ @change_set_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
47
+ @data_source_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
48
+ @data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @logger)
49
+
50
+ recovery_listener = Object.new
51
+ recovery_listener.define_singleton_method(:update) do |data_store_status|
52
+ persistent_store_outage_recovery(data_store_status)
53
+ end
54
+ @data_store_broadcaster.add_listener(recovery_listener)
55
+
56
+ # Create the store
57
+ @store = LaunchDarkly::Impl::DataStore::Store.new(@flag_change_broadcaster, @change_set_broadcaster, @logger)
58
+
59
+ # Status providers
60
+ @data_source_status_provider = LaunchDarkly::Impl::DataSource::StatusProviderV2.new(
61
+ @data_source_broadcaster
62
+ )
63
+ @data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProviderV2.new(nil, @data_store_broadcaster)
64
+
65
+ # Configure persistent store if provided
66
+ if @data_system_config.data_store
67
+ @data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProviderV2.new(
68
+ @data_system_config.data_store,
69
+ @data_store_broadcaster
70
+ )
71
+ writable = @data_system_config.data_store_mode == :read_write
72
+ wrapper = LaunchDarkly::Impl::DataStore::FeatureStoreClientWrapperV2.new(
73
+ @data_system_config.data_store,
74
+ @data_store_status_provider,
75
+ @logger
76
+ )
77
+ @store.with_persistence(wrapper, writable, @data_store_status_provider)
78
+ end
79
+
80
+ # Threading
81
+ @stop_event = Concurrent::Event.new
82
+ @ready_event = Concurrent::Event.new
83
+ @lock = Mutex.new
84
+ @active_synchronizer = nil
85
+ @threads = []
86
+
87
+ # Track configuration
88
+ @configured_with_data_sources = (@data_system_config.initializers && !@data_system_config.initializers.empty?) ||
89
+ !@data_system_config.primary_synchronizer.nil?
90
+ end
91
+
92
+ # (see DataSystem#start)
93
+ def start
94
+ if @disabled
95
+ @logger.warn { "[LDClient] Data system is disabled, SDK will return application-defined default values" }
96
+ @ready_event.set
97
+ return @ready_event
98
+ end
99
+
100
+ @stop_event.reset
101
+ @ready_event.reset
102
+
103
+ # Start the main coordination thread
104
+ main_thread = Thread.new { run_main_loop }
105
+ main_thread.name = "FDv2-main"
106
+ @threads << main_thread
107
+
108
+ @ready_event
109
+ end
110
+
111
+ # (see DataSystem#stop)
112
+ def stop
113
+ @stop_event.set
114
+
115
+ @lock.synchronize do
116
+ if @active_synchronizer
117
+ begin
118
+ @active_synchronizer.stop
119
+ rescue => e
120
+ @logger.error { "[LDClient] Error stopping active data source: #{e.message}" }
121
+ end
122
+ end
123
+ end
124
+
125
+ # Wait for all threads to complete
126
+ @threads.each do |thread|
127
+ next unless thread.alive?
128
+
129
+ thread.join(5.0) # 5 second timeout
130
+ @logger.warn { "[LDClient] Thread #{thread.name} did not terminate in time" } if thread.alive?
131
+ end
132
+
133
+ # Close the store
134
+ @store.close
135
+
136
+ # Shutdown the shared executor
137
+ @shared_executor.shutdown
138
+ end
139
+
140
+ # (see DataSystem#set_diagnostic_accumulator)
141
+ def set_diagnostic_accumulator(diagnostic_accumulator)
142
+ @diagnostic_accumulator = diagnostic_accumulator
143
+ end
144
+
145
+ # (see DataSystem#store)
146
+ def store
147
+ @store.get_active_store
148
+ end
149
+
150
+ # (see DataSystem#data_source_status_provider)
151
+ def data_source_status_provider
152
+ @data_source_status_provider
153
+ end
154
+
155
+ # (see DataSystem#data_store_status_provider)
156
+ def data_store_status_provider
157
+ @data_store_status_provider
158
+ end
159
+
160
+ # (see DataSystem#flag_change_broadcaster)
161
+ def flag_change_broadcaster
162
+ @flag_change_broadcaster
163
+ end
164
+
165
+ # (see DataSystem#data_availability)
166
+ def data_availability
167
+ return DataAvailability::REFRESHED if @store.selector.defined?
168
+ return DataAvailability::CACHED if !@configured_with_data_sources || @store.initialized?
169
+
170
+ DataAvailability::DEFAULTS
171
+ end
172
+
173
+ # (see DataSystem#target_availability)
174
+ def target_availability
175
+ return DataAvailability::REFRESHED if @configured_with_data_sources
176
+
177
+ DataAvailability::CACHED
178
+ end
179
+
180
+ private
181
+
182
+ #
183
+ # Main coordination loop that manages initializers and synchronizers.
184
+ #
185
+ # @return [void]
186
+ #
187
+ def run_main_loop
188
+ begin
189
+ @data_source_status_provider.update_status(
190
+ LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
191
+ nil
192
+ )
193
+
194
+ # Run initializers first
195
+ run_initializers
196
+
197
+ # Run synchronizers
198
+ run_synchronizers
199
+ rescue => e
200
+ @logger.error { "[LDClient] Error in FDv2 main loop: #{e.message}" }
201
+ @ready_event.set
202
+ end
203
+ end
204
+
205
+ #
206
+ # Run initializers to get initial data.
207
+ #
208
+ # @return [void]
209
+ #
210
+ def run_initializers
211
+ return unless @data_system_config.initializers
212
+
213
+ @data_system_config.initializers.each do |initializer_builder|
214
+ return if @stop_event.set?
215
+
216
+ begin
217
+ initializer = initializer_builder.call(@sdk_key, @config)
218
+ @logger.info { "[LDClient] Attempting to initialize via #{initializer.name}" }
219
+
220
+ basis_result = initializer.fetch(@store)
221
+
222
+ if basis_result.success?
223
+ basis = basis_result.value
224
+ @logger.info { "[LDClient] Initialized via #{initializer.name}" }
225
+
226
+ # Apply the basis to the store
227
+ @store.apply(basis.change_set, basis.persist)
228
+
229
+ # Set ready event if and only if a selector is defined for the changeset
230
+ if basis.change_set.selector && basis.change_set.selector.defined?
231
+ @ready_event.set
232
+ return
233
+ end
234
+ else
235
+ @logger.warn { "[LDClient] Initializer #{initializer.name} failed: #{basis_result.error}" }
236
+ end
237
+ rescue => e
238
+ @logger.error { "[LDClient] Initializer failed with exception: #{e.message}" }
239
+ end
240
+ end
241
+ end
242
+
243
+ #
244
+ # Run synchronizers to keep data up-to-date.
245
+ #
246
+ # @return [void]
247
+ #
248
+ def run_synchronizers
249
+ # If no primary synchronizer configured, just set ready and return
250
+ if @primary_synchronizer_builder.nil?
251
+ @ready_event.set
252
+ return
253
+ end
254
+
255
+ # Start synchronizer loop in a separate thread
256
+ sync_thread = Thread.new { synchronizer_loop }
257
+ sync_thread.name = "FDv2-synchronizers"
258
+ @threads << sync_thread
259
+ end
260
+
261
+ #
262
+ # Synchronizer loop that manages primary/secondary/fallback synchronizers.
263
+ #
264
+ # @return [void]
265
+ #
266
+ def synchronizer_loop
267
+ begin
268
+ while !@stop_event.set? && @primary_synchronizer_builder
269
+ # Try primary synchronizer
270
+ begin
271
+ @lock.synchronize do
272
+ primary_sync = @primary_synchronizer_builder.call(@sdk_key, @config)
273
+ if primary_sync.respond_to?(:set_diagnostic_accumulator) && @diagnostic_accumulator
274
+ primary_sync.set_diagnostic_accumulator(@diagnostic_accumulator)
275
+ end
276
+ @active_synchronizer = primary_sync
277
+ end
278
+
279
+ @logger.info { "[LDClient] Primary synchronizer #{@active_synchronizer.name} is starting" }
280
+
281
+ remove_sync, fallback_v1 = consume_synchronizer_results(
282
+ @active_synchronizer,
283
+ method(:fallback_condition)
284
+ )
285
+
286
+ if remove_sync
287
+ @primary_synchronizer_builder = fallback_v1 ? @fdv1_fallback_synchronizer_builder : @secondary_synchronizer_builder
288
+ @secondary_synchronizer_builder = nil
289
+
290
+ if @primary_synchronizer_builder.nil?
291
+ @logger.warn { "[LDClient] No more synchronizers available" }
292
+ @data_source_status_provider.update_status(
293
+ LaunchDarkly::Interfaces::DataSource::Status::OFF,
294
+ @data_source_status_provider.status.last_error
295
+ )
296
+ break
297
+ end
298
+ else
299
+ @logger.info { "[LDClient] Fallback condition met" }
300
+ end
301
+
302
+ break if @stop_event.set?
303
+
304
+ next if @secondary_synchronizer_builder.nil?
305
+
306
+ @lock.synchronize do
307
+ secondary_sync = @secondary_synchronizer_builder.call(@sdk_key, @config)
308
+ if secondary_sync.respond_to?(:set_diagnostic_accumulator) && @diagnostic_accumulator
309
+ secondary_sync.set_diagnostic_accumulator(@diagnostic_accumulator)
310
+ end
311
+ @logger.info { "[LDClient] Secondary synchronizer #{secondary_sync.name} is starting" }
312
+ @active_synchronizer = secondary_sync
313
+ end
314
+
315
+ remove_sync, fallback_v1 = consume_synchronizer_results(
316
+ @active_synchronizer,
317
+ method(:recovery_condition)
318
+ )
319
+
320
+ if remove_sync
321
+ @secondary_synchronizer_builder = nil
322
+ @primary_synchronizer_builder = @fdv1_fallback_synchronizer_builder if fallback_v1
323
+
324
+ if @primary_synchronizer_builder.nil?
325
+ @logger.warn { "[LDClient] No more synchronizers available" }
326
+ @data_source_status_provider.update_status(
327
+ LaunchDarkly::Interfaces::DataSource::Status::OFF,
328
+ @data_source_status_provider.status.last_error
329
+ )
330
+ break
331
+ end
332
+ end
333
+
334
+ @logger.info { "[LDClient] Recovery condition met, returning to primary synchronizer" }
335
+ rescue => e
336
+ @logger.error { "[LDClient] Failed to build synchronizer: #{e.message}" }
337
+ break
338
+ end
339
+ end
340
+ rescue => e
341
+ @logger.error { "[LDClient] Error in synchronizer loop: #{e.message}" }
342
+ ensure
343
+ # Ensure we always set the ready event when exiting
344
+ @ready_event.set
345
+ @lock.synchronize do
346
+ @active_synchronizer&.stop
347
+ @active_synchronizer = nil
348
+ end
349
+ end
350
+ end
351
+
352
+ #
353
+ # Consume results from a synchronizer until a condition is met or it fails.
354
+ #
355
+ # @param synchronizer [Object] The synchronizer
356
+ # @param condition_func [Proc] Function to check if condition is met
357
+ # @return [Array(Boolean, Boolean)] [should_remove_sync, fallback_to_fdv1]
358
+ #
359
+ def consume_synchronizer_results(synchronizer, condition_func)
360
+ action_queue = Queue.new
361
+ timer = LaunchDarkly::Impl::RepeatingTask.new(10, 10, -> { action_queue.push("check") }, @logger, "FDv2-sync-cond-timer")
362
+
363
+ # Start reader thread
364
+ sync_reader = Thread.new do
365
+ begin
366
+ synchronizer.sync(@store) do |update|
367
+ action_queue.push(update)
368
+ end
369
+ ensure
370
+ action_queue.push("quit")
371
+ end
372
+ end
373
+ sync_reader.name = "FDv2-sync-reader"
374
+
375
+ begin
376
+ timer.start
377
+
378
+ loop do
379
+ update = action_queue.pop
380
+
381
+ if update.is_a?(String)
382
+ break if update == "quit"
383
+
384
+ if update == "check"
385
+ # Check condition periodically
386
+ current_status = @data_source_status_provider.status
387
+ return [false, false] if condition_func.call(current_status)
388
+ end
389
+ next
390
+ end
391
+
392
+ @logger.info { "[LDClient] Synchronizer #{synchronizer.name} update: #{update.state}" }
393
+ return [false, false] if @stop_event.set?
394
+
395
+ # Handle the update
396
+ @store.apply(update.change_set, true) if update.change_set
397
+
398
+ # Set ready event on valid update
399
+ @ready_event.set if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
400
+
401
+ # Update status
402
+ @data_source_status_provider.update_status(update.state, update.error)
403
+
404
+ # Check if we should revert to FDv1 immediately
405
+ return [true, true] if update.revert_to_fdv1
406
+
407
+ # Check for OFF state indicating permanent failure
408
+ return [true, false] if update.state == LaunchDarkly::Interfaces::DataSource::Status::OFF
409
+ end
410
+ rescue => e
411
+ @logger.error { "[LDClient] Error consuming synchronizer results: #{e.message}" }
412
+ return [true, false]
413
+ ensure
414
+ synchronizer.stop
415
+ timer.stop
416
+ sync_reader.join(0.5) if sync_reader.alive?
417
+ end
418
+
419
+ [true, false]
420
+ end
421
+
422
+ #
423
+ # Determine if we should fallback to secondary synchronizer.
424
+ #
425
+ # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
426
+ # @return [Boolean] true if fallback condition is met
427
+ #
428
+ def fallback_condition(status)
429
+ interrupted_at_runtime = status.state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
430
+ Time.now - status.state_since > 60 # 1 minute
431
+ cannot_initialize = status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING &&
432
+ Time.now - status.state_since > 10 # 10 seconds
433
+
434
+ interrupted_at_runtime || cannot_initialize
435
+ end
436
+
437
+ #
438
+ # Determine if we should try to recover to primary synchronizer.
439
+ #
440
+ # @param status [LaunchDarkly::Interfaces::DataSource::Status] Current data source status
441
+ # @return [Boolean] true if recovery condition is met
442
+ #
443
+ def recovery_condition(status)
444
+ interrupted_at_runtime = status.state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
445
+ Time.now - status.state_since > 60 # 1 minute
446
+ healthy_for_too_long = status.state == LaunchDarkly::Interfaces::DataSource::Status::VALID &&
447
+ Time.now - status.state_since > 300 # 5 minutes
448
+ cannot_initialize = status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING &&
449
+ Time.now - status.state_since > 10 # 10 seconds
450
+
451
+ interrupted_at_runtime || healthy_for_too_long || cannot_initialize
452
+ end
453
+
454
+ #
455
+ # Monitor the data store status. If the store comes online and
456
+ # potentially has stale data, we should write our known state to it.
457
+ #
458
+ # @param data_store_status [LaunchDarkly::Interfaces::DataStore::Status] The store status
459
+ # @return [void]
460
+ #
461
+ def persistent_store_outage_recovery(data_store_status)
462
+ return unless data_store_status.available
463
+ return unless data_store_status.stale
464
+
465
+ err = @store.commit
466
+ @logger.error { "[LDClient] Failed to reinitialize data store: #{err.message}" } if err
467
+ end
468
+ end
469
+ end
470
+ end
471
+ end