launchdarkly-server-sdk 8.13.0-java → 8.15.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/ldclient-rb/config.rb +24 -3
  4. data/lib/ldclient-rb/data_system/polling_data_source_builder.rb +7 -5
  5. data/lib/ldclient-rb/impl/context_filter.rb +7 -7
  6. data/lib/ldclient-rb/impl/data_source/polling.rb +14 -1
  7. data/lib/ldclient-rb/impl/data_source/requestor.rb +13 -3
  8. data/lib/ldclient-rb/impl/data_source/stream.rb +2 -0
  9. data/lib/ldclient-rb/impl/data_source.rb +37 -0
  10. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +12 -0
  11. data/lib/ldclient-rb/impl/data_store/store.rb +24 -0
  12. data/lib/ldclient-rb/impl/data_system/fdv1.rb +5 -0
  13. data/lib/ldclient-rb/impl/data_system/fdv2.rb +92 -13
  14. data/lib/ldclient-rb/impl/data_system/polling.rb +100 -33
  15. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +3 -29
  16. data/lib/ldclient-rb/impl/data_system/streaming.rb +54 -31
  17. data/lib/ldclient-rb/impl/data_system.rb +15 -5
  18. data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +29 -22
  19. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +4 -0
  20. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +53 -48
  21. data/lib/ldclient-rb/integrations/consul.rb +6 -2
  22. data/lib/ldclient-rb/integrations/dynamodb.rb +6 -2
  23. data/lib/ldclient-rb/integrations/file_data.rb +1 -4
  24. data/lib/ldclient-rb/integrations/redis.rb +6 -2
  25. data/lib/ldclient-rb/integrations/test_data_v2.rb +0 -3
  26. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +39 -19
  27. data/lib/ldclient-rb/interfaces/data_system.rb +60 -57
  28. data/lib/ldclient-rb/interfaces/hooks.rb +11 -1
  29. data/lib/ldclient-rb/ldclient.rb +1 -1
  30. data/lib/ldclient-rb/util.rb +3 -2
  31. data/lib/ldclient-rb/version.rb +1 -1
  32. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94c8797b35ad5d1abce1827cd6eaf48961856cf8102eabe2d9de1c674c17405a
4
- data.tar.gz: 7f02d923d1522f3976ee5ab457379dd5f40992539894175ce93a54083d107bca
3
+ metadata.gz: dc18b4c7af16130b9f7e9f7c8d0a4acd8e562d16ebb7714fc7846c9ff96d1d3f
4
+ data.tar.gz: 2c9bfdff939fb1f11a9bc83e707aa0e550136004c4a2870b7220fb293ad268f2
5
5
  SHA512:
6
- metadata.gz: 7fde3a50440b01752b84cebe45fbcb660258252ba1d162fbfc20fb5131686751de4c917ce1e61441d0981b606f4a44091a4724792019476bf27dd852a68877b6
7
- data.tar.gz: f1efa40715fb7909b2793d89de9e4f957f8ac468c5865c4fa46a0b111c0544cb627fc9ec0c0c477ea6718eac2ff18decd0d3d54b0c7edcacf7168c23d8f2343c
6
+ metadata.gz: 0b23cf9eda9b570844be07e71831f19734881b72d08c464b109929b17d225ac2083d78ead046418bdfec68e67db82f940c344efd3ce408ad1a1ea49e0d3dcaf2
7
+ data.tar.gz: ceb93287c34afbbb2301d3991267a10b1349aa126bea72c028552c431dc3493ec5f136d06b79f21641363dd9bfa87cce1a5ceb225534ff98ab8ad3088c2d7fc0
data/README.md CHANGED
@@ -43,7 +43,7 @@ We encourage pull requests and other contributions from the community. Check out
43
43
  Verifying SDK build provenance with the SLSA framework
44
44
  ------------
45
45
 
46
- LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md).
46
+ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md).
47
47
 
48
48
  About LaunchDarkly
49
49
  -----------
@@ -399,6 +399,30 @@ module LaunchDarkly
399
399
  #
400
400
  attr_reader :wrapper_version
401
401
 
