launchdarkly-server-sdk 8.11.2-java → 8.12.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +69 -9
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +227 -0
  5. data/lib/ldclient-rb/events.rb +34 -19
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +113 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +83 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +76 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/data_source_builder_common.rb +77 -0
  21. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  22. data/lib/ldclient-rb/impl/data_system/fdv2.rb +472 -0
  23. data/lib/ldclient-rb/impl/data_system/http_config_options.rb +32 -0
  24. data/lib/ldclient-rb/impl/data_system/polling.rb +628 -0
  25. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  26. data/lib/ldclient-rb/impl/data_system/streaming.rb +401 -0
  27. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  28. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  29. data/lib/ldclient-rb/impl/event_sender.rb +14 -6
  30. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  32. data/lib/ldclient-rb/impl/integrations/file_data_source_v2.rb +460 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +290 -0
  34. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  35. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  36. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  37. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  38. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  39. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  40. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  41. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +1 -1
  42. data/lib/ldclient-rb/impl/util.rb +71 -0
  43. data/lib/ldclient-rb/impl.rb +1 -2
  44. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  45. data/lib/ldclient-rb/integrations/file_data.rb +67 -0
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  47. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  48. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  49. data/lib/ldclient-rb/integrations/test_data_v2.rb +254 -0
  50. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  51. data/lib/ldclient-rb/interfaces/data_system.rb +704 -0
  52. data/lib/ldclient-rb/interfaces/feature_store.rb +5 -2
  53. data/lib/ldclient-rb/ldclient.rb +66 -132
  54. data/lib/ldclient-rb/util.rb +11 -70
  55. data/lib/ldclient-rb/version.rb +1 -1
  56. data/lib/ldclient-rb.rb +9 -17
  57. metadata +41 -19
  58. data/lib/ldclient-rb/cache_store.rb +0 -45
  59. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  60. data/lib/ldclient-rb/memoized_value.rb +0 -32
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  62. data/lib/ldclient-rb/polling.rb +0 -102
  63. data/lib/ldclient-rb/requestor.rb +0 -102
  64. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  65. 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,113 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+ require "ldclient-rb/impl/util"
3
+ require "ldclient-rb/impl/data_system/http_config_options"
4
+
5
+ require "concurrent/atomics"
6
+ require "json"
7
+ require "uri"
8
+ require "http"
9
+
10
+ module LaunchDarkly
11
+ module Impl
12
+ module DataSource
13
+ class UnexpectedResponseError < StandardError
14
+ def initialize(status)
15
+ @status = status
16
+ super("HTTP error #{status}")
17
+ end
18
+
19
+ def status
20
+ @status
21
+ end
22
+ end
23
+
24
+ class Requestor
25
+ CacheEntry = Struct.new(:etag, :body)
26
+
27
+ def initialize(sdk_key, config)
28
+ @sdk_key = sdk_key
29
+ @config = config
30
+ @http_config = DataSystem::HttpConfigOptions.new(
31
+ base_uri: config.base_uri,
32
+ socket_factory: config.socket_factory,
33
+ read_timeout: config.read_timeout,
34
+ connect_timeout: config.connect_timeout
35
+ )
36
+ @http_client = Impl::Util.new_http_client(@http_config)
37
+ .use(:auto_inflate)
38
+ .headers("Accept-Encoding" => "gzip")
39
+ @cache = @config.cache_store
40
+ end
41
+
42
+ def request_all_data()
43
+ all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
44
+ Impl::Model.make_all_store_data(all_data, @config.logger)
45
+ end
46
+
47
+ def stop
48
+ begin
49
+ @http_client.close
50
+ rescue
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def make_request(path)
57
+ uri = URI(
58
+ Util.add_payload_filter_key(@http_config.base_uri + path, @config)
59
+ )
60
+ headers = {}
61
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
62
+ headers["Connection"] = "keep-alive"
63
+ cached = @cache.read(uri)
64
+ unless cached.nil?
65
+ headers["If-None-Match"] = cached.etag
66
+ end
67
+ response = @http_client.request("GET", uri, {
68
+ headers: headers,
69
+ })
70
+ status = response.status.code
71
+ # must fully read body for persistent connections
72
+ body = response.to_s
73
+ @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers.to_h}\n\tbody: #{body}" }
74
+ if status == 304 && !cached.nil?
75
+ body = cached.body
76
+ else
77
+ @cache.delete(uri)
78
+ if status < 200 || status >= 300
79
+ raise UnexpectedResponseError.new(status)
80
+ end
81
+ body = fix_encoding(body, response.headers["content-type"])
82
+ etag = response.headers["etag"]
83
+ @cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
84
+ end
85
+ body
86
+ end
87
+
88
+ def fix_encoding(body, content_type)
89
+ return body if content_type.nil?
90
+ media_type, charset = parse_content_type(content_type)
91
+ return body if charset.nil?
92
+ body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
93
+ end
94
+
95
+ def parse_content_type(value)
96
+ return [nil, nil] if value.nil? || value == ''
97
+ parts = value.split(/; */)
98
+ return [value, nil] if parts.count < 2
99
+ charset = nil
100
+ parts.each do |part|
101
+ fields = part.split('=')
102
+ if fields.count >= 2 && fields[0] == 'charset'
103
+ charset = fields[1]
104
+ break
105
+ end
106
+ end
107
+ [parts[0], charset]
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
@@ -0,0 +1,83 @@
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
+ # Special handling: You can't go back to INITIALIZING after being anything else
58
+ if new_state == LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING && !old_status.state.nil?
59
+ new_state = old_status.state
60
+ end
61
+
62
+ # No change if state is the same and no error
63
+ return if new_state == old_status.state && new_error.nil?
64
+
65
+ new_since = new_state == old_status.state ? @status.state_since : Time.now
66
+ new_error = @status.last_error if new_error.nil?
67
+
68
+ @status = LaunchDarkly::Interfaces::DataSource::Status.new(
69
+ new_state,
70
+ new_since,
71
+ new_error
72
+ )
73
+
74
+ status_to_broadcast = @status
75
+ end
76
+
77
+ @status_broadcaster.broadcast(status_to_broadcast) if status_to_broadcast
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
@@ -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
- LaunchDarkly::ALL_KINDS.each do |kind|
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
- LaunchDarkly::ALL_KINDS.each do |kind|
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] == LaunchDarkly::FEATURES
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