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
@@ -21,6 +21,60 @@ module LaunchDarkly
21
21
  LD_ENVID_HEADER = "X-LD-EnvID"
22
22
  LD_FD_FALLBACK_HEADER = "X-LD-FD-Fallback"
23
23
 
24
+ #
25
+ # Reports whether the response headers signal that the SDK should fall
26
+ # back to the FDv1 protocol. Lookup is case-insensitive so callers do
27
+ # not need to know whether the header map preserves canonical casing
28
+ # (e.g. {HTTP::Headers}) or has been normalized to lowercase (e.g.
29
+ # {HTTPPollingRequester#fetch}).
30
+ #
31
+ # @param headers [#[], Hash, nil]
32
+ # @return [Boolean]
33
+ #
34
+ def self.fdv1_fallback_requested?(headers)
35
+ return false if headers.nil?
36
+ value = lookup_header(headers, LD_FD_FALLBACK_HEADER)
37
+ # http gem returns arrays for repeated headers; normalize to a string.
38
+ value = value.first if value.is_a?(Array)
39
+ value == 'true'
40
+ end
41
+
42
+ #
43
+ # Performs a case-insensitive header lookup that works with both
44
+ # case-insensitive header containers (e.g. `HTTP::Headers`) and plain
45
+ # Ruby hashes -- including hashes whose keys we have downcased
46
+ # ourselves before reaching this code path.
47
+ #
48
+ # @param headers [#[], Hash]
49
+ # @param name [String]
50
+ # @return [String, Array, nil]
51
+ #
52
+ def self.lookup_header(headers, name)
53
+ return nil if headers.nil?
54
+
55
+ if headers.is_a?(Hash)
56
+ # Plain hash: try canonical case, then exact lowercase, then a
57
+ # case-insensitive scan as a final fallback.
58
+ value = headers[name]
59
+ return value unless value.nil?
60
+
61
+ downcased = name.downcase
62
+ value = headers[downcased]
63
+ return value unless value.nil?
64
+
65
+ headers.each_pair do |key, val|
66
+ return val if key.to_s.downcase == downcased
67
+ end
68
+ return nil
69
+ end
70
+
71
+ # Non-hash container (e.g. HTTP::Headers). Lookup via [] is
72
+ # already case-insensitive on those types.
73
+ return headers[name] if headers.respond_to?(:[])
74
+
75
+ nil
76
+ end
77
+
24
78
  #
25
79
  # PollingDataSource is a data source that can retrieve information from
26
80
  # LaunchDarkly either as an Initializer or as a Synchronizer.
@@ -46,10 +100,12 @@ module LaunchDarkly
46
100
  end
47
101
 
48
102
  #
49
- # Fetch returns a Basis, or an error if the Basis could not be retrieved.
103
+ # Fetch returns a {LaunchDarkly::Interfaces::DataSystem::FetchResult}
104
+ # wrapping a Basis (or an error) and the FDv1 Fallback Directive
105
+ # signal carried on the server response.
50
106
  #
51
107
  # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
52
- # @return [LaunchDarkly::Interfaces::DataSystem::Basis, nil]
108
+ # @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
53
109
  #
54
110
  def fetch(ss)
55
111
  poll(ss)
@@ -71,16 +127,10 @@ module LaunchDarkly
71
127
 
72
128
  until @stop.set?
73
129
  result = @requester.fetch(ss.selector)
130
+ fallback = LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(result.headers)
131
+ envid = LaunchDarkly::Impl::DataSystem.lookup_header(result.headers, LD_ENVID_HEADER)
74
132
 
75
133
  if !result.success?
76
- fallback = false
77
- envid = nil
78
-
79
- if result.headers
80
- fallback = result.headers[LD_FD_FALLBACK_HEADER] == 'true'
81
- envid = result.headers[LD_ENVID_HEADER]
82
- end
83
-
84
134
  if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
85
135
  error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
86
136
  LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE,
@@ -99,7 +149,7 @@ module LaunchDarkly
99
149
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
100
150
  error: error_info,
101
151
  environment_id: envid,
102
- revert_to_fdv1: true
152
+ fallback_to_fdv1: true
103
153
  )
104
154
  break
105
155
  end
@@ -108,7 +158,7 @@ module LaunchDarkly
108
158
  state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
109
159
  error: error_info,
110
160
  environment_id: envid,
