launchdarkly-server-sdk 7.0.2 → 8.4.2
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 +9 -4
- data/lib/ldclient-rb/config.rb +50 -70
- data/lib/ldclient-rb/context.rb +65 -50
- data/lib/ldclient-rb/evaluation_detail.rb +5 -1
- data/lib/ldclient-rb/events.rb +81 -8
- data/lib/ldclient-rb/impl/big_segments.rb +1 -1
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +3 -3
- data/lib/ldclient-rb/impl/context_filter.rb +30 -9
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
- data/lib/ldclient-rb/impl/event_sender.rb +1 -0
- data/lib/ldclient-rb/impl/event_types.rb +61 -3
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +12 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +8 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +16 -3
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +19 -2
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +25 -3
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -3
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/in_memory_store.rb +7 -0
- data/lib/ldclient-rb/integrations/file_data.rb +1 -1
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +84 -15
- data/lib/ldclient-rb/integrations/test_data.rb +3 -3
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +11 -0
- data/lib/ldclient-rb/interfaces.rb +671 -0
- data/lib/ldclient-rb/ldclient.rb +313 -22
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/polling.rb +51 -5
- data/lib/ldclient-rb/reference.rb +11 -0
- data/lib/ldclient-rb/requestor.rb +5 -5
- data/lib/ldclient-rb/stream.rb +91 -29
- data/lib/ldclient-rb/util.rb +89 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +1 -0
- metadata +44 -6
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'ldclient-rb/impl/migrations/migrator'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
#
|
5
|
+
# Namespace for feature-flag based technology migration support.
|
6
|
+
#
|
7
|
+
module Migrations
|
8
|
+
# Symbol representing the old origin, or the old technology source you are migrating away from.
|
9
|
+
ORIGIN_OLD = :old
|
10
|
+
# Symbol representing the new origin, or the new technology source you are migrating towards.
|
11
|
+
ORIGIN_NEW = :new
|
12
|
+
|
13
|
+
# Symbol defining a read-related operation
|
14
|
+
OP_READ = :read
|
15
|
+
# Symbol defining a write-related operation
|
16
|
+
OP_WRITE = :write
|
17
|
+
|
18
|
+
STAGE_OFF = :off
|
19
|
+
STAGE_DUALWRITE = :dualwrite
|
20
|
+
STAGE_SHADOW = :shadow
|
21
|
+
STAGE_LIVE = :live
|
22
|
+
STAGE_RAMPDOWN = :rampdown
|
23
|
+
STAGE_COMPLETE = :complete
|
24
|
+
|
25
|
+
VALID_OPERATIONS = [
|
26
|
+
OP_READ,
|
27
|
+
OP_WRITE,
|
28
|
+
]
|
29
|
+
|
30
|
+
VALID_ORIGINS = [
|
31
|
+
ORIGIN_OLD,
|
32
|
+
ORIGIN_NEW,
|
33
|
+
]
|
34
|
+
|
35
|
+
VALID_STAGES = [
|
36
|
+
STAGE_OFF,
|
37
|
+
STAGE_DUALWRITE,
|
38
|
+
STAGE_SHADOW,
|
39
|
+
STAGE_LIVE,
|
40
|
+
STAGE_RAMPDOWN,
|
41
|
+
STAGE_COMPLETE,
|
42
|
+
]
|
43
|
+
|
44
|
+
#
|
45
|
+
# The OperationResult wraps the {LaunchDarkly::Result} class to tie an operation origin to a result.
|
46
|
+
#
|
47
|
+
class OperationResult
|
48
|
+
extend Forwardable
|
49
|
+
def_delegators :@result, :value, :error, :exception, :success?
|
50
|
+
|
51
|
+
#
|
52
|
+
# @param origin [Symbol]
|
53
|
+
# @param result [LaunchDarkly::Result]
|
54
|
+
#
|
55
|
+
def initialize(origin, result)
|
56
|
+
@origin = origin
|
57
|
+
@result = result
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# @return [Symbol] The origin this result is associated with.
|
62
|
+
#
|
63
|
+
attr_reader :origin
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# A write result contains the operation results against both the authoritative and non-authoritative origins.
|
68
|
+
#
|
69
|
+
# Authoritative writes are always executed first. In the event of a failure, the non-authoritative write will not
|
70
|
+
# be executed, resulting in a nil value in the final WriteResult.
|
71
|
+
#
|
72
|
+
class WriteResult
|
73
|
+
#
|
74
|
+
# @param authoritative [OperationResult]
|
75
|
+
# @param nonauthoritative [OperationResult, nil]
|
76
|
+
#
|
77
|
+
def initialize(authoritative, nonauthoritative = nil)
|
78
|
+
@authoritative = authoritative
|
79
|
+
@nonauthoritative = nonauthoritative
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Returns the operation result for the authoritative origin.
|
84
|
+
#
|
85
|
+
# @return [OperationResult]
|
86
|
+
#
|
87
|
+
attr_reader :authoritative
|
88
|
+
|
89
|
+
#
|
90
|
+
# Returns the operation result for the non-authoritative origin.
|
91
|
+
#
|
92
|
+
# This result might be nil as the non-authoritative write does not execute in every stage, and will not execute
|
93
|
+
# if the authoritative write failed.
|
94
|
+
#
|
95
|
+
# @return [OperationResult, nil]
|
96
|
+
#
|
97
|
+
attr_reader :nonauthoritative
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
#
|
102
|
+
# The migration builder is used to configure and construct an instance of a
|
103
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator}. This migrator can be used to perform LaunchDarkly assisted
|
104
|
+
# technology migrations through the use of migration-based feature flags.
|
105
|
+
#
|
106
|
+
class MigratorBuilder
|
107
|
+
EXECUTION_SERIAL = :serial
|
108
|
+
EXECUTION_RANDOM = :random
|
109
|
+
EXECUTION_PARALLEL = :parallel
|
110
|
+
|
111
|
+
VALID_EXECUTION_ORDERS = [EXECUTION_SERIAL, EXECUTION_RANDOM, EXECUTION_PARALLEL]
|
112
|
+
private_constant :VALID_EXECUTION_ORDERS
|
113
|
+
|
114
|
+
#
|
115
|
+
# @param client [LaunchDarkly::LDClient]
|
116
|
+
#
|
117
|
+
def initialize(client)
|
118
|
+
@client = client
|
119
|
+
|
120
|
+
# Default settings as required by the spec
|
121
|
+
@read_execution_order = EXECUTION_PARALLEL
|
122
|
+
@measure_latency = true
|
123
|
+
@measure_errors = true
|
124
|
+
|
125
|
+
@read_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
|
126
|
+
@write_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# The read execution order influences the parallelism and execution order for read operations involving multiple
|
131
|
+
# origins.
|
132
|
+
#
|
133
|
+
# @param order [Symbol]
|
134
|
+
#
|
135
|
+
def read_execution_order(order)
|
136
|
+
return unless VALID_EXECUTION_ORDERS.include? order
|
137
|
+
|
138
|
+
@read_execution_order = order
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Enable or disable latency tracking for migration operations. This latency information can be sent upstream to
|
143
|
+
# LaunchDarkly to enhance migration visibility.
|
144
|
+
#
|
145
|
+
# @param enabled [Boolean]
|
146
|
+
#
|
147
|
+
def track_latency(enabled)
|
148
|
+
@measure_latency = !!enabled
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Enable or disable error tracking for migration operations. This error information can be sent upstream to
|
153
|
+
# LaunchDarkly to enhance migration visibility.
|
154
|
+
#
|
155
|
+
# @param enabled [Boolean]
|
156
|
+
#
|
157
|
+
def track_errors(enabled)
|
158
|
+
@measure_errors = !!enabled
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Read can be used to configure the migration-read behavior of the resulting
|
163
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
|
164
|
+
#
|
165
|
+
# Users are required to provide two different read methods -- one to read from the old migration origin, and one
|
166
|
+
# to read from the new origin. Additionally, customers can opt-in to consistency tracking by providing a
|
167
|
+
# comparison function.
|
168
|
+
#
|
169
|
+
# Depending on the migration stage, one or both of these read methods may be called.
|
170
|
+
#
|
171
|
+
# The read methods should accept a single nullable parameter. This parameter is a payload passed through the
|
172
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator#read} method. This method should return a {LaunchDarkly::Result}
|
173
|
+
# instance.
|
174
|
+
#
|
175
|
+
# The consistency method should accept 2 parameters of any type. These parameters are the results of executing the
|
176
|
+
# read operation against the old and new origins. If both operations were successful, the consistency method will
|
177
|
+
# be invoked. This method should return true if the two parameters are equal, or false otherwise.
|
178
|
+
#
|
179
|
+
# @param old_read [#call]
|
180
|
+
# @param new_read [#call]
|
181
|
+
# @param comparison [#call, nil]
|
182
|
+
#
|
183
|
+
def read(old_read, new_read, comparison = nil)
|
184
|
+
return unless old_read.respond_to?(:call) && old_read.arity == 1
|
185
|
+
return unless new_read.respond_to?(:call) && new_read.arity == 1
|
186
|
+
return unless comparison.nil? || (comparison.respond_to?(:call) && comparison.arity == 2)
|
187
|
+
|
188
|
+
@read_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_read, new_read, comparison)
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Write can be used to configure the migration-write behavior of the resulting
|
193
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
|
194
|
+
#
|
195
|
+
# Users are required to provide two different write methods -- one to write to the old migration origin, and one
|
196
|
+
# to write to the new origin.
|
197
|
+
#
|
198
|
+
# Depending on the migration stage, one or both of these write methods may be called.
|
199
|
+
#
|
200
|
+
# The write methods should accept a single nullable parameter. This parameter is a payload passed through the
|
201
|
+
# {LaunchDarkly::Interfaces::Migrations::Migrator#write} method. This method should return a {LaunchDarkly::Result}
|
202
|
+
# instance.
|
203
|
+
#
|
204
|
+
# @param old_write [#call]
|
205
|
+
# @param new_write [#call]
|
206
|
+
#
|
207
|
+
def write(old_write, new_write)
|
208
|
+
return unless old_write.respond_to?(:call) && old_write.arity == 1
|
209
|
+
return unless new_write.respond_to?(:call) && new_write.arity == 1
|
210
|
+
|
211
|
+
@write_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_write, new_write, nil)
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Build constructs a {LaunchDarkly::Interfaces::Migrations::Migrator} instance to support migration-based reads
|
216
|
+
# and writes. A string describing any failure conditions will be returned if the build fails.
|
217
|
+
#
|
218
|
+
# @return [LaunchDarkly::Interfaces::Migrations::Migrator, string]
|
219
|
+
#
|
220
|
+
def build
|
221
|
+
return "client not provided" if @client.nil?
|
222
|
+
return "read configuration not provided" if @read_config.nil?
|
223
|
+
return "write configuration not provided" if @write_config.nil?
|
224
|
+
|
225
|
+
LaunchDarkly::Impl::Migrations::Migrator.new(@client, @read_execution_order, @read_config, @write_config, @measure_latency, @measure_errors)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "ldclient-rb/impl/repeating_task"
|
2
2
|
|
3
3
|
require "concurrent/atomics"
|
4
|
+
require "json"
|
4
5
|
require "thread"
|
5
6
|
|
6
7
|
module LaunchDarkly
|
@@ -27,30 +28,75 @@ module LaunchDarkly
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def stop
|
30
|
-
|
31
|
-
@config.logger.info { "[LDClient] Polling connection stopped" }
|
31
|
+
stop_with_error_info
|
32
32
|
end
|
33
33
|
|
34
34
|
def poll
|
35
35
|
begin
|
36
36
|
all_data = @requestor.request_all_data
|
37
37
|
if all_data
|
38
|
-
|
38
|
+
update_sink_or_data_store.init(all_data)
|
39
39
|
if @initialized.make_true
|
40
40
|
@config.logger.info { "[LDClient] Polling connection initialized" }
|
41
41
|
@ready.set
|
42
42
|
end
|
43
43
|
end
|
44
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
45
|
+
rescue JSON::ParserError => e
|
46
|
+
@config.logger.error { "[LDClient] JSON parsing failed for polling response." }
|
47
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
48
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
49
|
+
0,
|
50
|
+
e.to_s,
|
51
|
+
Time.now
|
52
|
+
)
|
53
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
44
54
|
rescue UnexpectedResponseError => e
|
55
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
56
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, e.status, nil, Time.now)
|
45
57
|
message = Util.http_error_message(e.status, "polling request", "will retry")
|
46
58
|
@config.logger.error { "[LDClient] #{message}" }
|
47
|
-
|
59
|
+
|
60
|
+
if Util.http_error_recoverable?(e.status)
|
61
|
+
@config.data_source_update_sink&.update_status(
|
62
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
63
|
+
error_info
|
64
|
+
)
|
65
|
+
else
|
48
66
|
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
49
|
-
|
67
|
+
stop_with_error_info error_info
|
50
68
|
end
|
51
69
|
rescue StandardError => e
|
52
70
|
Util.log_exception(@config.logger, "Exception while polling", e)
|
71
|
+
@config.data_source_update_sink&.update_status(
|
72
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
73
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, e.to_s, Time.now)
|
74
|
+
)
|
53
75
|
end
|
54
76
|
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# The original implementation of this class relied on the feature store
|
80
|
+
# directly, which we are trying to move away from. Customers who might have
|
81
|
+
# instantiated this directly for some reason wouldn't know they have to set
|
82
|
+
# the config's sink manually, so we have to fall back to the store if the
|
83
|
+
# sink isn't present.
|
84
|
+
#
|
85
|
+
# The next major release should be able to simplify this structure and
|
86
|
+
# remove the need for fall back to the data store because the update sink
|
87
|
+
# should always be present.
|
88
|
+
#
|
89
|
+
private def update_sink_or_data_store
|
90
|
+
@config.data_source_update_sink || @config.feature_store
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
|
95
|
+
#
|
96
|
+
private def stop_with_error_info(error_info = nil)
|
97
|
+
@task.stop
|
98
|
+
@config.logger.info { "[LDClient] Polling connection stopped" }
|
99
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
|
100
|
+
end
|
55
101
|
end
|
56
102
|
end
|
@@ -109,6 +109,8 @@ module LaunchDarkly
|
|
109
109
|
end
|
110
110
|
private_class_method :new
|
111
111
|
|
112
|
+
protected attr_reader :components
|
113
|
+
|
112
114
|
#
|
113
115
|
# Creates a Reference from a string. For the supported syntax and examples,
|
114
116
|
# see comments on the Reference type.
|
@@ -227,6 +229,15 @@ module LaunchDarkly
|
|
227
229
|
@components[index]
|
228
230
|
end
|
229
231
|
|
232
|
+
def ==(other)
|
233
|
+
self.error == other.error && self.components == other.components
|
234
|
+
end
|
235
|
+
alias eql? ==
|
236
|
+
|
237
|
+
def hash
|
238
|
+
([error] + components).hash
|
239
|
+
end
|
240
|
+
|
230
241
|
#
|
231
242
|
# Performs unescaping of attribute reference path components:
|
232
243
|
#
|
@@ -26,6 +26,8 @@ module LaunchDarkly
|
|
26
26
|
@sdk_key = sdk_key
|
27
27
|
@config = config
|
28
28
|
@http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
|
29
|
+
.use(:auto_inflate)
|
30
|
+
.headers("Accept-Encoding" => "gzip")
|
29
31
|
@cache = @config.cache_store
|
30
32
|
end
|
31
33
|
|
@@ -43,12 +45,10 @@ module LaunchDarkly
|
|
43
45
|
|
44
46
|
private
|
45
47
|
|
46
|
-
def request_single_item(kind, path)
|
47
|
-
Impl::Model.deserialize(kind, make_request(path), @config.logger)
|
48
|
-
end
|
49
|
-
|
50
48
|
def make_request(path)
|
51
|
-
uri = URI(
|
49
|
+
uri = URI(
|
50
|
+
Util.add_payload_filter_key(@config.base_uri + path, @config)
|
51
|
+
)
|
52
52
|
headers = {}
|
53
53
|
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
54
54
|
headers["Connection"] = "keep-alive"
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -25,6 +25,7 @@ module LaunchDarkly
|
|
25
25
|
def initialize(sdk_key, config, diagnostic_accumulator = nil)
|
26
26
|
@sdk_key = sdk_key
|
27
27
|
@config = config
|
28
|
+
@data_source_update_sink = config.data_source_update_sink
|
28
29
|
@feature_store = config.feature_store
|
29
30
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
30
31
|
@started = Concurrent::AtomicBoolean.new(false)
|
@@ -51,19 +52,40 @@ module LaunchDarkly
|
|
51
52
|
reconnect_time: @config.initial_reconnect_delay,
|
52
53
|
}
|
53
54
|
log_connection_started
|
54
|
-
|
55
|
+
|
56
|
+
uri = Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
|
57
|
+
@es = SSE::Client.new(uri, **opts) do |conn|
|
55
58
|
conn.on_event { |event| process_message(event) }
|
56
59
|
conn.on_error { |err|
|
57
60
|
log_connection_result(false)
|
58
61
|
case err
|
59
62
|
when SSE::Errors::HTTPStatusError
|
60
63
|
status = err.status
|
64
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
65
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, status, nil, Time.now)
|
61
66
|
message = Util.http_error_message(status, "streaming connection", "will retry")
|
62
67
|
@config.logger.error { "[LDClient] #{message}" }
|
63
|
-
|
68
|
+
|
69
|
+
if Util.http_error_recoverable?(status)
|
70
|
+
@data_source_update_sink&.update_status(
|
71
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
72
|
+
error_info
|
73
|
+
)
|
74
|
+
else
|
64
75
|
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
65
|
-
|
76
|
+
stop_with_error_info error_info
|
66
77
|
end
|
78
|
+
when SSE::Errors::HTTPContentTypeError, SSE::Errors::HTTPProxyError, SSE::Errors::ReadTimeoutError
|
79
|
+
@data_source_update_sink&.update_status(
|
80
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
81
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR, 0, err.to_s, Time.now)
|
82
|
+
)
|
83
|
+
|
84
|
+
else
|
85
|
+
@data_source_update_sink&.update_status(
|
86
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
87
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, err.to_s, Time.now)
|
88
|
+
)
|
67
89
|
end
|
68
90
|
}
|
69
91
|
end
|
@@ -72,46 +94,86 @@ module LaunchDarkly
|
|
72
94
|
end
|
73
95
|
|
74
96
|
def stop
|
97
|
+
stop_with_error_info
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
#
|
103
|
+
# @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
|
104
|
+
#
|
105
|
+
def stop_with_error_info(error_info = nil)
|
75
106
|
if @stopped.make_true
|
76
107
|
@es.close
|
108
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
|
77
109
|
@config.logger.info { "[LDClient] Stream connection stopped" }
|
78
110
|
end
|
79
111
|
end
|
80
112
|
|
81
|
-
|
113
|
+
#
|
114
|
+
# The original implementation of this class relied on the feature store
|
115
|
+
# directly, which we are trying to move away from. Customers who might have
|
116
|
+
# instantiated this directly for some reason wouldn't know they have to set
|
117
|
+
# the config's sink manually, so we have to fall back to the store if the
|
118
|
+
# sink isn't present.
|
119
|
+
#
|
120
|
+
# The next major release should be able to simplify this structure and
|
121
|
+
# remove the need for fall back to the data store because the update sink
|
122
|
+
# should always be present.
|
123
|
+
#
|
124
|
+
def update_sink_or_data_store
|
125
|
+
@data_source_update_sink || @feature_store
|
126
|
+
end
|
82
127
|
|
83
128
|
def process_message(message)
|
84
129
|
log_connection_result(true)
|
85
130
|
method = message.type
|
86
131
|
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
132
|
+
|
133
|
+
begin
|
134
|
+
if method == PUT
|
135
|
+
message = JSON.parse(message.data, symbolize_names: true)
|
136
|
+
all_data = Impl::Model.make_all_store_data(message[:data], @config.logger)
|
137
|
+
update_sink_or_data_store.init(all_data)
|
138
|
+
@initialized.make_true
|
139
|
+
@config.logger.info { "[LDClient] Stream initialized" }
|
140
|
+
@ready.set
|
141
|
+
elsif method == PATCH
|
142
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
143
|
+
for kind in [FEATURES, SEGMENTS]
|
144
|
+
key = key_for_path(kind, data[:path])
|
145
|
+
if key
|
146
|
+
item = Impl::Model.deserialize(kind, data[:data], @config.logger)
|
147
|
+
update_sink_or_data_store.upsert(kind, item)
|
148
|
+
break
|
149
|
+
end
|
102
150
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
151
|
+
elsif method == DELETE
|
152
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
153
|
+
for kind in [FEATURES, SEGMENTS]
|
154
|
+
key = key_for_path(kind, data[:path])
|
155
|
+
if key
|
156
|
+
update_sink_or_data_store.delete(kind, key, data[:version])
|
157
|
+
break
|
158
|
+
end
|
111
159
|
end
|
160
|
+
else
|
161
|
+
@config.logger.warn { "[LDClient] Unknown message received: #{method}" }
|
112
162
|
end
|
113
|
-
|
114
|
-
@
|
163
|
+
|
164
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
165
|
+
rescue JSON::ParserError => e
|
166
|
+
@config.logger.error { "[LDClient] JSON parsing failed for method #{method}. Ignoring event." }
|
167
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
168
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
169
|
+
0,
|
170
|
+
e.to_s,
|
171
|
+
Time.now
|
172
|
+
)
|
173
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
174
|
+
|
175
|
+
# Re-raise the exception so the SSE implementation can catch it and restart the stream.
|
176
|
+
raise
|
115
177
|
end
|
116
178
|
end
|
117
179
|
|
data/lib/ldclient-rb/util.rb
CHANGED
@@ -2,8 +2,97 @@ require "uri"
|
|
2
2
|
require "http"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
|
+
#
|
6
|
+
# A Result is used to reflect the outcome of any operation.
|
7
|
+
#
|
8
|
+
# Results can either be considered a success or a failure.
|
9
|
+
#
|
10
|
+
# In the event of success, the Result will contain an option, nullable value to hold any success value back to the
|
11
|
+
# calling function.
|
12
|
+
#
|
13
|
+
# If the operation fails, the Result will contain an error describing the value.
|
14
|
+
#
|
15
|
+
class Result
|
16
|
+
#
|
17
|
+
# Create a successful result with the provided value.
|
18
|
+
#
|
19
|
+
# @param value [Object, nil]
|
20
|
+
# @return [Result]
|
21
|
+
#
|
22
|
+
def self.success(value)
|
23
|
+
Result.new(value)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Create a failed result with the provided error description.
|
28
|
+
#
|
29
|
+
# @param error [String]
|
30
|
+
# @param exception [Exception, nil]
|
31
|
+
# @return [Result]
|
32
|
+
#
|
33
|
+
def self.fail(error, exception = nil)
|
34
|
+
Result.new(nil, error, exception)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Was this result successful or did it encounter an error?
|
39
|
+
#
|
40
|
+
# @return [Boolean]
|
41
|
+
#
|
42
|
+
def success?
|
43
|
+
@error.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# @return [Object, nil] The value returned from the operation if it was successful; nil otherwise.
|
48
|
+
#
|
49
|
+
attr_reader :value
|
50
|
+
|
51
|
+
#
|
52
|
+
# @return [String, nil] An error description of the failure; nil otherwise
|
53
|
+
#
|
54
|
+
attr_reader :error
|
55
|
+
|
56
|
+
#
|
57
|
+
# @return [Exception, nil] An optional exception which caused the failure
|
58
|
+
#
|
59
|
+
attr_reader :exception
|
60
|
+
|
61
|
+
private def initialize(value, error = nil, exception = nil)
|
62
|
+
@value = value
|
63
|
+
@error = error
|
64
|
+
@exception = exception
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
5
68
|
# @private
|
6
69
|
module Util
|
70
|
+
#
|
71
|
+
# Append the payload filter key query parameter to the provided URI.
|
72
|
+
#
|
73
|
+
# @param uri [String]
|
74
|
+
# @param config [Config]
|
75
|
+
# @return [String]
|
76
|
+
#
|
77
|
+
def self.add_payload_filter_key(uri, config)
|
78
|
+
return uri if config.payload_filter_key.nil?
|
79
|
+
|
80
|
+
unless config.payload_filter_key.is_a?(String) && !config.payload_filter_key.empty?
|
81
|
+
config.logger.warn { "[LDClient] Filter key must be a non-empty string. No filtering will be applied." }
|
82
|
+
return uri
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
parsed = URI.parse(uri)
|
87
|
+
new_query_params = URI.decode_www_form(String(parsed.query)) << ["filter", config.payload_filter_key]
|
88
|
+
parsed.query = URI.encode_www_form(new_query_params)
|
89
|
+
parsed.to_s
|
90
|
+
rescue URI::InvalidURIError
|
91
|
+
config.logger.warn { "[LDClient] URI could not be parsed. No filtering will be applied." }
|
92
|
+
uri
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
7
96
|
def self.new_http_client(uri_s, config)
|
8
97
|
http_client_options = {}
|
9
98
|
if config.socket_factory
|
data/lib/ldclient-rb/version.rb
CHANGED
data/lib/ldclient-rb.rb
CHANGED
@@ -9,6 +9,7 @@ require "ldclient-rb/version"
|
|
9
9
|
require "ldclient-rb/interfaces"
|
10
10
|
require "ldclient-rb/util"
|
11
11
|
require "ldclient-rb/flags_state"
|
12
|
+
require "ldclient-rb/migrations"
|
12
13
|
require "ldclient-rb/ldclient"
|
13
14
|
require "ldclient-rb/cache_store"
|
14
15
|
require "ldclient-rb/expiring_cache"
|