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,108 @@
|
|
|
1
|
+
require "ldclient-rb/impl/repeating_task"
|
|
2
|
+
require "ldclient-rb/impl/util"
|
|
3
|
+
|
|
4
|
+
require "concurrent/atomics"
|
|
5
|
+
require "json"
|
|
6
|
+
require "thread"
|
|
7
|
+
|
|
8
|
+
module LaunchDarkly
|
|
9
|
+
module Impl
|
|
10
|
+
module DataSource
|
|
11
|
+
class PollingProcessor
|
|
12
|
+
def initialize(config, requestor)
|
|
13
|
+
@config = config
|
|
14
|
+
@requestor = requestor
|
|
15
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
|
16
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
|
17
|
+
@ready = Concurrent::Event.new
|
|
18
|
+
@task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger, 'LD/PollingDataSource')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialized?
|
|
22
|
+
@initialized.value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
return @ready unless @started.make_true
|
|
27
|
+
@config.logger.info { "[LDClient] Initializing polling connection" }
|
|
28
|
+
@task.start
|
|
29
|
+
@ready
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stop
|
|
33
|
+
stop_with_error_info
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def poll
|
|
37
|
+
begin
|
|
38
|
+
all_data = @requestor.request_all_data
|
|
39
|
+
if all_data
|
|
40
|
+
update_sink_or_data_store.init(all_data)
|
|
41
|
+
if @initialized.make_true
|
|
42
|
+
@config.logger.info { "[LDClient] Polling connection initialized" }
|
|
43
|
+
@ready.set
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
@config.logger.error { "[LDClient] JSON parsing failed for polling response." }
|
|
49
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
50
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
|
51
|
+
0,
|
|
52
|
+
e.to_s,
|
|
53
|
+
Time.now
|
|
54
|
+
)
|
|
55
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
|
56
|
+
rescue Impl::DataSource::UnexpectedResponseError => e
|
|
57
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
58
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, e.status, nil, Time.now)
|
|
59
|
+
message = Util.http_error_message(e.status, "polling request", "will retry")
|
|
60
|
+
@config.logger.error { "[LDClient] #{message}" }
|
|
61
|
+
|
|
62
|
+
if Util.http_error_recoverable?(e.status)
|
|
63
|
+
@config.data_source_update_sink&.update_status(
|
|
64
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
65
|
+
error_info
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
|
69
|
+
stop_with_error_info error_info
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
Impl::Util.log_exception(@config.logger, "Exception while polling", e)
|
|
73
|
+
@config.data_source_update_sink&.update_status(
|
|
74
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
75
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, e.to_s, Time.now)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# The original implementation of this class relied on the feature store
|
|
82
|
+
# directly, which we are trying to move away from. Customers who might have
|
|
83
|
+
# instantiated this directly for some reason wouldn't know they have to set
|
|
84
|
+
# the config's sink manually, so we have to fall back to the store if the
|
|
85
|
+
# sink isn't present.
|
|
86
|
+
#
|
|
87
|
+
# The next major release should be able to simplify this structure and
|
|
88
|
+
# remove the need for fall back to the data store because the update sink
|
|
89
|
+
# should always be present.
|
|
90
|
+
#
|
|
91
|
+
private def update_sink_or_data_store
|
|
92
|
+
@config.data_source_update_sink || @config.feature_store
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
|
|
97
|
+
#
|
|
98
|
+
private def stop_with_error_info(error_info = nil)
|
|
99
|
+
@task.stop
|
|
100
|
+
@requestor.stop if @requestor.respond_to?(:stop)
|
|
101
|
+
@config.logger.info { "[LDClient] Polling connection stopped" }
|
|
102
|
+
@config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
|
2
|
+
require "ldclient-rb/impl/util"
|
|
3
|
+
|
|
4
|
+
require "concurrent/atomics"
|
|
5
|
+
require "json"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "http"
|
|
8
|
+
|
|
9
|
+
module LaunchDarkly
|
|
10
|
+
module Impl
|
|
11
|
+
module DataSource
|
|
12
|
+
class UnexpectedResponseError < StandardError
|
|
13
|
+
def initialize(status)
|
|
14
|
+
@status = status
|
|
15
|
+
super("HTTP error #{status}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def status
|
|
19
|
+
@status
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Requestor
|
|
24
|
+
CacheEntry = Struct.new(:etag, :body)
|
|
25
|
+
|
|
26
|
+
def initialize(sdk_key, config)
|
|
27
|
+
@sdk_key = sdk_key
|
|
28
|
+
@config = config
|
|
29
|
+
@http_client = Impl::Util.new_http_client(config.base_uri, config)
|
|
30
|
+
.use(:auto_inflate)
|
|
31
|
+
.headers("Accept-Encoding" => "gzip")
|
|
32
|
+
@cache = @config.cache_store
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def request_all_data()
|
|
36
|
+
all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
|
|
37
|
+
Impl::Model.make_all_store_data(all_data, @config.logger)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stop
|
|
41
|
+
begin
|
|
42
|
+
@http_client.close
|
|
43
|
+
rescue
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def make_request(path)
|
|
50
|
+
uri = URI(
|
|
51
|
+
Util.add_payload_filter_key(@config.base_uri + path, @config)
|
|
52
|
+
)
|
|
53
|
+
headers = {}
|
|
54
|
+
Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
|
|
55
|
+
headers["Connection"] = "keep-alive"
|
|
56
|
+
cached = @cache.read(uri)
|
|
57
|
+
unless cached.nil?
|
|
58
|
+
headers["If-None-Match"] = cached.etag
|
|
59
|
+
end
|
|
60
|
+
response = @http_client.request("GET", uri, {
|
|
61
|
+
headers: headers,
|
|
62
|
+
})
|
|
63
|
+
status = response.status.code
|
|
64
|
+
# must fully read body for persistent connections
|
|
65
|
+
body = response.to_s
|
|
66
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers.to_h}\n\tbody: #{body}" }
|
|
67
|
+
if status == 304 && !cached.nil?
|
|
68
|
+
body = cached.body
|
|
69
|
+
else
|
|
70
|
+
@cache.delete(uri)
|
|
71
|
+
if status < 200 || status >= 300
|
|
72
|
+
raise UnexpectedResponseError.new(status)
|
|
73
|
+
end
|
|
74
|
+
body = fix_encoding(body, response.headers["content-type"])
|
|
75
|
+
etag = response.headers["etag"]
|
|
76
|
+
@cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
|
|
77
|
+
end
|
|
78
|
+
body
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def fix_encoding(body, content_type)
|
|
82
|
+
return body if content_type.nil?
|
|
83
|
+
media_type, charset = parse_content_type(content_type)
|
|
84
|
+
return body if charset.nil?
|
|
85
|
+
body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_content_type(value)
|
|
89
|
+
return [nil, nil] if value.nil? || value == ''
|
|
90
|
+
parts = value.split(/; */)
|
|
91
|
+
return [value, nil] if parts.count < 2
|
|
92
|
+
charset = nil
|
|
93
|
+
parts.each do |part|
|
|
94
|
+
fields = part.split('=')
|
|
95
|
+
if fields.count >= 2 && fields[0] == 'charset'
|
|
96
|
+
charset = fields[1]
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
[parts[0], charset]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
require "ldclient-rb/interfaces"
|
|
6
|
+
|
|
7
|
+
module LaunchDarkly
|
|
8
|
+
module Impl
|
|
9
|
+
module DataSource
|
|
10
|
+
#
|
|
11
|
+
# Provides status tracking and listener management for data sources.
|
|
12
|
+
#
|
|
13
|
+
# This class implements the {LaunchDarkly::Interfaces::DataSource::StatusProvider} interface.
|
|
14
|
+
# It maintains the current status of the data source and broadcasts status changes to listeners.
|
|
15
|
+
#
|
|
16
|
+
class StatusProviderV2
|
|
17
|
+
include LaunchDarkly::Interfaces::DataSource::StatusProvider
|
|
18
|
+
|
|
19
|
+
extend Forwardable
|
|
20
|
+
def_delegators :@status_broadcaster, :add_listener, :remove_listener
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Creates a new status provider.
|
|
24
|
+
#
|
|
25
|
+
# @param status_broadcaster [LaunchDarkly::Impl::Broadcaster] Broadcaster for status changes
|
|
26
|
+
#
|
|
27
|
+
def initialize(status_broadcaster)
|
|
28
|
+
@status_broadcaster = status_broadcaster
|
|
29
|
+
@status = LaunchDarkly::Interfaces::DataSource::Status.new(
|
|
30
|
+
LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
|
|
31
|
+
Time.now,
|
|
32
|
+
nil
|
|
33
|
+
)
|
|
34
|
+
@lock = Concurrent::ReadWriteLock.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# (see LaunchDarkly::Interfaces::DataSource::StatusProvider#status)
|
|
38
|
+
def status
|
|
39
|
+
@lock.with_read_lock do
|
|
40
|
+
@status
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# (see LaunchDarkly::Interfaces::DataSource::UpdateSink#update_status)
|
|
45
|
+
def update_status(new_state, new_error)
|
|
46
|
+
status_to_broadcast = nil
|
|
47
|
+
|
|
48
|
+
@lock.with_write_lock do
|
|
49
|
+
old_status = @status
|
|
50
|
+
|
|
51
|
+
# Special handling: INTERRUPTED during INITIALIZING stays INITIALIZING
|
|
52
|
+
if new_state == LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED &&
|
|
53
|
+
old_status.state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
|
|
54
|
+
new_state = LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# No change if state is the same and no error
|
|
58
|
+
return if new_state == old_status.state && new_error.nil?
|
|
59
|
+
|
|
60
|
+
new_since = new_state == old_status.state ? @status.state_since : Time.now
|
|
61
|
+
new_error = @status.last_error if new_error.nil?
|
|
62
|
+
|
|
63
|
+
@status = LaunchDarkly::Interfaces::DataSource::Status.new(
|
|
64
|
+
new_state,
|
|
65
|
+
new_since,
|
|
66
|
+
new_error
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
status_to_broadcast = @status
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@status_broadcaster.broadcast(status_to_broadcast) if status_to_broadcast
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
require "ldclient-rb/impl/model/serialization"
|
|
2
|
+
require "ldclient-rb/impl/util"
|
|
3
|
+
require "ldclient-rb/in_memory_store"
|
|
4
|
+
|
|
5
|
+
require "concurrent/atomics"
|
|
6
|
+
require "json"
|
|
7
|
+
require "ld-eventsource"
|
|
8
|
+
|
|
9
|
+
module LaunchDarkly
|
|
10
|
+
module Impl
|
|
11
|
+
module DataSource
|
|
12
|
+
PUT = :put
|
|
13
|
+
PATCH = :patch
|
|
14
|
+
DELETE = :delete
|
|
15
|
+
READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
|
|
16
|
+
|
|
17
|
+
KEY_PATHS = {
|
|
18
|
+
Impl::DataStore::FEATURES => "/flags/",
|
|
19
|
+
Impl::DataStore::SEGMENTS => "/segments/",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class StreamProcessor
|
|
23
|
+
def initialize(sdk_key, config, diagnostic_accumulator = nil)
|
|
24
|
+
@sdk_key = sdk_key
|
|
25
|
+
@config = config
|
|
26
|
+
@diagnostic_accumulator = diagnostic_accumulator
|
|
27
|
+
@data_source_update_sink = config.data_source_update_sink
|
|
28
|
+
@feature_store = config.feature_store
|
|
29
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
|
30
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
|
31
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
|
32
|
+
@ready = Concurrent::Event.new
|
|
33
|
+
@connection_attempt_start_time = 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialized?
|
|
37
|
+
@initialized.value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start
|
|
41
|
+
return @ready unless @started.make_true
|
|
42
|
+
|
|
43
|
+
@config.logger.info { "[LDClient] Initializing stream connection" }
|
|
44
|
+
|
|
45
|
+
headers = Impl::Util.default_http_headers(@sdk_key, @config)
|
|
46
|
+
opts = {
|
|
47
|
+
headers: headers,
|
|
48
|
+
read_timeout: READ_TIMEOUT_SECONDS,
|
|
49
|
+
logger: @config.logger,
|
|
50
|
+
socket_factory: @config.socket_factory,
|
|
51
|
+
reconnect_time: @config.initial_reconnect_delay,
|
|
52
|
+
}
|
|
53
|
+
log_connection_started
|
|
54
|
+
|
|
55
|
+
uri = Impl::Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
|
|
56
|
+
@es = SSE::Client.new(uri, **opts) do |conn|
|
|
57
|
+
conn.on_event { |event| process_message(event) }
|
|
58
|
+
conn.on_error { |err|
|
|
59
|
+
log_connection_result(false)
|
|
60
|
+
case err
|
|
61
|
+
when SSE::Errors::HTTPStatusError
|
|
62
|
+
status = err.status
|
|
63
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
64
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, status, nil, Time.now)
|
|
65
|
+
message = Util.http_error_message(status, "streaming connection", "will retry")
|
|
66
|
+
@config.logger.error { "[LDClient] #{message}" }
|
|
67
|
+
|
|
68
|
+
if Util.http_error_recoverable?(status)
|
|
69
|
+
@data_source_update_sink&.update_status(
|
|
70
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
71
|
+
error_info
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
|
75
|
+
stop_with_error_info error_info
|
|
76
|
+
end
|
|
77
|
+
when SSE::Errors::HTTPContentTypeError, SSE::Errors::HTTPProxyError, SSE::Errors::ReadTimeoutError
|
|
78
|
+
@data_source_update_sink&.update_status(
|
|
79
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
80
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR, 0, err.to_s, Time.now)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
else
|
|
84
|
+
@data_source_update_sink&.update_status(
|
|
85
|
+
LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
|
|
86
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, err.to_s, Time.now)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@ready
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def stop
|
|
96
|
+
stop_with_error_info
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
|
|
103
|
+
#
|
|
104
|
+
def stop_with_error_info(error_info = nil)
|
|
105
|
+
if @stopped.make_true
|
|
106
|
+
@es.close
|
|
107
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
|
|
108
|
+
@config.logger.info { "[LDClient] Stream connection stopped" }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
#
|
|
113
|
+
# The original implementation of this class relied on the feature store
|
|
114
|
+
# directly, which we are trying to move away from. Customers who might have
|
|
115
|
+
# instantiated this directly for some reason wouldn't know they have to set
|
|
116
|
+
# the config's sink manually, so we have to fall back to the store if the
|
|
117
|
+
# sink isn't present.
|
|
118
|
+
#
|
|
119
|
+
# The next major release should be able to simplify this structure and
|
|
120
|
+
# remove the need for fall back to the data store because the update sink
|
|
121
|
+
# should always be present.
|
|
122
|
+
#
|
|
123
|
+
def update_sink_or_data_store
|
|
124
|
+
@data_source_update_sink || @feature_store
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def process_message(message)
|
|
128
|
+
log_connection_result(true)
|
|
129
|
+
method = message.type
|
|
130
|
+
@config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
if method == PUT
|
|
134
|
+
message = JSON.parse(message.data, symbolize_names: true)
|
|
135
|
+
all_data = Impl::Model.make_all_store_data(message[:data], @config.logger)
|
|
136
|
+
update_sink_or_data_store.init(all_data)
|
|
137
|
+
@initialized.make_true
|
|
138
|
+
@config.logger.info { "[LDClient] Stream initialized" }
|
|
139
|
+
@ready.set
|
|
140
|
+
elsif method == PATCH
|
|
141
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
|
142
|
+
for kind in [Impl::DataStore::FEATURES, Impl::DataStore::SEGMENTS]
|
|
143
|
+
key = key_for_path(kind, data[:path])
|
|
144
|
+
if key
|
|
145
|
+
item = Impl::Model.deserialize(kind, data[:data], @config.logger)
|
|
146
|
+
update_sink_or_data_store.upsert(kind, item)
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
elsif method == DELETE
|
|
151
|
+
data = JSON.parse(message.data, symbolize_names: true)
|
|
152
|
+
for kind in [Impl::DataStore::FEATURES, Impl::DataStore::SEGMENTS]
|
|
153
|
+
key = key_for_path(kind, data[:path])
|
|
154
|
+
if key
|
|
155
|
+
update_sink_or_data_store.delete(kind, key, data[:version])
|
|
156
|
+
break
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
@config.logger.warn { "[LDClient] Unknown message received: #{method}" }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
|
|
164
|
+
rescue JSON::ParserError => e
|
|
165
|
+
@config.logger.error { "[LDClient] JSON parsing failed for method #{method}. Ignoring event." }
|
|
166
|
+
error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
167
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
|
|
168
|
+
0,
|
|
169
|
+
e.to_s,
|
|
170
|
+
Time.now
|
|
171
|
+
)
|
|
172
|
+
@data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
|
|
173
|
+
|
|
174
|
+
# Re-raise the exception so the SSE implementation can catch it and restart the stream.
|
|
175
|
+
raise
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def key_for_path(kind, path)
|
|
180
|
+
path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def log_connection_started
|
|
184
|
+
@connection_attempt_start_time = Impl::Util::current_time_millis
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def log_connection_result(is_success)
|
|
188
|
+
if !@diagnostic_accumulator.nil? && @connection_attempt_start_time > 0
|
|
189
|
+
@diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success,
|
|
190
|
+
Impl::Util::current_time_millis - @connection_attempt_start_time)
|
|
191
|
+
@connection_attempt_start_time = 0
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
@@ -52,7 +52,7 @@ module LaunchDarkly
|
|
|
52
52
|
monitor_store_update do
|
|
53
53
|
if @flag_change_broadcaster.has_listeners?
|
|
54
54
|
old_data = {}
|
|
55
|
-
|
|
55
|
+
Impl::DataStore::ALL_KINDS.each do |kind|
|
|
56
56
|
old_data[kind] = @data_store.all(kind)
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -153,7 +153,7 @@ module LaunchDarkly
|
|
|
153
153
|
private def compute_changed_items_for_full_data_set(old_data, new_data)
|
|
154
154
|
affected_items = Set.new
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
Impl::DataStore::ALL_KINDS.each do |kind|
|
|
157
157
|
old_items = old_data[kind] || {}
|
|
158
158
|
new_items = new_data[kind] || {}
|
|
159
159
|
|
|
@@ -177,7 +177,7 @@ module LaunchDarkly
|
|
|
177
177
|
#
|
|
178
178
|
private def send_change_events(affected_items)
|
|
179
179
|
affected_items.each do |item|
|
|
180
|
-
if item[:kind] ==
|
|
180
|
+
if item[:kind] == Impl::DataStore::FEATURES
|
|
181
181
|
@flag_change_broadcaster.broadcast(LaunchDarkly::Interfaces::FlagChange.new(item[:key]))
|
|
182
182
|
end
|
|
183
183
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require 'concurrent'
|
|
2
|
+
require "ldclient-rb/interfaces"
|
|
3
|
+
|
|
4
|
+
module LaunchDarkly
|
|
5
|
+
module Impl
|
|
6
|
+
module DataStore
|
|
7
|
+
class DataKind
|
|
8
|
+
FEATURES = "features".freeze
|
|
9
|
+
SEGMENTS = "segments".freeze
|
|
10
|
+
|
|
11
|
+
FEATURE_PREREQ_FN = lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :namespace
|
|
14
|
+
attr_reader :priority
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# @param namespace [String]
|
|
18
|
+
# @param priority [Integer]
|
|
19
|
+
#
|
|
20
|
+
def initialize(namespace:, priority:)
|
|
21
|
+
@namespace = namespace
|
|
22
|
+
@priority = priority
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# Maintain the same behavior when these data kinds were standard ruby hashes.
|
|
27
|
+
#
|
|
28
|
+
# @param key [Symbol]
|
|
29
|
+
# @return [Object]
|
|
30
|
+
#
|
|
31
|
+
def [](key)
|
|
32
|
+
return priority if key == :priority
|
|
33
|
+
return namespace if key == :namespace
|
|
34
|
+
return get_dependency_keys_fn() if key == :get_dependency_keys
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Retrieve the dependency keys for a particular data kind. Right now, this is only defined for flags.
|
|
40
|
+
#
|
|
41
|
+
def get_dependency_keys_fn()
|
|
42
|
+
return nil unless @namespace == FEATURES
|
|
43
|
+
|
|
44
|
+
FEATURE_PREREQ_FN
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def eql?(other)
|
|
48
|
+
other.is_a?(DataKind) && namespace == other.namespace && priority == other.priority
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hash
|
|
52
|
+
[namespace, priority].hash
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class StatusProvider
|
|
57
|
+
include LaunchDarkly::Interfaces::DataStore::StatusProvider
|
|
58
|
+
|
|
59
|
+
def initialize(store, update_sink)
|
|
60
|
+
# @type [LaunchDarkly::Impl::FeatureStoreClientWrapper]
|
|
61
|
+
@store = store
|
|
62
|
+
# @type [UpdateSink]
|
|
63
|
+
@update_sink = update_sink
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def status
|
|
67
|
+
@update_sink.last_status.get
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def monitoring_enabled?
|
|
71
|
+
@store.monitoring_enabled?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_listener(listener)
|
|
75
|
+
@update_sink.broadcaster.add_listener(listener)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def remove_listener(listener)
|
|
79
|
+
@update_sink.broadcaster.remove_listener(listener)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class UpdateSink
|
|
84
|
+
include LaunchDarkly::Interfaces::DataStore::UpdateSink
|
|
85
|
+
|
|
86
|
+
# @return [LaunchDarkly::Impl::Broadcaster]
|
|
87
|
+
attr_reader :broadcaster
|
|
88
|
+
|
|
89
|
+
# @return [Concurrent::AtomicReference]
|
|
90
|
+
attr_reader :last_status
|
|
91
|
+
|
|
92
|
+
def initialize(broadcaster)
|
|
93
|
+
@broadcaster = broadcaster
|
|
94
|
+
@last_status = Concurrent::AtomicReference.new(
|
|
95
|
+
LaunchDarkly::Interfaces::DataStore::Status.new(true, false)
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def update_status(status)
|
|
100
|
+
return if status.nil?
|
|
101
|
+
|
|
102
|
+
old_status = @last_status.get_and_set(status)
|
|
103
|
+
@broadcaster.broadcast(status) unless old_status == status
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|