402
+ #
403
+ # Returns a copy of this configuration with different wrapper information.
404
+ #
405
+ # This is intended for use by wrapper libraries, such as the LaunchDarkly OpenFeature provider, which need to
406
+ # identify themselves rather than the application which created the configuration.
407
+ #
408
+ # The copy is shallow: it shares the objects the original configuration references, such as the feature store,
409
+ # the logger, and the socket factory. Mutating one of those objects affects both configurations.
410
+ #
411
+ # @param wrapper_name [String, nil] see {#wrapper_name}
412
+ # @param wrapper_version [String, nil] see {#wrapper_version}
413
+ #
414
+ # @return [LaunchDarkly::Config]
415
+ #
416
+ def with_wrapper_information(wrapper_name, wrapper_version = nil)
417
+ updated = dup
418
+ updated.wrapper_name = wrapper_name
419
+ updated.wrapper_version = wrapper_version
420
+
421
+ updated
422
+ end
423
+
424
+ protected attr_writer :wrapper_name, :wrapper_version
425
+
402
426
  #
403
427
  # The factory used to construct sockets for HTTP operations. The factory must
404
428
  # provide the method `open(uri, timeout)`. The `open` method must return a
@@ -697,9 +721,6 @@ module LaunchDarkly
697
721
  #
698
722
  # Configuration for LaunchDarkly's data acquisition strategy.
699
723
  #
700
- # This is not stable and is not subject to any backwards compatibility guarantees
701
- # or semantic versioning. It is not suitable for production usage.
702
- #
703
724
  class DataSystemConfig
704
725
  #
705
726
  # @param initializers [Array<#build(String, Config)>, nil] The (optional) array of builders
@@ -21,9 +21,9 @@ module LaunchDarkly
21
21
  # include LaunchDarkly::DataSystem::Requester
22
22
  #
23
23
  # def fetch(selector)
24
- # # Fetch data and return a Result containing [ChangeSet, headers]
25
- # # ...
26
- # LaunchDarkly::Result.success([change_set, {}])
24
+ # # Fetch data and return a Result whose value is a ChangeSet and
25
+ # # whose headers carry any response metadata (e.g. directives).
26
+ # LaunchDarkly::Result.success(change_set, response_headers)
27
27
  # end
28
28
  #
29
29
  # def stop
@@ -43,8 +43,10 @@ module LaunchDarkly
43
43
  # @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
44
44
  # The selector describing what data to fetch. May be nil if no
45
45
  # selector is available (e.g., on the first request).
46
- # @return [LaunchDarkly::Result] A Result containing a tuple of
47
- # [ChangeSet, headers] on success, or an error message on failure.
46
+ # @return [LaunchDarkly::Result] A Result whose `value` is a
47
+ # {LaunchDarkly::Interfaces::DataSystem::ChangeSet} on success and
48
+ # whose `headers` carry response headers in either case (so callers
49
+ # can inspect directives such as `X-LD-FD-Fallback`).
48
50
  #
49
51
  def fetch(selector)
50
52
  raise NotImplementedError
@@ -70,15 +70,15 @@ module LaunchDarkly
70
70
  filtered[:anonymous] = true if anonymous
71
71
 
72
72
  redacted = []
73
- private_attributes = @private_attributes.concat(context.private_attributes)
73
+ private_attributes = @private_attributes + context.private_attributes
74
74
 
75
75
  name = context.get_value(:name)
76
- if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted, anonymous && redact_anonymous)
76
+ if !name.nil? && !check_whole_attribute_private(Reference.create_literal(:name), private_attributes, redacted, anonymous && redact_anonymous)
77
77
  filtered[:name] = name
78
78
  end
79
79
 
80
80
  context.get_custom_attribute_names.each do |attribute|
81
- unless check_whole_attribute_private(attribute, private_attributes, redacted, anonymous && redact_anonymous)
81
+ unless check_whole_attribute_private(Reference.create_literal(attribute), private_attributes, redacted, anonymous && redact_anonymous)
82
82
  value = context.get_value(attribute)
83
83
  filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted)
84
84
  end
@@ -92,7 +92,7 @@ module LaunchDarkly
92
92
  #
93
93
  # Check if an entire attribute should be redacted.
