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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +69 -9
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +227 -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 +113 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -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 +76 -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/data_source_builder_common.rb +77 -0
  21. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  22. data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
  23. data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
  24. data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
  25. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  26. data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
  27. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  28. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  29. data/lib/ldclient-rb/impl/event_sender.rb +14 -6
  30. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  32. data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +290 -0
  34. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  35. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  36. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  37. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  38. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  39. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  40. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  41. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
  42. data/lib/ldclient-rb/impl/util.rb +71 -0
  43. data/lib/ldclient-rb/impl.rb +1 -2
  44. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  45. data/lib/ldclient-rb/integrations/file_data.rb +67 -0
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  47. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  48. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  49. data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
  50. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  51. data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
  52. data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
  53. data/lib/ldclient-rb/ldclient.rb +66 -132
  54. data/lib/ldclient-rb/util.rb +11 -70
  55. data/lib/ldclient-rb/version.rb +1 -1
  56. data/lib/ldclient-rb.rb +9 -17
  57. metadata +41 -19
  58. data/lib/ldclient-rb/cache_store.rb +0 -45
  59. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  60. data/lib/ldclient-rb/memoized_value.rb +0 -32
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  62. data/lib/ldclient-rb/polling.rb +0 -102
  63. data/lib/ldclient-rb/requestor.rb +0 -102
  64. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  65. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,264 @@
