launchdarkly-server-sdk 8.11.2-java → 8.12.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ldclient-rb/config.rb +69 -9
- data/lib/ldclient-rb/context.rb +1 -1
- data/lib/ldclient-rb/data_system.rb +227 -0
- data/lib/ldclient-rb/events.rb +34 -19
- data/lib/ldclient-rb/flags_state.rb +1 -1
- data/lib/ldclient-rb/impl/big_segments.rb +4 -4
- data/lib/ldclient-rb/impl/cache_store.rb +44 -0
- data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
- data/lib/ldclient-rb/impl/data_source/requestor.rb +113 -0
- data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -0
- data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
- data/lib/ldclient-rb/impl/data_source.rb +3 -3
- data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
- data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
- data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
- data/lib/ldclient-rb/impl/data_store/status_provider.rb +76 -0
- data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
- data/lib/ldclient-rb/impl/data_store.rb +11 -97
- data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
- data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
- data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
- data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
- data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
- data/lib/ldclient-rb/impl/evaluator.rb +3 -2
- data/lib/ldclient-rb/impl/event_sender.rb +14 -6
- data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +290 -0
- data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
- data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
- data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
- data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
- data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
- data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +71 -0
- data/lib/ldclient-rb/impl.rb +1 -2
- data/lib/ldclient-rb/in_memory_store.rb +1 -18
- data/lib/ldclient-rb/integrations/file_data.rb +67 -0
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
- data/lib/ldclient-rb/integrations/test_data.rb +11 -11
- data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
- data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
- data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
- data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
- data/lib/ldclient-rb/ldclient.rb +66 -132
- data/lib/ldclient-rb/util.rb +11 -70
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +9 -17
- metadata +41 -19
- data/lib/ldclient-rb/cache_store.rb +0 -45
- data/lib/ldclient-rb/expiring_cache.rb +0 -77
- data/lib/ldclient-rb/memoized_value.rb +0 -32
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
- data/lib/ldclient-rb/polling.rb +0 -102
- data/lib/ldclient-rb/requestor.rb +0 -102
- data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
- data/lib/ldclient-rb/stream.rb +0 -197
|
@@ -0,0 +1,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
|