launchdarkly-server-sdk 8.11.2 → 8.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ldclient-rb/config.rb +66 -3
- data/lib/ldclient-rb/context.rb +1 -1
- data/lib/ldclient-rb/data_system.rb +243 -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 +106 -0
- data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -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 +82 -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/fdv1.rb +20 -7
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
- data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -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 +4 -3
- 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/test_data/test_data_source_v2.rb +288 -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/util.rb +65 -0
- data/lib/ldclient-rb/impl.rb +1 -2
- data/lib/ldclient-rb/in_memory_store.rb +1 -18
- 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 +248 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
- data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
- data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
- data/lib/ldclient-rb/ldclient.rb +55 -131
- data/lib/ldclient-rb/util.rb +11 -70
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +8 -17
- metadata +35 -17
- 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,601 @@
|
|
|
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_source/requestor"
|
|
8
|
+
require "ldclient-rb/impl/util"
|
|
9
|
+
require "concurrent"
|
|
10
|
+
require "json"
|
|
11
|
+
require "uri"
|
|
12
|
+
require "http"
|
|
13
|
+
|
|
14
|
+
module LaunchDarkly
|
|
15
|
+
module Impl
|
|
16
|
+
module DataSystem
|
|
17
|
+
FDV2_POLLING_ENDPOINT = "/sdk/poll"
|
|
18
|
+
FDV1_POLLING_ENDPOINT = "/sdk/latest-all"
|
|
19
|
+
|
|
20
|
+
LD_ENVID_HEADER = "x-launchdarkly-env-id"
|
|
21
|
+
LD_FD_FALLBACK_HEADER = "x-launchdarkly-fd-fallback"
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Requester protocol for polling data source
|
|
25
|
+
#
|
|
26
|
+
module Requester
|
|
27
|
+
#
|
|
28
|
+
# Fetches the data for the given selector.
|
|
29
|
+
# Returns a Result containing a tuple of [ChangeSet, headers],
|
|
30
|
+
# or an error if the data could not be retrieved.
|
|
31
|
+
#
|
|
32
|
+
# @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
|
|
33
|
+
# @return [Result]
|
|
34
|
+
#
|
|
35
|
+
def fetch(selector)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Closes any persistent connections and releases resources.
|
|
41
|
+
# This method should be called when the requester is no longer needed.
|
|
42
|
+
# Implementations should handle being called multiple times gracefully.
|
|
43
|
+
#
|
|
44
|
+
def stop
|
|
45
|
+
# Optional - implementations may override if they need cleanup
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# PollingDataSource is a data source that can retrieve information from
|
|
51
|
+
# LaunchDarkly either as an Initializer or as a Synchronizer.
|
|
52
|
+
#
|
|
53
|
+
class PollingDataSource
|
|
54
|
+
include LaunchDarkly::Interfaces::DataSystem::Initializer
|
|
55
|
+
include LaunchDarkly::Interfaces::DataSystem::Synchronizer
|
|
56
|
+
|
|
57
|
+
attr_reader :name
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# @param poll_interval [Float] Polling interval in seconds
|
|
61
|
+
# @param requester [Requester] The requester to use for fetching data
|
|
62
|
+
# @param logger [Logger] The logger
|
|
63
|
+
#
|
|
64
|
+
def initialize(poll_interval, requester, logger)
|
|
65
|
+
@requester = requester
|
|
66
|
+
@poll_interval = poll_interval
|
|
67
|
+
@logger = logger
|
|
68
|
+
@interrupt_event = Concurrent::Event.new
|
|
69
|
+
@stop = Concurrent::Event.new
|
|
70
|
+
@name = "PollingDataSourceV2"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Fetch returns a Basis, or an error if the Basis could not be retrieved.
|
|
75
|
+
#
|
|
76
|
+
# @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
|
|
77
|
+
# @return [LaunchDarkly::Interfaces::DataSystem::Basis, nil]
|
|
78
|
+
#
|
|
79
|
+
def fetch(ss)
|
|
80
|
+
poll(ss)
|
|
81
|
+
ensure
|
|
82
|
+
# Ensure the requester is stopped to avoid leaving open connections.
|
|
83
|
+
@requester.stop if @requester.respond_to?(:stop)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
#
|
|
87
|
+
# sync begins the synchronization process for the data source, yielding
|
|
88
|
+
# Update objects until the connection is closed or an unrecoverable error
|
|
89
|
+
# occurs.
|
|
90
|
+
#
|
|
91
|
+
# @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
|
|
92
|
+
# @yieldparam update [LaunchDarkly::Interfaces::DataSystem::Update]
|
|
93
|
+
#
|
|
94
|
+
def sync(ss)
|
|
95
|
+
@logger.info { "[LDClient] Starting PollingDataSourceV2 synchronizer" }
|
|
96
|
+
@stop.reset
|
|
97
|
+
@interrupt_event.reset
|
|
98
|
+
|
|
99
|
+
until @stop.set?
|
|
100
|
+
result = @requester.fetch(ss.selector)
|
|
101
|
+
|
|
102
|
+
if !result.success?
|
|
103
|
+
fallback = false
|
|
104
|
+
envid = nil
|
|
105
|
+
|
|
106
|
+
if result.headers
|
|
107
|
+
fallback = result.headers[LD_FD_FALLBACK_HEADER] == 'true'
|
|
108
|
+
envid = result.headers[LD_ENVID_HEADER]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
|
|
112
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
113
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE,
|
|
114
|
+
result.exception.status,
|
|
115
|
+
Impl::Util.http_error_message(
|
|
116
|
+
result.exception.status, "polling request", "will retry"
|
|
117
|
+
),
|
|
118
|
+
Time.now
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
status_code = result.exception.status
|
|
122
|
+
if Impl::Util.http_error_recoverable?(status_code)
|
|
123
|
+
# If fallback is requested, send OFF status to signal shutdown
|
|
124
|
+
if fallback
|
|
125
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
126
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
127
|
+
error: error_info,
|
|
128
|
+
environment_id: envid,
|
|
129
|
+
revert_to_fdv1: true
|
|
130
|
+
)
|
|
131
|
+
break
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
135
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
136
|
+
error: error_info,
|
|
137
|
+
environment_id: envid,
|
|
138
|
+
revert_to_fdv1: false
|
|
139
|
+
)
|
|
140
|
+
@interrupt_event.wait(@poll_interval)
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
145
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
146
|
+
error: error_info,
|
|
147
|
+
environment_id: envid,
|
|
148
|
+
revert_to_fdv1: fallback
|
|
149
|
+
)
|
|
150
|
+
break
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
154
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR,
|
|
155
|
+
0,
|
|
156
|
+
result.error,
|
|
157
|
+
Time.now
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# If fallback is requested, send OFF status to signal shutdown
|
|
161
|
+
if fallback
|
|
162
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
163
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
164
|
+
error: error_info,
|
|
165
|
+
environment_id: envid,
|
|
166
|
+
revert_to_fdv1: true
|
|
167
|
+
)
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
172
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
173
|
+
error: error_info,
|
|
174
|
+
environment_id: envid,
|
|
175
|
+
revert_to_fdv1: false
|
|
176
|
+
)
|
|
177
|
+
else
|
|
178
|
+
change_set, headers = result.value
|
|
179
|
+
fallback = headers[LD_FD_FALLBACK_HEADER] == 'true'
|
|
180
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
181
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
182
|
+
change_set: change_set,
|
|
183
|
+
environment_id: headers[LD_ENVID_HEADER],
|
|
184
|
+
revert_to_fdv1: fallback
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
break if fallback
|
|
189
|
+
break if @interrupt_event.wait(@poll_interval)
|
|
190
|
+
end
|
|
191
|
+
ensure
|
|
192
|
+
# Ensure the requester is stopped to avoid leaving open connections.
|
|
193
|
+
@requester.stop if @requester.respond_to?(:stop)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# Stops the synchronizer.
|
|
198
|
+
#
|
|
199
|
+
def stop
|
|
200
|
+
@logger.info { "[LDClient] Stopping PollingDataSourceV2 synchronizer" }
|
|
201
|
+
@interrupt_event.set
|
|
202
|
+
@stop.set
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#
|
|
206
|
+
# @param ss [LaunchDarkly::Interfaces::DataSystem::SelectorStore]
|
|
207
|
+
# @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::Basis, String>]
|
|
208
|
+
#
|
|
209
|
+
private def poll(ss)
|
|
210
|
+
result = @requester.fetch(ss.selector)
|
|
211
|
+
|
|
212
|
+
unless result.success?
|
|
213
|
+
if result.exception.is_a?(LaunchDarkly::Impl::DataSource::UnexpectedResponseError)
|
|
214
|
+
status_code = result.exception.status
|
|
215
|
+
http_error_message_result = Impl::Util.http_error_message(
|
|
216
|
+
status_code, "polling request", "will retry"
|
|
217
|
+
)
|
|
218
|
+
@logger.warn { "[LDClient] #{http_error_message_result}" } if Impl::Util.http_error_recoverable?(status_code)
|
|
219
|
+
return LaunchDarkly::Result.fail(http_error_message_result, result.exception)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
return LaunchDarkly::Result.fail(result.error || 'Failed to request payload', result.exception)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
change_set, headers = result.value
|
|
226
|
+
|
|
227
|
+
env_id = headers[LD_ENVID_HEADER]
|
|
228
|
+
env_id = nil unless env_id.is_a?(String)
|
|
229
|
+
|
|
230
|
+
basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(
|
|
231
|
+
change_set: change_set,
|
|
232
|
+
persist: change_set.selector.defined?,
|
|
233
|
+
environment_id: env_id
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
LaunchDarkly::Result.success(basis)
|
|
237
|
+
rescue => e
|
|
238
|
+
msg = "Error: Exception encountered when updating flags. #{e}"
|
|
239
|
+
@logger.error { "[LDClient] #{msg}" }
|
|
240
|
+
@logger.debug { "[LDClient] Exception trace: #{e.backtrace}" }
|
|
241
|
+
LaunchDarkly::Result.fail(msg, e)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
#
|
|
246
|
+
# HTTPPollingRequester is a Requester that uses HTTP to make
|
|
247
|
+
# requests to the FDv2 polling endpoint.
|
|
248
|
+
#
|
|
249
|
+
class HTTPPollingRequester
|
|
250
|
+
include Requester
|
|
251
|
+
|
|
252
|
+
#
|
|
253
|
+
# @param sdk_key [String]
|
|
254
|
+
# @param config [LaunchDarkly::Config]
|
|
255
|
+
#
|
|
256
|
+
def initialize(sdk_key, config)
|
|
257
|
+
@etag = nil
|
|
258
|
+
@config = config
|
|
259
|
+
@sdk_key = sdk_key
|
|
260
|
+
@poll_uri = config.base_uri + FDV2_POLLING_ENDPOINT
|
|
261
|
+
@http_client = Impl::Util.new_http_client(config.base_uri, config)
|
|
262
|
+
.use(:auto_inflate)
|
|
263
|
+
.headers("Accept-Encoding" => "gzip")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
|
|
268
|
+
# @return [Result]
|
|
269
|
+
#
|
|
270
|
+
def fetch(selector)
|
|
271
|
+
query_params = []
|
|
272
|
+
query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
|
|
273
|
+
|
|
274
|
+
if selector && selector.defined?
|
|
275
|
+
query_params << ["selector", selector.state]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
uri = @poll_uri
|
|
279
|
+
if query_params.any?
|
|
280
|
+
filter_query = URI.encode_www_form(query_params)
|
|
281
|
+
uri = "#{uri}?#{filter_query}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
headers = {}
|
|
285
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
|
286
|
+
headers["If-None-Match"] = @etag unless @etag.nil?
|
|
287
|
+
|
|
288
|
+
begin
|
|
289
|
+
response = @http_client.request("GET", uri, headers: headers)
|
|
290
|
+
status = response.status.code
|
|
291
|
+
response_headers = response.headers.to_h.transform_keys(&:downcase)
|
|
292
|
+
|
|
293
|
+
if status >= 400
|
|
294
|
+
return LaunchDarkly::Result.fail(
|
|
295
|
+
"HTTP error #{status}",
|
|
296
|
+
LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
|
|
297
|
+
response_headers
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
if status == 304
|
|
302
|
+
return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
body = response.to_s
|
|
306
|
+
data = JSON.parse(body, symbolize_names: true)
|
|
307
|
+
etag = response_headers["etag"]
|
|
308
|
+
@etag = etag unless etag.nil?
|
|
309
|
+
|
|
310
|
+
@config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
|
|
311
|
+
|
|
312
|
+
changeset_result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(data)
|
|
313
|
+
if changeset_result.success?
|
|
314
|
+
LaunchDarkly::Result.success([changeset_result.value, response_headers])
|
|
315
|
+
else
|
|
316
|
+
LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
|
|
317
|
+
end
|
|
318
|
+
rescue JSON::ParserError => e
|
|
319
|
+
LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
|
|
320
|
+
rescue => e
|
|
321
|
+
LaunchDarkly::Result.fail("Network error: #{e.message}", e)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
#
|
|
326
|
+
# Closes the HTTP client and releases any persistent connections.
|
|
327
|
+
#
|
|
328
|
+
def stop
|
|
329
|
+
begin
|
|
330
|
+
@http_client.close if @http_client
|
|
331
|
+
rescue
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
#
|
|
337
|
+
# HTTPFDv1PollingRequester is a Requester that uses HTTP to make
|
|
338
|
+
# requests to the FDv1 polling endpoint.
|
|
339
|
+
#
|
|
340
|
+
class HTTPFDv1PollingRequester
|
|
341
|
+
include Requester
|
|
342
|
+
|
|
343
|
+
#
|
|
344
|
+
# @param sdk_key [String]
|
|
345
|
+
# @param config [LaunchDarkly::Config]
|
|
346
|
+
#
|
|
347
|
+
def initialize(sdk_key, config)
|
|
348
|
+
@etag = nil
|
|
349
|
+
@config = config
|
|
350
|
+
@sdk_key = sdk_key
|
|
351
|
+
@poll_uri = config.base_uri + FDV1_POLLING_ENDPOINT
|
|
352
|
+
@http_client = Impl::Util.new_http_client(config.base_uri, config)
|
|
353
|
+
.use(:auto_inflate)
|
|
354
|
+
.headers("Accept-Encoding" => "gzip")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
#
|
|
358
|
+
# @param selector [LaunchDarkly::Interfaces::DataSystem::Selector, nil]
|
|
359
|
+
# @return [Result]
|
|
360
|
+
#
|
|
361
|
+
def fetch(selector)
|
|
362
|
+
query_params = []
|
|
363
|
+
query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
|
|
364
|
+
|
|
365
|
+
uri = @poll_uri
|
|
366
|
+
if query_params.any?
|
|
367
|
+
filter_query = URI.encode_www_form(query_params)
|
|
368
|
+
uri = "#{uri}?#{filter_query}"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
headers = {}
|
|
372
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
|
373
|
+
headers["If-None-Match"] = @etag unless @etag.nil?
|
|
374
|
+
|
|
375
|
+
begin
|
|
376
|
+
response = @http_client.request("GET", uri, headers: headers)
|
|
377
|
+
status = response.status.code
|
|
378
|
+
response_headers = response.headers.to_h.transform_keys(&:downcase)
|
|
379
|
+
|
|
380
|
+
if status >= 400
|
|
381
|
+
return LaunchDarkly::Result.fail(
|
|
382
|
+
"HTTP error #{status}",
|
|
383
|
+
LaunchDarkly::Impl::DataSource::UnexpectedResponseError.new(status),
|
|
384
|
+
response_headers
|
|
385
|
+
)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if status == 304
|
|
389
|
+
return LaunchDarkly::Result.success([LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes, response_headers])
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
body = response.to_s
|
|
393
|
+
data = JSON.parse(body, symbolize_names: true)
|
|
394
|
+
etag = response_headers["etag"]
|
|
395
|
+
@etag = etag unless etag.nil?
|
|
396
|
+
|
|
397
|
+
@config.logger.debug { "[LDClient] #{uri} response status:[#{status}] ETag:[#{etag}]" }
|
|
398
|
+
|
|
399
|
+
changeset_result = LaunchDarkly::Impl::DataSystem.fdv1_polling_payload_to_changeset(data)
|
|
400
|
+
if changeset_result.success?
|
|
401
|
+
LaunchDarkly::Result.success([changeset_result.value, response_headers])
|
|
402
|
+
else
|
|
403
|
+
LaunchDarkly::Result.fail(changeset_result.error, changeset_result.exception, response_headers)
|
|
404
|
+
end
|
|
405
|
+
rescue JSON::ParserError => e
|
|
406
|
+
LaunchDarkly::Result.fail("Failed to parse JSON: #{e.message}", e, response_headers)
|
|
407
|
+
rescue => e
|
|
408
|
+
LaunchDarkly::Result.fail("Network error: #{e.message}", e)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
#
|
|
413
|
+
# Closes the HTTP client and releases any persistent connections.
|
|
414
|
+
#
|
|
415
|
+
def stop
|
|
416
|
+
begin
|
|
417
|
+
@http_client.close if @http_client
|
|
418
|
+
rescue
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
#
|
|
424
|
+
# Converts a polling payload into a ChangeSet.
|
|
425
|
+
#
|
|
426
|
+
# @param data [Hash] The polling payload
|
|
427
|
+
# @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
|
|
428
|
+
#
|
|
429
|
+
def self.polling_payload_to_changeset(data)
|
|
430
|
+
unless data[:events].is_a?(Array)
|
|
431
|
+
return LaunchDarkly::Result.fail("Invalid payload: 'events' key is missing or not a list")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
435
|
+
|
|
436
|
+
data[:events].each do |event|
|
|
437
|
+
unless event.is_a?(Hash)
|
|
438
|
+
return LaunchDarkly::Result.fail("Invalid payload: 'events' must be a list of objects")
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
next unless event[:event]
|
|
442
|
+
|
|
443
|
+
case event[:event]
|
|
444
|
+
when LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT
|
|
445
|
+
begin
|
|
446
|
+
server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.from_h(event[:data])
|
|
447
|
+
rescue ArgumentError => e
|
|
448
|
+
return LaunchDarkly::Result.fail("Invalid JSON in server intent", e)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if server_intent.payload.code == LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_NONE
|
|
452
|
+
return LaunchDarkly::Result.success(LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.no_changes)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
builder.start(server_intent.payload.code)
|
|
456
|
+
|
|
457
|
+
when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
|
|
458
|
+
begin
|
|
459
|
+
put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data])
|
|
460
|
+
rescue ArgumentError => e
|
|
461
|
+
return LaunchDarkly::Result.fail("Invalid JSON in put object", e)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
builder.add_put(put.kind, put.key, put.version, put.object)
|
|
465
|
+
|
|
466
|
+
when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
|
|
467
|
+
begin
|
|
468
|
+
delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data])
|
|
469
|
+
rescue ArgumentError => e
|
|
470
|
+
return LaunchDarkly::Result.fail("Invalid JSON in delete object", e)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
|
|
474
|
+
|
|
475
|
+
when LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED
|
|
476
|
+
begin
|
|
477
|
+
selector = LaunchDarkly::Interfaces::DataSystem::Selector.from_h(event[:data])
|
|
478
|
+
changeset = builder.finish(selector)
|
|
479
|
+
return LaunchDarkly::Result.success(changeset)
|
|
480
|
+
rescue ArgumentError, RuntimeError => e
|
|
481
|
+
return LaunchDarkly::Result.fail("Invalid JSON in payload transferred object", e)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
LaunchDarkly::Result.fail("didn't receive any known protocol events in polling payload")
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
#
|
|
490
|
+
# Converts an FDv1 polling payload into a ChangeSet.
|
|
491
|
+
#
|
|
492
|
+
# @param data [Hash] The FDv1 polling payload
|
|
493
|
+
# @return [LaunchDarkly::Result<LaunchDarkly::Interfaces::DataSystem::ChangeSet, String>] Result containing ChangeSet on success, or error message on failure
|
|
494
|
+
#
|
|
495
|
+
def self.fdv1_polling_payload_to_changeset(data)
|
|
496
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
497
|
+
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
|
|
498
|
+
selector = LaunchDarkly::Interfaces::DataSystem::Selector.no_selector
|
|
499
|
+
|
|
500
|
+
kind_mappings = [
|
|
501
|
+
[LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, :flags],
|
|
502
|
+
[LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, :segments],
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
kind_mappings.each do |kind, fdv1_key|
|
|
506
|
+
kind_data = data[fdv1_key]
|
|
507
|
+
next if kind_data.nil?
|
|
508
|
+
|
|
509
|
+
unless kind_data.is_a?(Hash)
|
|
510
|
+
return LaunchDarkly::Result.fail("Invalid format: #{fdv1_key} is not an object")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
kind_data.each do |key, flag_or_segment|
|
|
514
|
+
unless flag_or_segment.is_a?(Hash)
|
|
515
|
+
return LaunchDarkly::Result.fail("Invalid format: #{key} is not an object")
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
version = flag_or_segment[:version]
|
|
519
|
+
return LaunchDarkly::Result.fail("Invalid format: #{key} does not have a version set") if version.nil?
|
|
520
|
+
|
|
521
|
+
builder.add_put(kind, key.to_s, version, flag_or_segment)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
LaunchDarkly::Result.success(builder.finish(selector))
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
#
|
|
529
|
+
# Builder for a PollingDataSource.
|
|
530
|
+
#
|
|
531
|
+
class PollingDataSourceBuilder
|
|
532
|
+
#
|
|
533
|
+
# @param sdk_key [String]
|
|
534
|
+
# @param config [LaunchDarkly::Config]
|
|
535
|
+
#
|
|
536
|
+
def initialize(sdk_key, config)
|
|
537
|
+
@sdk_key = sdk_key
|
|
538
|
+
@config = config
|
|
539
|
+
@requester = nil
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
#
|
|
543
|
+
# Sets a custom Requester for the PollingDataSource.
|
|
544
|
+
#
|
|
545
|
+
# @param requester [Requester]
|
|
546
|
+
# @return [PollingDataSourceBuilder]
|
|
547
|
+
#
|
|
548
|
+
def requester(requester)
|
|
549
|
+
@requester = requester
|
|
550
|
+
self
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
#
|
|
554
|
+
# Builds the PollingDataSource with the configured parameters.
|
|
555
|
+
#
|
|
556
|
+
# @return [PollingDataSource]
|
|
557
|
+
#
|
|
558
|
+
def build
|
|
559
|
+
requester = @requester || HTTPPollingRequester.new(@sdk_key, @config)
|
|
560
|
+
PollingDataSource.new(@config.poll_interval, requester, @config.logger)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
#
|
|
565
|
+
# Builder for an FDv1 PollingDataSource.
|
|
566
|
+
#
|
|
567
|
+
class FDv1PollingDataSourceBuilder
|
|
568
|
+
#
|
|
569
|
+
# @param sdk_key [String]
|
|
570
|
+
# @param config [LaunchDarkly::Config]
|
|
571
|
+
#
|
|
572
|
+
def initialize(sdk_key, config)
|
|
573
|
+
@sdk_key = sdk_key
|
|
574
|
+
@config = config
|
|
575
|
+
@requester = nil
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
#
|
|
579
|
+
# Sets a custom Requester for the PollingDataSource.
|
|
580
|
+
#
|
|
581
|
+
# @param requester [Requester]
|
|
582
|
+
# @return [FDv1PollingDataSourceBuilder]
|
|
583
|
+
#
|
|
584
|
+
def requester(requester)
|
|
585
|
+
@requester = requester
|
|
586
|
+
self
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
#
|
|
590
|
+
# Builds the PollingDataSource with the configured parameters.
|
|
591
|
+
#
|
|
592
|
+
# @return [PollingDataSource]
|
|
593
|
+
#
|
|
594
|
+
def build
|
|
595
|
+
requester = @requester || HTTPFDv1PollingRequester.new(@sdk_key, @config)
|
|
596
|
+
PollingDataSource.new(@config.poll_interval, requester, @config.logger)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|