111
- revert_to_fdv1: false
161
+ fallback_to_fdv1: false
112
162
  )
113
163
  @interrupt_event.wait(@poll_interval)
114
164
  next
@@ -118,7 +168,7 @@ module LaunchDarkly
118
168
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
119
169
  error: error_info,
120
170
  environment_id: envid,
121
- revert_to_fdv1: fallback
171
+ fallback_to_fdv1: fallback
122
172
  )
123
173
  break
124
174
  end
@@ -136,7 +186,7 @@ module LaunchDarkly
136
186
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
137
187
  error: error_info,
138
188
  environment_id: envid,
139
- revert_to_fdv1: true
189
+ fallback_to_fdv1: true
140
190
  )
141
191
  break
142
192
  end
@@ -145,16 +195,14 @@ module LaunchDarkly
145
195
  state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
146
196
  error: error_info,
147
197
  environment_id: envid,
148
- revert_to_fdv1: false
198
+ fallback_to_fdv1: false
149
199
  )
150
200
  else
151
- change_set, headers = result.value
152
- fallback = headers[LD_FD_FALLBACK_HEADER] == 'true'
153
201
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
154
202
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
155
- change_set: change_set,
156
- environment_id: headers[LD_ENVID_HEADER],
157
- revert_to_fdv1: fallback
203
+ change_set: result.value,
204
+ environment_id: envid,
205
+ fallback_to_fdv1: fallback
158
206
  )
159
207
  end
160
208
 
@@ -177,10 +225,14 @@ module LaunchDarkly
177
225
 
178
226
  #
179
227
  # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
180
- # @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::Basis, String>]
228
+ # @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
181
229
  #
182
230
  private def poll(ss)
231
+ # Default to false so the rescue clause has a defined value even when an
232
+ # exception is raised before the header read below has had a chance to run.
233
+ fallback = false
183
234
  result = @requester.fetch(ss.selector)
235
+ fallback = LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(result.headers)
184
236
 
185
237
  unless result.success?
186
238
  if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
@@ -189,15 +241,20 @@ module LaunchDarkly
189
241
  status_code, "polling request", "will retry"
190
242
  )
191
243
  @logger.warn { "[LDClient] #{http_error_message_result}" } if Impl::Util.http_error_recoverable?(status_code)
192
- return LaunchDarkly::Result.fail(http_error_message_result, result.exception)
244
+ return LaunchDarkly::Interfaces::DataSystem::FetchResult.new(
245
+ result: LaunchDarkly::Result.fail(http_error_message_result, result.exception),
246
+ fallback_to_fdv1: fallback
247
+ )
193
248
  end
194
249
 
195
- return LaunchDarkly::Result.fail(result.error || 'Failed to request payload', result.exception)
250
+ return LaunchDarkly::Interfaces::DataSystem::FetchResult.new(
251
+ result: LaunchDarkly::Result.fail(result.error || 'Failed to request payload', result.exception),
252
+ fallback_to_fdv1: fallback
253
+ )
196
254
  end
197
255
 
198
- change_set, headers = result.value
199
-
200
- env_id = headers[LD_ENVID_HEADER]
256
+ change_set = result.value
257
+ env_id = LaunchDarkly::Impl::DataSystem.lookup_header(result.headers, LD_ENVID_HEADER)
201
258
  env_id = nil unless env_id.is_a?(String)
202
259
 
203
260
  basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
@@ -206,12 +263,22 @@ module LaunchDarkly
206
263
  environment_id: env_id
207
264
  )
208
265
 
209
- LaunchDarkly::Result.success(basis)
266
+ LaunchDarkly::Interfaces::DataSystem::FetchResult.new(
267
+ result: LaunchDarkly::Result.success(basis),
268
+ fallback_to_fdv1: fallback
269
+ )
210
270
  rescue => e
271
+ # An exception can fire after we have already read the response
272
+ # headers (e.g. a malformed change_set whose selector is nil makes
273
+ # change_set.selector.defined? raise). Carry the computed fallback
274
+ # signal through so the directive is not silently dropped.
211
275
  msg = "Error: Exception encountered when updating flags. #{e}"
212
276
  @logger.error { "[LDClient] #{msg}" }
213
277
  @logger.debug { "[LDClient] Exception trace: #{e.backtrace}" }