94
94
  #
95
- # @param attribute [Symbol]
95
+ # @param attribute [Reference]
96
96
  # @param private_attributes [Array<Reference>]
97
97
  # @param redacted [Array<Symbol>]
98
98
  # @param redact_all [Boolean]
@@ -100,13 +100,13 @@ module LaunchDarkly
100
100
  #
101
101
  private def check_whole_attribute_private(attribute, private_attributes, redacted, redact_all)
102
102
  if @all_attributes_private || redact_all
103
- redacted << attribute
103
+ redacted << attribute.raw_path.to_sym
104
104
  return true
105
105
  end
106
106
 
107
107
  private_attributes.each do |private_attribute|
108
- if private_attribute.component(0) == attribute && private_attribute.depth == 1
109
- redacted << attribute
108
+ if private_attribute.component(0) == attribute.component(0) && private_attribute.depth == 1
109
+ redacted << attribute.raw_path.to_sym
110
110
  return true
111
111
  end
112
112
  end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/data_source"
1
2
  require "ldclient-rb/impl/repeating_task"
2
3
  require "ldclient-rb/impl/util"
3
4
 
@@ -35,7 +36,8 @@ module LaunchDarkly
35
36
 
36
37
  def poll
37
38
  begin
38
- all_data = @requestor.request_all_data
39
+ all_data, headers = request_all_data
40
+ DataSource.record_environment_id(@config.data_source_update_sink, headers)
39
41
  if all_data
40
42
  update_sink_or_data_store.init(all_data)
41
43
  if @initialized.make_true
@@ -92,6 +94,17 @@ module LaunchDarkly
92
94
  @config.data_source_update_sink || @config.feature_store
93
95
  end
94
96
 
97
+ #
98
+ # Requestors provided by application code may not be able to report the response headers.
99
+ #
100
+ # @return [Array(Hash, HTTP::Headers, nil)]
101
+ #
102
+ private def request_all_data
103
+ return @requestor.request_all_data_with_headers if @requestor.respond_to?(:request_all_data_with_headers)
104
+
105
+ [@requestor.request_all_data, nil]
106
+ end
107
+
95
108
  #
96
109
  # @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
97
110
  #
@@ -40,8 +40,18 @@ module LaunchDarkly
40
40
  end
41
41
 
42
42
  def request_all_data()
43
- all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
44
- Impl::Model.make_all_store_data(all_data, @config.logger)
43
+ request_all_data_with_headers.first
44
+ end
45
+
46
+ #
47
+ # Requests the full data set, also returning the headers of the response it was retrieved from.
48
+ #
49
+ # @return [Array(Hash, HTTP::Headers)]
50
+ #
51
+ def request_all_data_with_headers
52
+ body, headers = make_request("/sdk/latest-all")
53
+ all_data = JSON.parse(body, symbolize_names: true)
54
+ [Impl::Model.make_all_store_data(all_data, @config.logger), headers]
45
55
  end
46
56
 
47
57
  def stop
@@ -80,7 +90,7 @@ module LaunchDarkly
80
90
  etag = response.headers["etag"]
81
91
  @cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
82
92
  end
83
- body
93
+ [body, response.headers]
84
94
  end
85
95
 
86
96
  def fix_encoding(body, content_type)
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/data_source"
1
2
  require "ldclient-rb/impl/model/serialization"
2
3
  require "ldclient-rb/impl/util"
3
4
  require "ldclient-rb/in_memory_store"
@@ -54,6 +55,7 @@ module LaunchDarkly
54
55
 
55
56
  uri = Impl::Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
56
57
  @es = SSE::Client.new(uri, **opts) do |conn|
58
+ conn.on_connect { |response_headers| DataSource.record_environment_id(@data_source_update_sink, response_headers) }
57
59
  conn.on_event { |event| process_message(event) }
