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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +66 -3
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +243 -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 +106 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -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 +82 -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/fdv1.rb +20 -7
  21. data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
  22. data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
  23. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  24. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  25. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  26. data/lib/ldclient-rb/impl/event_sender.rb +4 -3
  27. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  28. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  29. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
  30. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  31. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  32. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  33. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  34. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  35. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  36. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  37. data/lib/ldclient-rb/impl/util.rb +65 -0
  38. data/lib/ldclient-rb/impl.rb +1 -2
  39. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  40. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  41. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  42. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  43. data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
  44. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  45. data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
  46. data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
  47. data/lib/ldclient-rb/ldclient.rb +55 -131
  48. data/lib/ldclient-rb/util.rb +11 -70
  49. data/lib/ldclient-rb/version.rb +1 -1
  50. data/lib/ldclient-rb.rb +8 -17
  51. metadata +35 -17
  52. data/lib/ldclient-rb/cache_store.rb +0 -45
  53. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  54. data/lib/ldclient-rb/memoized_value.rb +0 -32
  55. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  56. data/lib/ldclient-rb/polling.rb +0 -102
  57. data/lib/ldclient-rb/requestor.rb +0 -102
  58. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  59. 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
- 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