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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/ldclient-rb/config.rb +24 -3
- data/lib/ldclient-rb/data_system/polling_data_source_builder.rb +7 -5
- data/lib/ldclient-rb/impl/context_filter.rb +7 -7
- data/lib/ldclient-rb/impl/data_source/polling.rb +14 -1
- data/lib/ldclient-rb/impl/data_source/requestor.rb +13 -3
- data/lib/ldclient-rb/impl/data_source/stream.rb +2 -0
- data/lib/ldclient-rb/impl/data_source.rb +37 -0
- data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +12 -0
- data/lib/ldclient-rb/impl/data_store/store.rb +24 -0
- data/lib/ldclient-rb/impl/data_system/fdv1.rb +5 -0
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +92 -13
- data/lib/ldclient-rb/impl/data_system/polling.rb +100 -33
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +3 -29
- data/lib/ldclient-rb/impl/data_system/streaming.rb +54 -31
- data/lib/ldclient-rb/impl/data_system.rb +15 -5
- data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +29 -22
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +4 -0
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +53 -48
- data/lib/ldclient-rb/integrations/consul.rb +6 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +6 -2
- data/lib/ldclient-rb/integrations/file_data.rb +1 -4
- data/lib/ldclient-rb/integrations/redis.rb +6 -2
- data/lib/ldclient-rb/integrations/test_data_v2.rb +0 -3
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +39 -19
- data/lib/ldclient-rb/interfaces/data_system.rb +60 -57
- data/lib/ldclient-rb/interfaces/hooks.rb +11 -1
- data/lib/ldclient-rb/ldclient.rb +1 -1
- data/lib/ldclient-rb/util.rb +3 -2
- data/lib/ldclient-rb/version.rb +1 -1
- 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
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
156
|
-
environment_id:
|
|
157
|
-
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
|
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::
|
|
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::
|
|
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 << ["
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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?
|
|
182
|
+
raise ArgumentError, "Missing required fields in Goodbye" if reason.nil?
|
|
206
183
|
|
|
207
|
-
new(reason: reason
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if headers
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
251
|
-
|
|
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
|
|
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,
|
|
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
|
-
@
|
|
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::
|
|
78
|
+
# @return [LaunchDarkly::Interfaces::DataSystem::FetchResult]
|
|
77
79
|
#
|
|
78
80
|
def fetch(selector_store)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
88
|
+
load_result = load_all_to_changeset
|
|
89
|
+
next load_result unless load_result.success?
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
114
|
-
unless
|
|
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
|
-
|
|
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:
|
|
136
|
+
change_set: initial_fetch.value.change_set
|
|
130
137
|
)
|
|
131
138
|
|
|
132
139
|
# Start watching for file changes
|