58
60
  conn.on_error { |err|
59
61
  log_connection_result(false)
@@ -7,6 +7,27 @@ require "set"
7
7
  module LaunchDarkly
8
8
  module Impl
9
9
  module DataSource
10
+ LD_ENVID_HEADER = "X-LD-EnvID"
11
+
12
+ #
13
+ # Records the environment ID reported by LaunchDarkly, when the response headers provide one and the sink is
14
+ # able to hold it. Externally implemented sinks are not required to support this.
15
+ #
16
+ # @param sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil]
17
+ # @param headers [#[], nil]
18
+ # @return [void]
19
+ #
20
+ def self.record_environment_id(sink, headers)
21
+ return if headers.nil? || !sink.respond_to?(:set_environment_id)
22
+
23
+ environment_id = headers[LD_ENVID_HEADER]
24
+ # The http gem returns arrays for repeated headers; normalize to a string.
25
+ environment_id = environment_id.first if environment_id.is_a?(Array)
26
+ return unless environment_id.is_a?(String) && !environment_id.empty?
27
+
28
+ sink.set_environment_id(environment_id)
29
+ end
30
+
10
31
  class StatusProvider
11
32
  include LaunchDarkly::Interfaces::DataSource::StatusProvider
12
33
 
@@ -41,12 +62,28 @@ module LaunchDarkly
41
62
  @dependency_tracker = LaunchDarkly::Impl::DependencyTracker.new
42
63
 
43
64
  @mutex = Mutex.new
65
+ @environment_id = nil
44
66
  @current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
45
67
  LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
46
68
  Time.now,
47
69
  nil)
48
70
  end
49
71
 
72
+ #
73
+ # @return [String, nil] The environment ID reported by LaunchDarkly, if known
74
+ #
75
+ def environment_id
76
+ @mutex.synchronize { @environment_id }
77
+ end
78
+
79
+ #
80
+ # @param environment_id [String]
81
+ # @return [void]
82
+ #
83
+ def set_environment_id(environment_id)
84
+ @mutex.synchronize { @environment_id = environment_id }
85
+ end
86
+
50
87
  def init(all_data)
51
88
  old_data = nil
52
89
  monitor_store_update do
@@ -65,6 +65,18 @@ module LaunchDarkly
65
65
  @store.initialized?
66
66
  end
67
67
 
68
+ #
69
+ # Passthrough for the FDv2 store coordinator. If the wrapped store supports
70
+ # disabling its cache (e.g. {LaunchDarkly::Integrations::Util::CachingStoreWrapper}),
71
+ # forward the call; otherwise no-op.
72
+ #
73
+ # @return [void]
74
+ #
75
+ def disable_cache
76
+ return unless @store.respond_to?(:disable_cache)
77
+ wrapper { @store.disable_cache }
78
+ end
79
+
68
80
  # (see LaunchDarkly::Interfaces::FeatureStore#stop)
69
81
  def stop
70
82
  poller_to_stop = nil
@@ -221,6 +221,12 @@ module LaunchDarkly
221
221
  # Switch to memory store as active
222
222
  @active_store = @memory_store
223
223
 
224
+ # In-memory store is now authoritative. Disable the persistent-store cache
225
+ # so we don't hold a duplicate copy of every flag. Done before the
226
+ # persistent_store.init below so the wrapper can skip its cache-population
227
+ # loop on this very call.
228
+ disable_persistent_cache
229
+
224
230
  # Persist to persistent store if configured and writable
225
231
  @persistent_store.init(collections) if should_persist?
226
232
 
@@ -271,6 +277,24 @@ module LaunchDarkly
271
277
  send_change_events(affected_items) unless affected_items.empty?
272
278
  end
273
279
 
280
+ #
281
+ # Disable the persistent store's in-memory cache, if it has one. The FDv2 in-memory
282
+ # store is now the source of truth, so the cache is dead weight and would just
283
+ # hold a duplicate copy of every flag.
284
+ #
285
+ # @return [void]
286
+ #
287
+ private def disable_persistent_cache
288
+ return if @persistent_store.nil?
289
+ return unless @persistent_store.respond_to?(:disable_cache)
290
+
291
+ begin
292
+ @persistent_store.disable_cache
293
+ rescue => e
294
+ @logger.warn { "[LDClient] Failed to disable persistent store cache: #{e.message}" }
295
+ end
296
+ end
297
+
274
298
  #
275
299
  # Returns whether data should be persisted to the persistent store.
276
300
  #