214
- LaunchDarkly::Result.fail(msg, e)
278
+ LaunchDarkly::Interfaces::DataSystem::FetchResult.new(
279
+ result: LaunchDarkly::Result.fail(msg, e),
280
+ fallback_to_fdv1: fallback
281
+ )
215
282
  end
216
283
  end
217
284
 
@@ -246,7 +313,7 @@ module LaunchDarkly
246
313
  query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
247
314
 
248
315
  if selector && selector.defined?
249
- query_params << ["selector", selector.state]
316
+ query_params << ["basis", selector.state]
250
317
  end
251
318
 
252
319
  uri = @poll_uri
@@ -273,7 +340,7 @@ module LaunchDarkly
273
340
  end
274
341
 
275
342
  if status == 304
276
- return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
343
+ return LaunchDarkly::Result.success(LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers)
277
344
  end
278
345
 
279
346
  body = response.to_s
@@ -285,7 +352,7 @@ module LaunchDarkly
285
352
 
286
353
  changeset_result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(data)
287
354
  if changeset_result.success?
288
- LaunchDarkly::Result.success([changeset_result.value, response_headers])
355
+ LaunchDarkly::Result.success(changeset_result.value, response_headers)
289
356
  else
290
357
  LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
291
358
  end
@@ -361,7 +428,7 @@ module LaunchDarkly
361
428
  end
362
429
 
363
430
  if status == 304
364
- return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
431
+ return LaunchDarkly::Result.success(LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers)
365
432
  end
366
433
 
367
434
  body = response.to_s
@@ -373,7 +440,7 @@ module LaunchDarkly
373
440
 
374
441
  changeset_result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data)
375
442
  if changeset_result.success?
376
- LaunchDarkly::Result.success([changeset_result.value, response_headers])
443
+ LaunchDarkly::Result.success(changeset_result.value, response_headers)
377
444
  else
378
445
  LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
379
446
  end
@@ -12,9 +12,6 @@ module LaunchDarkly
12
12
  #
13
13
  # DeleteObject specifies the deletion of a particular object.
14
14
  #
15
- # This type is not stable, and not subject to any backwards
16
- # compatibility guarantees or semantic versioning. It is not suitable for production usage.
17
- #
18
15
  class DeleteObject
19
16
  # @return [Integer] The version
20
17
  attr_reader :version
@@ -79,9 +76,6 @@ module LaunchDarkly
79
76
  #
80
77
  # PutObject specifies the addition of a particular object with upsert semantics.
81
78
  #
82
- # This type is not stable, and not subject to any backwards
83
- # compatibility guarantees or semantic versioning. It is not suitable for production usage.
84
- #
85
79
  class PutObject
86
80
  # @return [Integer] The version
87
81
  attr_reader :version
@@ -153,28 +147,15 @@ module LaunchDarkly
153
147
  #
154
148
  # Goodbye represents a goodbye event.
155
149
  #
156
- # This type is not stable, and not subject to any backwards
157
- # compatibility guarantees or semantic versioning. It is not suitable for production usage.
158
- #
159
150
  class Goodbye
160
151
  # @return [String] The reason for goodbye
161
152
  attr_reader :reason
162
153
 
163
- # @return [Boolean] Whether the goodbye is silent
164
- attr_reader :silent
165
-
166
- # @return [Boolean] Whether this represents a catastrophic failure
167
- attr_reader :catastrophe
168
-
169
154
  #
170
155
  # @param reason [String] The reason for goodbye
171
- # @param silent [Boolean] Whether the goodbye is silent
172
- # @param catastrophe [Boolean] Whether this represents a catastrophic failure
173
156
  #
174
- def initialize(reason:, silent:, catastrophe:)
157
+ def initialize(reason:)
175
158
  @reason = reason
176
- @silent = silent
177
- @catastrophe = catastrophe
178
159
  end
179
160
 
180
161
  #
@@ -185,8 +166,6 @@ module LaunchDarkly
185
166
  def to_h
186
167
  {
187
168
  reason: @reason,
188
- silent: @silent,
189
- catastrophe: @catastrophe,
190
169
  }
191
170
  end
192
171
 
@@ -199,21 +178,16 @@ module LaunchDarkly
199
178
  #
200
179
  def self.from_h(data)
201
180
  reason = data[:reason]
202
- silent = data[:silent]
203
- catastrophe = data[:catastrophe]
204
181
 