1
+ require 'json'
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module DataSystem
6
+ module ProtocolV2
7
+ #
8
+ # This module contains the protocol definitions and data types for the
9
+ # LaunchDarkly data system version 2 (FDv2).
10
+ #
11
+
12
+ #
13
+ # DeleteObject specifies the deletion of a particular object.
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
+ class DeleteObject
19
+ # @return [Integer] The version
20
+ attr_reader :version
21
+
22
+ # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
23
+ attr_reader :kind
24
+
25
+ # @return [Symbol] The key
26
+ attr_reader :key
27
+
28
+ #
29
+ # @param version [Integer] The version
30
+ # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
31
+ # @param key [Symbol] The key
32
+ #
33
+ def initialize(version:, kind:, key:)
34
+ @version = version
35
+ @kind = kind
36
+ @key = key
37
+ end
38
+
39
+ #
40
+ # Returns the event name.
41
+ #
42
+ # @return [Symbol]
43
+ #
44
+ def name
45
+ LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
46
+ end
47
+
48
+ #
49
+ # Serializes the DeleteObject to a JSON-compatible hash.
50
+ #
51
+ # @return [Hash]
52
+ #
53
+ def to_h
54
+ {
55
+ version: @version,
56
+ kind: @kind,
57
+ key: @key,
58
+ }
59
+ end
60
+
61
+ #
62
+ # Deserializes a DeleteObject from a JSON-compatible hash.
63
+ #
64
+ # @param data [Hash] The hash representation
65
+ # @return [DeleteObject]
66
+ # @raise [ArgumentError] if required fields are missing
67
+ #
68
+ def self.from_h(data)
69
+ version = data[:version]
70
+ kind = data[:kind]
71
+ key = data[:key]
72
+
73
+ raise ArgumentError, "Missing required fields in DeleteObject" if version.nil? || kind.nil? || key.nil?
74
+
75
+ new(version: version, kind: kind, key: key.to_sym)
76
+ end
77
+ end
78
+
79
+ #
80
+ # PutObject specifies the addition of a particular object with upsert semantics.
81
+ #
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
+ class PutObject
86
+ # @return [Integer] The version
87
+ attr_reader :version
88
+
89
+ # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
90
+ attr_reader :kind
91
+
92
+ # @return [Symbol] The key
93
+ attr_reader :key
94
+
95
+ # @return [Hash] The object data
96
+ attr_reader :object
97
+
98
+ #
99
+ # @param version [Integer] The version
100
+ # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
101
+ # @param key [Symbol] The key
102
+ # @param object [Hash] The object data
103
+ #
104
+ def initialize(version:, kind:, key:, object:)
105
+ @version = version
106
+ @kind = kind
107
+ @key = key
108
+ @object = object
109
+ end
110
+
111
+ #
112
+ # Returns the event name.
113
+ #
114
+ # @return [Symbol]
115
+ #
116
+ def name
117
+ LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
118
+ end
119
+
120
+ #
121
+ # Serializes the PutObject to a JSON-compatible hash.
122
+ #
123
+ # @return [Hash]
124
+ #
125
+ def to_h
126
+ {
127
+ version: @version,
128
+ kind: @kind,
129
+ key: @key,
130
+ object: @object,
131
+ }
132
+ end
133
+
134
+ #
135
+ # Deserializes a PutObject from a JSON-compatible hash.
136
+ #
137
+ # @param data [Hash] The hash representation
138
+ # @return [PutObject]
139
+ # @raise [ArgumentError] if required fields are missing
140
+ #
141
+ def self.from_h(data)
142
+ version = data[:version]
143
+ kind = data[:kind]
144
+ key = data[:key]
145
+ object_data = data[:object]
146
+
147
+ raise ArgumentError, "Missing required fields in PutObject" if version.nil? || kind.nil? || key.nil? || object_data.nil?
148
+
149
+ new(version: version, kind: kind, key: key.to_sym, object: object_data)
150
+ end
151
+ end
152
+
153
+ #
154
+ # Goodbye represents a goodbye event.
155
+ #
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
+ class Goodbye
160
+ # @return [String] The reason for goodbye
161
+ attr_reader :reason
162
+
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
+ #
170
+ # @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
+ #
174
+ def initialize(reason:, silent:, catastrophe:)
175
+ @reason = reason
176
+ @silent = silent
177
+ @catastrophe = catastrophe
178
+ end
179
+
180
+ #
181
+ # Serializes the Goodbye to a JSON-compatible hash.
182
+ #
183
+ # @return [Hash]
184
+ #
185
+ def to_h
186
+ {
187
+ reason: @reason,
188
+ silent: @silent,
189
+ catastrophe: @catastrophe,
190
+ }
191
+ end
192
+
193
+ #
194
+ # Deserializes a Goodbye event from a JSON-compatible hash.
195
+ #
196
+ # @param data [Hash] The hash representation
197
+ # @return [Goodbye]
198
+ # @raise [ArgumentError] if required fields are missing
199
+ #
200
+ def self.from_h(data)
201
+ reason = data[:reason]
202
+ silent = data[:silent]
203
+ catastrophe = data[:catastrophe]
204
+
205
+ raise ArgumentError, "Missing required fields in Goodbye" if reason.nil? || silent.nil? || catastrophe.nil?
206
+
207
+ new(reason: reason, silent: silent, catastrophe: catastrophe)
208
+ end
209
+ end
210
+
211
+ #
212
+ # Error represents an error event.
213
+ #
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
+ class Error
218
+ # @return [String] The payload ID
219
+ attr_reader :payload_id
220
+
221
+ # @return [String] The reason for the error
222
+ attr_reader :reason
223
+
224
+ #
225
+ # @param payload_id [String] The payload ID
226
+ # @param reason [String] The reason for the error
227
+ #
228
+ def initialize(payload_id:, reason:)
229
+ @payload_id = payload_id
230
+ @reason = reason
231
+ end
232
+
233
+ #
234
+ # Serializes the Error to a JSON-compatible hash.
235
+ #
236
+ # @return [Hash]
237
+ #
238
+ def to_h
239
+ {
240
+ payloadId: @payload_id,
241
+ reason: @reason,
242
+ }
243
+ end
244
+
245
+ #
246
+ # Deserializes an Error from a JSON-compatible hash.
247
+ #
248
+ # @param data [Hash] The hash representation
249
+ # @return [Error]
250
+ # @raise [ArgumentError] if required fields are missing
251
+ #
252
+ def self.from_h(data)
253
+ payload_id = data[:payloadId]
254
+ reason = data[:reason]
255
+
256
+ raise ArgumentError, "Missing required fields in Error" if payload_id.nil? || reason.nil?
257
+
258
+ new(payload_id: payload_id, reason: reason)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ldclient-rb/interfaces"
4
+ require "ldclient-rb/interfaces/data_system"
5
+ require "ldclient-rb/impl/data_system"
6
+ require "ldclient-rb/impl/data_system/protocolv2"
7
+ require "ldclient-rb/impl/data_system/polling" # For shared constants
8
+ require "ldclient-rb/impl/data_system/data_source_builder_common"
9
+ require "ldclient-rb/impl/util"
10
+ require "concurrent"
11
+ require "json"
12
+ require "uri"
13
+ require "ld-eventsource"
14
+
15
+ module LaunchDarkly
16
+ module Impl
17
+ module DataSystem
18
+ FDV2_STREAMING_ENDPOINT = "/sdk/stream"
19
+
20
+ # Allows for up to 5 minutes to elapse without any data sent across the stream.
21
+ # The heartbeats sent as comments on the stream will keep this from triggering.
22
+ STREAM_READ_TIMEOUT = 5 * 60
23
+
24
+ #
25
+ # StreamingDataSource is a Synchronizer that uses Server-Sent Events (SSE)
26
+ # to receive real-time updates from LaunchDarkly's Flag Delivery services.
27
+ #
28
+ class StreamingDataSource
29
+ include LaunchDarkly::Interfaces::DataSystem::Synchronizer
30
+
31
+ attr_reader :name
32
+
33
+ #
34
+ # @param sdk_key [String]
35
+ # @param http_config [HttpConfigOptions] HTTP connection settings
36
+ # @param initial_reconnect_delay [Float] Initial delay before reconnecting after an error
37
+ # @param config [LaunchDarkly::Config] Used for global header settings
38
+ #
39
+ def initialize(sdk_key, http_config, initial_reconnect_delay, config)
40
+ @sdk_key = sdk_key
41
+ @http_config = http_config
42
+ @initial_reconnect_delay = initial_reconnect_delay
43
+ @config = config
44
+ @logger = config.logger
45
+ @name = "StreamingDataSourceV2"
46
+ @sse = nil
47
+ @stopped = Concurrent::Event.new
48
+ @diagnostic_accumulator = nil
49
+ @connection_attempt_start_time = 0
50
+ end
51
+
52
+ #
53
+ # Sets the diagnostic accumulator for streaming initialization metrics.
54
+ #
55
+ # @param diagnostic_accumulator [LaunchDarkly::Impl::DiagnosticAccumulator]
56
+ #
57
+ def set_diagnostic_accumulator(diagnostic_accumulator)
58
+ @diagnostic_accumulator = diagnostic_accumulator
59
+ end
60
+
61
+ #
62
+ # sync begins the synchronization process for the data source, yielding
63
+ # Update objects until the connection is closed or an unrecoverable error
64
+ # occurs.
65
+ #
66
+ # @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
67
+ # @yieldparam update [LaunchDarkly::Interfaces::DataSystem::Update]
68
+ #
69
+ def sync(ss)
70
+ @logger.info { "[LDClient] Starting StreamingDataSourceV2 synchronizer" }
71
+ log_connection_started
72
+
73
+ change_set_builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
74
+ envid = nil
75
+
76
+ base_uri = @http_config.base_uri + FDV2_STREAMING_ENDPOINT
77
+ headers = Impl::Util.default_http_headers(@sdk_key, @config)
78
+ opts = {
79
+ headers: headers,
80
+ read_timeout: STREAM_READ_TIMEOUT,
81
+ logger: @logger,
82
+ socket_factory: @http_config.socket_factory,
83
+ reconnect_time: @initial_reconnect_delay,
84
+ }
85
+
86
+ @sse = SSE::Client.new(base_uri, **opts) do |client|
87
+ client.on_connect do |headers|
88
+ # Extract environment ID and check for fallback on successful connection
89
+ 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
102
+ end
103
+ end
104
+
105
+ client.on_event do |event|
106
+ 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
112
+ end
113
+ rescue JSON::ParserError => e
114
+ @logger.info { "[LDClient] Error parsing stream event; will restart stream: #{e}" }
115
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
116
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
117
+ error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
118
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
119
+ 0,
120
+ e.to_s,
121
+ Time.now
122
+ ),
123
+ environment_id: envid
124
+ )
125
+
126
+ # Re-raise the exception so the SSE implementation can catch it and restart the stream.
127
+ raise
128
+ rescue => e
129
+ @logger.info { "[LDClient] Error while handling stream event; will restart stream: #{e}" }
130
+ yield LaunchDarkly::Interfaces::DataSystem::Update.new(
131
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
132
+ error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
133
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
134
+ 0,
135
+ e.to_s,
136
+ Time.now
137
+ ),
138
+ environment_id: envid
139
+ )
140
+
141
+ # Re-raise the exception so the SSE implementation can catch it and restart the stream.
142
+ raise
143
+ end
144
+ end
145
+
146
+ client.on_error do |error|
147
+ log_connection_result(false)
148
+ fallback = false
149
+
150
+ # Extract envid and fallback from error headers if available
151
+ 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
157
+ end
158
+
159
+ update = handle_error(error, envid, fallback)
160
+ yield update if update
161
+ end
162
+
163
+ client.query_params do
164
+ selector = ss.selector
165
+ {
166
+ "filter" => @config.payload_filter_key,
167
+ "basis" => (selector.state if selector&.defined?),
168
+ }.compact
169
+ end
170
+ end
171
+
172
+ unless @sse
173
+ @logger.error { "[LDClient] Failed to create SSE client for streaming updates" }
174
+ return
175
+ end
176
+
177
+ # Client auto-starts in background thread. Wait here until stop() is called.
178
+ @stopped.wait
179
+ end
180
+
181
+ #
182
+ # Stops the streaming synchronizer.
183
+ #
184
+ def stop
185
+ @logger.info { "[LDClient] Stopping StreamingDataSourceV2 synchronizer" }
186
+ @sse&.close
187
+ @stopped.set
188
+ end
189
+
190
+ #
191
+ # Processes a single SSE message and returns an Update if applicable.
192
+ #
193
+ # @param message [SSE::StreamEvent]
194
+ # @param change_set_builder [LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder]
195
+ # @param envid [String, nil]
196
+ # @return [LaunchDarkly::Interfaces::DataSystem::Update, nil]
197
+ #
198
+ private def process_message(message, change_set_builder, envid)
199
+ event_type = message.type
200
+
201
+ # Handle heartbeat
202
+ if event_type == LaunchDarkly::Interfaces::DataSystem::EventName::HEARTBEAT
203
+ return nil
204
+ end
205
+
206
+ @logger.debug { "[LDClient] Stream received #{event_type} message: #{message.data}" }
207
+
208
+ case event_type
209
+ when LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT
210
+ server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.from_h(JSON.parse(message.data, symbolize_names: true))
211
+ change_set_builder.start(server_intent.payload.code)
212
+
213
+ if server_intent.payload.code == LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_NONE
214
+ change_set_builder.expect_changes
215
+ return LaunchDarkly::Interfaces::DataSystem::Update.new(
216
+ state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
217
+ environment_id: envid
218
+ )
219
+ end
220
+ nil
221
+
222
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
223
+ put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(JSON.parse(message.data, symbolize_names: true))
224
+ change_set_builder.add_put(put.kind, put.key, put.version, put.object)
225
+ nil
226
+
227
+ when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
228
+ delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(JSON.parse(message.data, symbolize_names: true))
229
+ change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
230
+ nil
231
+
232
+ when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE
233
+ 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
237
+ nil
238
+
239
+ when LaunchDarkly::Interfaces::DataSystem::EventName::ERROR
240
+ error = LaunchDarkly::Impl::DataSystem::ProtocolV2::Error.from_h(JSON.parse(message.data, symbolize_names: true))
241
+ @logger.error { "[LDClient] Error on #{error.payload_id}: #{error.reason}" }
242
+
243
+ # Reset any previous change events but continue with last server intent
244
+ change_set_builder.reset
245
+ nil
246
+
247
+ when LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED
248
+ selector = LaunchDarkly::Interfaces::DataSystem::Selector.from_h(JSON.parse(message.data, symbolize_names: true))
249
+ change_set = change_set_builder.finish(selector)
250
+
251
+ LaunchDarkly::Interfaces::DataSystem::Update.new(
252
+ state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
253
+ change_set: change_set,
254
+ environment_id: envid
255
+ )
256
+
257
+ else
258
+ @logger.info { "[LDClient] Unexpected event found in stream: #{event_type}" }
259
+ nil
260
+ end
261
+ end
262
+
263
+ #
264
+ # Handles errors that occur during streaming.
265
+ #
266
+ # @param error [Exception]
267
+ # @param envid [String, nil]
268
+ # @param fallback [Boolean]
269
+ # @return [LaunchDarkly::Interfaces::DataSystem::Update, nil]
270
+ #
271
+ private def handle_error(error, envid, fallback)
272
+ return nil if @stopped.set?
273
+
274
+ update = nil
275
+
276
+ case error
277
+ when SSE::Errors::HTTPStatusError
278
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
279
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE,
280
+ error.status,
281
+ Impl::Util.http_error_message(error.status, "stream connection", "will retry"),
282
+ Time.now
283
+ )
284
+
285
+ if fallback
286
+ update = LaunchDarkly::Interfaces::DataSystem::Update.new(
287
+ state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
288
+ error: error_info,
289
+ revert_to_fdv1: true,
290
+ environment_id: envid
291
+ )
292
+ stop
293
+ return update
294
+ end
295
+
296
+ is_recoverable = Impl::Util.http_error_recoverable?(error.status)
297
+
298
+ update = LaunchDarkly::Interfaces::DataSystem::Update.new(
299
+ state: is_recoverable ? LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED : LaunchDarkly::Interfaces::DataSource::Status::OFF,
300
+ error: error_info,
301
+ environment_id: envid
302
+ )
303
+
304
+ unless is_recoverable
305
+ @logger.error { "[LDClient] #{error_info.message}" }
306
+ stop
307
+ return update
308
+ end
309
+
310
+ @logger.warn { "[LDClient] #{error_info.message}" }
311
+
312
+ when SSE::Errors::HTTPContentTypeError, SSE::Errors::HTTPProxyError, SSE::Errors::ReadTimeoutError
313
+ @logger.warn { "[LDClient] Network error on stream connection: #{error}, will retry" }
314
+
315
+ update = LaunchDarkly::Interfaces::DataSystem::Update.new(
316
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
317
+ error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
318
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR,
319
+ 0,
320
+ error.to_s,
321
+ Time.now
322
+ ),
323
+ environment_id: envid
324
+ )
325
+
326
+ else
327
+ @logger.warn { "[LDClient] Unexpected error on stream connection: #{error}, will retry" }
328
+
329
+ update = LaunchDarkly::Interfaces::DataSystem::Update.new(
330
+ state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
331
+ error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
332
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
333
+ 0,
334
+ error.to_s,
335
+ Time.now
336
+ ),
337
+ environment_id: envid
338
+ )
339
+ end
340
+
341
+ update
342
+ end
343
+
344
+ private def log_connection_started
345
+ @connection_attempt_start_time = Impl::Util.current_time_millis
346
+ end
347
+
348
+ private def log_connection_result(is_success)
349
+ return unless @diagnostic_accumulator
350
+ return unless @connection_attempt_start_time > 0
351
+
352
+ current_time = Impl::Util.current_time_millis
353
+ elapsed = current_time - @connection_attempt_start_time
354
+ @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success, elapsed >= 0 ? elapsed : 0)
355
+ @connection_attempt_start_time = 0
356
+ end
357
+ end
358
+
359
+ #
360
+ # Builder for a StreamingDataSource.
361
+ #
362
+ class StreamingDataSourceBuilder
363
+ include DataSourceBuilderCommon
364
+
365
+ DEFAULT_BASE_URI = "https://stream.launchdarkly.com"
366
+ DEFAULT_INITIAL_RECONNECT_DELAY = 1
367
+
368
+ def initialize
369
+ # No initialization needed - defaults applied in build via nil-check
370
+ end
371
+
372
+ #
373
+ # Sets the initial delay before reconnecting after an error.
374
+ #
375
+ # @param delay [Float] Delay in seconds
376
+ # @return [StreamingDataSourceBuilder]
377
+ #
378
+ def initial_reconnect_delay(delay)
379
+ @initial_reconnect_delay = delay
380
+ self
381
+ end
382
+
383
+ #
384
+ # Builds the StreamingDataSource with the configured parameters.
385
+ #
386
+ # @param sdk_key [String]
387
+ # @param config [LaunchDarkly::Config]
388
+ # @return [StreamingDataSource]
389
+ #
390
+ def build(sdk_key, config)
391
+ http_opts = build_http_config
392
+ StreamingDataSource.new(
393
+ sdk_key, http_opts,
394
+ @initial_reconnect_delay || DEFAULT_INITIAL_RECONNECT_DELAY,
395
+ config
396
+ )
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end