@@ -121,6 +121,11 @@ module LaunchDarkly
121
121
  @flag_change_broadcaster
122
122
  end
123
123
 
124
+ # (see DataSystem#environment_id)
125
+ def environment_id
126
+ @data_source_update_sink.environment_id
127
+ end
128
+
124
129
  #
125
130
  # (see DataSystem#data_availability)
126
131
  #
@@ -100,6 +100,9 @@ module LaunchDarkly
100
100
  @store.with_persistence(wrapper, writable, @data_store_status_provider)
101
101
  end
102
102
 
103
+ @environment_id = nil
104
+ @environment_id_lock = Mutex.new
105
+
103
106
  # Threading
104
107
  @stop_event = Concurrent::Event.new
105
108
  @ready_event = Concurrent::Event.new
@@ -185,6 +188,11 @@ module LaunchDarkly
185
188
  @flag_change_broadcaster
186
189
  end
187
190
 
191
+ # (see DataSystem#environment_id)
192
+ def environment_id
193
+ @environment_id_lock.synchronize { @environment_id }
194
+ end
195
+
188
196
  # (see DataSystem#data_availability)
189
197
  def data_availability
190
198
  return DataAvailability::REFRESHED if @store.selector.defined?
@@ -214,8 +222,23 @@ module LaunchDarkly
214
222
  nil
215
223
  )
216
224
 
217
- # Run initializers first
218
- run_initializers
225
+ # Run initializers first. If an initializer signals the
226
+ # server-directed FDv1 Fallback Directive, switch terminally to
227
+ # the FDv1 Fallback Synchronizer (or transition to OFF if none
228
+ # is configured) before entering the synchronizer phase.
229
+ if run_initializers
230
+ if @fdv1_fallback_synchronizer_builder
231
+ @logger.warn { "[LDClient] Falling back to FDv1 protocol" }
232
+ @synchronizer_builders = [@fdv1_fallback_synchronizer_builder]
233
+ else
234
+ @logger.warn { "[LDClient] Initializer requested FDv1 fallback but none configured" }
235
+ @synchronizer_builders = []
236
+ @data_source_status_provider.update_status(
237
+ LaunchDarkly::Interfaces::DataSource::Status::OFF,
238
+ @data_source_status_provider.status.last_error
239
+ )
240
+ end
241
+ end
219
242
 
220
243
  # Run synchronizers
221
244
  run_synchronizers
@@ -228,39 +251,71 @@ module LaunchDarkly
228
251
  #
229
252
  # Run initializers to get initial data.
230
253
  #
231
- # @return [void]
254
+ # Each initializer is tried in order until one succeeds, the system
255
+ # is stopped, or an initializer signals the server-directed FDv1
256
+ # Fallback Directive. When fallback is signalled alongside a valid
257
+ # payload, that payload is applied before returning so evaluations
258
+ # can serve the server-provided data while the FDv1 synchronizer
259
+ # spins up. The method returns true when fallback was requested so
260
+ # that the caller can switch the synchronizer list.
261
+ #
262
+ # @return [Boolean] true when an initializer requested FDv1 fallback.
232
263
  #
233
264
  def run_initializers
234
- return unless @data_system_config.initializers
265
+ return false unless @data_system_config.initializers
235
266
 
236
267
  @data_system_config.initializers.each do |initializer_builder|
237
- return if @stop_event.set?
268
+ return false if @stop_event.set?
238
269
 
239
270
  begin
240
271
  initializer = initializer_builder.build(@sdk_key, @config)
241
272
  @logger.info { "[LDClient] Attempting to initialize via #{initializer.name}" }
242
273
 
243
- basis_result = initializer.fetch(@store)
274
+ fetch_result = initializer.fetch(@store)
275
+ fallback = fetch_result.fallback_to_fdv1
276
+ basis_result = fetch_result.result
244
277
 
245
278
  if basis_result.success?
246
279
  basis = basis_result.value
247
280
  @logger.info { "[LDClient] Initialized via #{initializer.name}" }
281
+ record_environment_id(basis.environment_id)
248
282
 