205
- raise ArgumentError, "Missing required fields in Goodbye" if reason.nil? || silent.nil? || catastrophe.nil?
182
+ raise ArgumentError, "Missing required fields in Goodbye" if reason.nil?
206
183
 
207
- new(reason: reason, silent: silent, catastrophe: catastrophe)
184
+ new(reason: reason)
208
185
  end
209
186
  end
210
187
 
211
188
  #
212
189
  # Error represents an error event.
213
190
  #
214
- # This type is not stable, and not subject to any backwards
215
- # compatibility guarantees or semantic versioning. It is not suitable for production usage.
216
- #
217
191
  class Error
218
192
  # @return [String] The payload ID
219
193
  attr_reader :payload_id
@@ -72,6 +72,25 @@ module LaunchDarkly
72
72
 
73
73
  change_set_builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
74
74
  envid = nil
75
+ # The FDv1 Fallback Directive is one-way and terminal: once any
76
+ # connect handshake within this sync invocation carries it, the SDK
77
+ # is committed to engaging FDv1 as soon as the next full payload
78
+ # has been applied. We therefore latch this flag to true on first
79
+ # observation and never reset it -- a mid-sync reconnect whose
80
+ # response no longer carries the directive does NOT cancel a
81
+ # directive seen earlier. This matches the Go and Python SDK
82
+ # implementations, both of which use the same latch pattern.
83
+ #
84
+ # The flag has to bridge two callbacks: on_connect sees the response
85
+ # headers but on_event does not. A local closed over by both blocks
86
+ # is correct because:
87
+ # 1. Scope -- bound to a single sync invocation, so a future sync
88
+ # starts fresh. (Within this invocation, persistence across
89
+ # reconnects is the intended semantics, per above.)
90
+ # 2. Thread safety -- ld-eventsource dispatches on_connect,
91
+ # on_event, and on_error on the same SSE worker thread, so
92
+ # reads and writes here are single-threaded by construction.
93
+ fdv1_fallback_pending = false
75
94
 
76
95
  base_uri = @http_config.base_uri + FDV2_STREAMING_ENDPOINT
77
96
  headers = Impl::Util.default_http_headers(@sdk_key, @config)
@@ -85,30 +104,32 @@ module LaunchDarkly
85
104
 
86
105
  @sse = SSE::Client.new(base_uri, **opts) do |client|
87
106
  client.on_connect do |headers|
88
- # Extract environment ID and check for fallback on successful connection
89
107
  if headers
90
- envid = headers[LD_ENVID_HEADER] || envid
91
-
92
- # Check for fallback header on connection
93
- if headers[LD_FD_FALLBACK_HEADER] == 'true'
94
- log_connection_result(true)
95
- yield LaunchDarkly::Interfaces::DataSystem::Update.new(
96
- state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
97
- revert_to_fdv1: true,
98
- environment_id: envid
99
- )
100
- stop
101
- end
108
+ # Per-environment identifier: server sends it on every connect,
109
+ # but it never changes once known so only assign once.
110
+ envid ||= LaunchDarkly::Impl::DataSystem.lookup_header(headers, LD_ENVID_HEADER)
111
+ fdv1_fallback_pending = true if LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(headers)
102
112
  end
103
113
  end
104
114
 
105
115
  client.on_event do |event|
106
116
  begin
107
- update = process_message(event, change_set_builder, envid)
108
- if update
109
- log_connection_result(true)
110
- @connection_attempt_start_time = 0
111
- yield update
117
+ update = process_message(event, change_set_builder, envid, fdv1_fallback_pending: fdv1_fallback_pending)
118
+ next unless update
119
+
120
+ log_connection_result(true)
121
+ @connection_attempt_start_time = 0
122
+
123
+ yield update
124
+
125
+ # When the FDv1 Fallback Directive rode along on a Valid update, close
126
+ # the stream so the primary synchronizer is stopped once the directive
127
+ # engages. process_message marks the Update with fallback_to_fdv1 only
128
+ # on payloads that complete a transfer, so the consumer has already
129
+ # applied the ChangeSet by the time we get here.
130
+ if update.fallback_to_fdv1
131
+ fdv1_fallback_pending = false
132
+ stop
112
133
  end
113
134
  rescue JSON::ParserError => e
114
135
  @logger.info { "[LDClient] Error parsing stream event; will restart stream: #{e}" }