249
- # Apply the basis to the store
283
+ # Apply the basis to the store regardless of whether fallback was signalled.
284
+ # If the server returned a valid payload alongside the directive we still want
285
+ # evaluations to serve that data while the FDv1 synchronizer spins up.
250
286
  @store.apply(basis.change_set, basis.persist)
251
287
 
252
- # Set ready event if and only if a selector is defined for the changeset
288
+ # Set ready event if and only if a selector is defined for the changeset.
253
289
  if basis.change_set.selector && basis.change_set.selector.defined?
254
290
  @ready_event.set
255
- return
291
+ return fallback
256
292
  end
257
293
  else
258
294
  @logger.warn { "[LDClient] Initializer #{initializer.name} failed: #{basis_result.error}" }
295
+ if fallback
296
+ # Record the underlying initializer error so that, if no FDv1 fallback is
297
+ # configured, the subsequent transition to OFF carries it as last_error.
298
+ @data_source_status_provider.update_status(
299
+ LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
300
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
301
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
302
+ 0,
303
+ basis_result.error || "",
304
+ Time.now
305
+ )
306
+ )
307
+ end
259
308
  end
309
+
310
+ # The fallback directive takes precedence over the regular failover algorithm,
311
+ # so do not fall through to the next initializer when it is set.
312
+ return true if fallback
260
313
  rescue => e
261
314
  @logger.error { "[LDClient] Initializer failed with exception: #{e.message}" }
262
315
  end
263
316
  end
317
+
318
+ false
264
319
  end
265
320
 
266
321
  #
@@ -313,12 +368,21 @@ module LaunchDarkly
313
368
  case sync_result
314
369
  when SyncResult::FDV1
315
370
  if @fdv1_fallback_synchronizer_builder
371
+ @logger.warn { "[LDClient] Falling back to FDv1 protocol" }
316
372
  @synchronizer_builders = [@fdv1_fallback_synchronizer_builder]
317
373
  current_index = 0
318
374
  next
319
375
  end
320
- # No FDv1 fallback configured, treat as regular fallback
321
- current_index += 1
376
+ # No FDv1 fallback configured: the data system must HALT
377
+ # rather than fall through to the next FDv2 synchronizer.
378
+ # Continuing to retry would reopen the connection that just
379
+ # delivered the directive.
380
+ @logger.warn { "[LDClient] Synchronizer requested FDv1 fallback but none configured; halting data system" }
381
+ @data_source_status_provider.update_status(
382
+ LaunchDarkly::Interfaces::DataSource::Status::OFF,
383
+ @data_source_status_provider.status.last_error
384
+ )
385
+ break
322
386
  when SyncResult::RECOVER
323
387
  @logger.info { "[LDClient] Recovery condition met, returning to primary synchronizer" }
324
388
  current_index = 0
@@ -405,12 +469,15 @@ module LaunchDarkly
405
469
  @store.apply(update.change_set, true) if update.change_set
406
470
 
407
471
  # Set ready event on valid update
408
- @ready_event.set if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
472
+ if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
473
+ @ready_event.set
474
+ record_environment_id(update.environment_id)
475
+ end
409
476
 
410
477
  # Update status
411
478
  @data_source_status_provider.update_status(update.state, update.error)
412
479
 
413
- return SyncResult::FDV1 if update.revert_to_fdv1
480
+ return SyncResult::FDV1 if update.fallback_to_fdv1
414
481
 
415
482
  return SyncResult::REMOVE if update.state == LaunchDarkly::Interfaces::DataSource::Status::OFF
416
483
  end
@@ -426,6 +493,18 @@ module LaunchDarkly
426
493
  SyncResult::REMOVE
427
494
  end
428
495
 
496
+ #
497
+ # Retains the environment ID reported by a successful connection to LaunchDarkly.
498
+ #
499
+ # @param environment_id [String, nil]
500
+ # @return [void]
501
+ #
502
+ def record_environment_id(environment_id)
503
+ return unless environment_id.is_a?(String) && !environment_id.empty?
504
+
505
+ @environment_id_lock.synchronize { @environment_id = environment_id }
506
+ end
507
+
429
508
  #
430
509
  # Determine if we should fallback to the next synchronizer.
431
510
  #