@@ -147,13 +168,9 @@ module LaunchDarkly
147
168
  log_connection_result(false)
148
169
  fallback = false
149
170
 
150
- # Extract envid and fallback from error headers if available
151
171
  if error.respond_to?(:headers) && error.headers
152
- envid = error.headers[LD_ENVID_HEADER] || envid
153
-
154
- if error.headers[LD_FD_FALLBACK_HEADER] == 'true'
155
- fallback = true
156
- end
172
+ envid ||= LaunchDarkly::Impl::DataSystem.lookup_header(error.headers, LD_ENVID_HEADER)
173
+ fallback = true if LaunchDarkly::Impl::DataSystem.fdv1_fallback_requested?(error.headers)
157
174
  end
158
175
 
159
176
  update = handle_error(error, envid, fallback)
@@ -193,9 +210,15 @@ module LaunchDarkly
193
210
  # @param message [SSE::StreamEvent]
194
211
  # @param change_set_builder [LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder]
195
212
  # @param envid [String, nil]
213
+ # @param fdv1_fallback_pending [Boolean] true when the connect-time
214
+ # response headers carried the FDv1 Fallback Directive. When set,
215
+ # the next Update that completes a payload transfer (TRANSFER_NONE
216
+ # or PAYLOAD_TRANSFERRED) is marked with fallback_to_fdv1: true so
217
+ # the consumer can engage the FDv1 Fallback Synchronizer after
218
+ # applying the in-flight ChangeSet.
196
219
  # @return [LaunchDarkly::Interfaces::DataSystem::Update, nil]
197
220
  #
198
- private def process_message(message, change_set_builder, envid)
221
+ private def process_message(message, change_set_builder, envid, fdv1_fallback_pending: false)
199
222
  event_type = message.type
200
223
 
201
224
  # Handle heartbeat
@@ -214,7 +237,8 @@ module LaunchDarkly
214
237
  change_set_builder.expect_changes
215
238
  return LaunchDarkly::Interfaces::DataSystem::Update.new(
216
239
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
217
- environment_id: envid
240
+ environment_id: envid,
241
+ fallback_to_fdv1: fdv1_fallback_pending
218
242
  )
219
243
  end
220
244
  nil
@@ -231,9 +255,7 @@ module LaunchDarkly
231
255
 
232
256
  when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE
233
257
  goodbye = LaunchDarkly::Impl::DataSystem::ProtocolV2::Goodbye.from_h(JSON.parse(message.data, symbolize_names: true))
234
- unless goodbye.silent
235
- @logger.error { "[LDClient] SSE server received error: #{goodbye.reason} (catastrophe: #{goodbye.catastrophe})" }
236
- end
258
+ @logger.info { "[LDClient] SSE server received goodbye: #{goodbye.reason}" }
237
259
  nil
238
260
 
239
261
  when LaunchDarkly::Interfaces::DataSystem::EventName::ERROR
@@ -251,7 +273,8 @@ module LaunchDarkly
251
273
  LaunchDarkly::Interfaces::DataSystem::Update.new(
252
274
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
253
275
  change_set: change_set,
254
- environment_id: envid
276
+ environment_id: envid,
277
+ fallback_to_fdv1: fdv1_fallback_pending
255
278
  )
256
279
 
257
280
  else
@@ -286,7 +309,7 @@ module LaunchDarkly
286
309
  update = LaunchDarkly::Interfaces::DataSystem::Update.new(
287
310
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
288
311
  error: error_info,
289
- revert_to_fdv1: true,
312
+ fallback_to_fdv1: true,
290
313
  environment_id: envid
291
314
  )
292
315
  stop
@@ -119,6 +119,15 @@ module LaunchDarkly
119
119
  raise NotImplementedError, "#{self.class} must implement #set_diagnostic_accumulator"
120
120
  end
121
121
 
122
+ #
123
+ # Returns the ID of the environment the SDK is connected to, if LaunchDarkly has reported one.
124
+ #
125
+ # @return [String, nil]
126
+ #
127
+ def environment_id
128
+ raise NotImplementedError, "#{self.class} must implement #environment_id"
129
+ end
130
+
122
131
  #
123
132
  # Represents the availability of data in the SDK.
124
133
  #
@@ -247,8 +256,9 @@ module LaunchDarkly
247
256
  # @return [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
248
257
  attr_reader :error
249
258
 
250
- # @return [Boolean] Whether to revert to FDv1
251
- attr_reader :revert_to_fdv1
259
+ # @return [Boolean] Whether the LaunchDarkly server has instructed the SDK to
260
+ # fall back to the FDv1 protocol.
261
+ attr_reader :fallback_to_fdv1
252
262
 
253
263
  # @return [String, nil] The environment ID if available
254
264
  attr_reader :environment_id
@@ -257,14 +267,14 @@ module LaunchDarkly
257
267
  # @param state [Symbol] The state of the data source
258
268
  # @param change_set [ChangeSet, nil] The change set if available
259
269
  # @param error [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
260
- # @param revert_to_fdv1 [Boolean] Whether to revert to FDv1
270
+ # @param fallback_to_fdv1 [Boolean] Whether to fall back to FDv1
261
271
  # @param environment_id [String, nil] The environment ID if available
262
272
  #
263
- def initialize(state:, change_set: nil, error: nil, revert_to_fdv1: false, environment_id: nil)
273
+ def initialize(state:, change_set: nil, error: nil, fallback_to_fdv1: false, environment_id: nil)
264
274
  @state = state
265
275
  @change_set = change_set
266
276
  @error = error
267
- @revert_to_fdv1 = revert_to_fdv1
277
+ @fallback_to_fdv1 = fallback_to_fdv1
268
278
  @environment_id = environment_id
269
279
  end
270
280
  end
@@ -71,31 +71,38 @@ module LaunchDarkly
71
71
  # Implementation of the Initializer.fetch method.
72
72
  #
73
73
  # Reads all configured files once and returns their contents as a Basis.
74
+ # File-based data sources never request the FDv1 Fallback Directive,
75
+ # so the returned {FetchResult} always reports `fallback_to_fdv1: false`.
74
76
  #
75
77
  # @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
78
+ # @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
77
79
  #
78
80
  def fetch(selector_store)
79
- @lock.synchronize do
80
- if @closed
81
- return LaunchDarkly::Result.fail('FileDataV2 source has been closed')
82
- end
81
+ result =
82
+ begin
83
+ @lock.synchronize do
84
+ if @closed
85
+ next LaunchDarkly::Result.fail('FileDataV2 source has been closed')
86
+ end
83
87
 
84
- result = load_all_to_changeset
85
- return result unless result.success?
88
+ load_result = load_all_to_changeset
89
+ next load_result unless load_result.success?
86
90
 
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
- )
91
+ change_set = load_result.value
92
+ basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
93
+ change_set: change_set,
94
+ persist: false,
95
+ environment_id: nil
96
+ )
93
97
 
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)
98
+ LaunchDarkly::Result.success(basis)
99
+ end
100
+ rescue => e
101
+ @logger.error { "[LDClient] Error fetching file data: #{e.message}" }
102
+ LaunchDarkly::Result.fail("Error fetching file data: #{e.message}", e)
103
+ end
104
+
105
+ LaunchDarkly::Interfaces::DataSystem::FetchResult.new(result: result, fallback_to_fdv1: false)
99
106
  end
100
107
 
101
108
  #
@@ -110,14 +117,14 @@ module LaunchDarkly
110
117
  #
111
118
  def sync(selector_store)
112
119
  # First yield initial data
113
- initial_result = fetch(selector_store)
114
- unless initial_result.success?
120
+ initial_fetch = fetch(selector_store)
121
+ unless initial_fetch.success?
115
122
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
116
123
  state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
117
124
  error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
118
125
  LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
119
126
  0,
120
- initial_result.error,
127
+ initial_fetch.error,
121
128
  Time.now
122
129
  )
123
130
  )
@@ -126,7 +133,7 @@ module LaunchDarkly
126
133
 
127
134
  yield LaunchDarkly::Interfaces::DataSystem::Update.new(
128
135
  state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
129
- change_set: initial_result.value.change_set
136
+ change_set: initial_fetch.value.change_set
130
137
  )
131
138
 
132
139
  # Start watching for file changes
@@ -93,6 +93,10 @@ module LaunchDarkly
93
93
  def stop
94
94
  @wrapper.stop
95
95
  end
96
+
97
+ def disable_cache
98
+ @wrapper.disable_cache
99
+ end
96
100
  end
97
101
 
98
102
  class RedisStoreImplBase