launchdarkly-server-sdk 8.8.3-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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +13 -0
  3. data/README.md +61 -0
  4. data/lib/launchdarkly-server-sdk.rb +1 -0
  5. data/lib/ldclient-rb/cache_store.rb +45 -0
  6. data/lib/ldclient-rb/config.rb +658 -0
  7. data/lib/ldclient-rb/context.rb +565 -0
  8. data/lib/ldclient-rb/evaluation_detail.rb +387 -0
  9. data/lib/ldclient-rb/events.rb +642 -0
  10. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  11. data/lib/ldclient-rb/flags_state.rb +88 -0
  12. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  13. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  14. data/lib/ldclient-rb/impl/context.rb +96 -0
  15. data/lib/ldclient-rb/impl/context_filter.rb +166 -0
  16. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  17. data/lib/ldclient-rb/impl/data_store.rb +109 -0
  18. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  19. data/lib/ldclient-rb/impl/diagnostic_events.rb +129 -0
  20. data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
  21. data/lib/ldclient-rb/impl/evaluator.rb +539 -0
  22. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +86 -0
  23. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  24. data/lib/ldclient-rb/impl/evaluator_operators.rb +131 -0
  25. data/lib/ldclient-rb/impl/event_sender.rb +100 -0
  26. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  27. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  28. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  29. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +170 -0
  30. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +300 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +229 -0
  32. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +306 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  34. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  35. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  36. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  37. data/lib/ldclient-rb/impl/model/feature_flag.rb +254 -0
  38. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  39. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  40. data/lib/ldclient-rb/impl/model/serialization.rb +72 -0
  41. data/lib/ldclient-rb/impl/repeating_task.rb +46 -0
  42. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  43. data/lib/ldclient-rb/impl/store_client_wrapper.rb +141 -0
  44. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  45. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  46. data/lib/ldclient-rb/impl/util.rb +95 -0
  47. data/lib/ldclient-rb/impl.rb +13 -0
  48. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  49. data/lib/ldclient-rb/integrations/consul.rb +45 -0
  50. data/lib/ldclient-rb/integrations/dynamodb.rb +92 -0
  51. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  52. data/lib/ldclient-rb/integrations/redis.rb +98 -0
  53. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +663 -0
  54. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  55. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +246 -0
  56. data/lib/ldclient-rb/integrations.rb +6 -0
  57. data/lib/ldclient-rb/interfaces.rb +974 -0
  58. data/lib/ldclient-rb/ldclient.rb +822 -0
  59. data/lib/ldclient-rb/memoized_value.rb +32 -0
  60. data/lib/ldclient-rb/migrations.rb +230 -0
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  62. data/lib/ldclient-rb/polling.rb +102 -0
  63. data/lib/ldclient-rb/reference.rb +295 -0
  64. data/lib/ldclient-rb/requestor.rb +102 -0
  65. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  66. data/lib/ldclient-rb/stream.rb +196 -0
  67. data/lib/ldclient-rb/util.rb +132 -0
  68. data/lib/ldclient-rb/version.rb +3 -0
  69. data/lib/ldclient-rb.rb +27 -0
  70. metadata +400 -0
@@ -0,0 +1,102 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+
3
+ require "concurrent/atomics"
4
+ require "json"
5
+ require "uri"
6
+ require "http"
7
+
8
+ module LaunchDarkly
9
+ # @private
10
+ class UnexpectedResponseError < StandardError
11
+ def initialize(status)
12
+ @status = status
13
+ super("HTTP error #{status}")
14
+ end
15
+
16
+ def status
17
+ @status
18
+ end
19
+ end
20
+
21
+ # @private
22
+ class Requestor
23
+ CacheEntry = Struct.new(:etag, :body)
24
+
25
+ def initialize(sdk_key, config)
26
+ @sdk_key = sdk_key
27
+ @config = config
28
+ @http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
29
+ .use(:auto_inflate)
30
+ .headers("Accept-Encoding" => "gzip")
31
+ @cache = @config.cache_store
32
+ end
33
+
34
+ def request_all_data()
35
+ all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
36
+ Impl::Model.make_all_store_data(all_data, @config.logger)
37
+ end
38
+
39
+ def stop
40
+ begin
41
+ @http_client.close
42
+ rescue
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def make_request(path)
49
+ uri = URI(
50
+ Util.add_payload_filter_key(@config.base_uri + path, @config)
51
+ )
52
+ headers = {}
53
+ Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
54
+ headers["Connection"] = "keep-alive"
55
+ cached = @cache.read(uri)
56
+ unless cached.nil?
57
+ headers["If-None-Match"] = cached.etag
58
+ end
59
+ response = @http_client.request("GET", uri, {
60
+ headers: headers,
61
+ })
62
+ status = response.status.code
63
+ # must fully read body for persistent connections
64
+ body = response.to_s
65
+ @config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers.to_h}\n\tbody: #{body}" }
66
+ if status == 304 && !cached.nil?
67
+ body = cached.body
68
+ else
69
+ @cache.delete(uri)
70
+ if status < 200 || status >= 300
71
+ raise UnexpectedResponseError.new(status)
72
+ end
73
+ body = fix_encoding(body, response.headers["content-type"])
74
+ etag = response.headers["etag"]
75
+ @cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
76
+ end
77
+ body
78
+ end
79
+
80
+ def fix_encoding(body, content_type)
81
+ return body if content_type.nil?
82
+ media_type, charset = parse_content_type(content_type)
83
+ return body if charset.nil?
84
+ body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
85
+ end
86
+
87
+ def parse_content_type(value)
88
+ return [nil, nil] if value.nil? || value == ''
89
+ parts = value.split(/; */)
90
+ return [value, nil] if parts.count < 2
91
+ charset = nil
92
+ parts.each do |part|
93
+ fields = part.split('=')
94
+ if fields.count >= 2 && fields[0] == 'charset'
95
+ charset = fields[1]
96
+ break
97
+ end
98
+ end
99
+ [parts[0], charset]
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,25 @@
1
+
2
+ module LaunchDarkly
3
+ # A non-thread-safe implementation of a LRU cache set with only add and reset methods.
4
+ # Based on https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/cache.rb
5
+ # @private
6
+ class SimpleLRUCacheSet
7
+ def initialize(capacity)
8
+ @values = {}
9
+ @capacity = capacity
10
+ end
11
+
12
+ # Adds a value to the cache or marks it recent if it was already there. Returns true if already there.
13
+ def add(value)
14
+ found = true
15
+ @values.delete(value) { found = false }
16
+ @values[value] = true
17
+ @values.shift if @values.length > @capacity
18
+ found
19
+ end
20
+
21
+ def clear
22
+ @values = {}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,196 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+
3
+ require "concurrent/atomics"
4
+ require "json"
5
+ require "ld-eventsource"
6
+
7
+ module LaunchDarkly
8
+ # @private
9
+ PUT = :put
10
+ # @private
11
+ PATCH = :patch
12
+ # @private
13
+ DELETE = :delete
14
+ # @private
15
+ READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
16
+
17
+ # @private
18
+ KEY_PATHS = {
19
+ FEATURES => "/flags/",
20
+ SEGMENTS => "/segments/",
21
+ }
22
+
23
+ # @private
24
+ class StreamProcessor
25
+ def initialize(sdk_key, config, diagnostic_accumulator = nil)
26
+ @sdk_key = sdk_key
27
+ @config = config
28
+ @data_source_update_sink = config.data_source_update_sink
29
+ @feature_store = config.feature_store
30
+ @initialized = Concurrent::AtomicBoolean.new(false)
31
+ @started = Concurrent::AtomicBoolean.new(false)
32
+ @stopped = Concurrent::AtomicBoolean.new(false)
33
+ @ready = Concurrent::Event.new
34
+ @connection_attempt_start_time = 0
35
+ end
36
+
37
+ def initialized?
38
+ @initialized.value
39
+ end
40
+
41
+ def start
42
+ return @ready unless @started.make_true
43
+
44
+ @config.logger.info { "[LDClient] Initializing stream connection" }
45
+
46
+ headers = Impl::Util.default_http_headers(@sdk_key, @config)
47
+ opts = {
48
+ headers: headers,
49
+ read_timeout: READ_TIMEOUT_SECONDS,
50
+ logger: @config.logger,
51
+ socket_factory: @config.socket_factory,
52
+ reconnect_time: @config.initial_reconnect_delay,
53
+ }
54
+ log_connection_started
55
+
56
+ uri = Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
57
+ @es = SSE::Client.new(uri, **opts) do |conn|
58
+ conn.on_event { |event| process_message(event) }
59
+ conn.on_error { |err|
60
+ log_connection_result(false)
61
+ case err
62
+ when SSE::Errors::HTTPStatusError
63
+ status = err.status
64
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
65
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, status, nil, Time.now)
66
+ message = Util.http_error_message(status, "streaming connection", "will retry")
67
+ @config.logger.error { "[LDClient] #{message}" }
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
75
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
76
+ stop_with_error_info error_info
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
+ )
89
+ end
90
+ }
91
+ end
92
+
93
+ @ready
94
+ end
95
+
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)
106
+ if @stopped.make_true
107
+ @es.close
108
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
109
+ @config.logger.info { "[LDClient] Stream connection stopped" }
110
+ end
111
+ end
112
+
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
127
+
128
+ def process_message(message)
129
+ log_connection_result(true)
130
+ method = message.type
131
+ @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
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
150
+ end
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
159
+ end
160
+ else
161
+ @config.logger.warn { "[LDClient] Unknown message received: #{method}" }
162
+ end
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
177
+ end
178
+ end
179
+
180
+ def key_for_path(kind, path)
181
+ path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
182
+ end
183
+
184
+ def log_connection_started
185
+ @connection_attempt_start_time = Impl::Util::current_time_millis
186
+ end
187
+
188
+ def log_connection_result(is_success)
189
+ if !@diagnostic_accumulator.nil? && @connection_attempt_start_time > 0
190
+ @diagnostic_accumulator.record_stream_init(@connection_attempt_start_time, !is_success,
191
+ Impl::Util::current_time_millis - @connection_attempt_start_time)
192
+ @connection_attempt_start_time = 0
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,132 @@
1
+ require "uri"
2
+ require "http"
3
+
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
+
68
+ # @private
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
+ begin
81
+ parsed = URI.parse(uri)
82
+ new_query_params = URI.decode_www_form(String(parsed.query)) << ["filter", config.payload_filter_key]
83
+ parsed.query = URI.encode_www_form(new_query_params)
84
+ parsed.to_s
85
+ rescue URI::InvalidURIError
86
+ config.logger.warn { "[LDClient] URI could not be parsed. No filtering will be applied." }
87
+ uri
88
+ end
89
+ end
90
+
91
+ def self.new_http_client(uri_s, config)
92
+ http_client_options = {}
93
+ if config.socket_factory
94
+ http_client_options["socket_class"] = config.socket_factory
95
+ end
96
+ proxy = URI.parse(uri_s).find_proxy
97
+ unless proxy.nil?
98
+ http_client_options["proxy"] = {
99
+ proxy_address: proxy.host,
100
+ proxy_port: proxy.port,
101
+ proxy_username: proxy.user,
102
+ proxy_password: proxy.password,
103
+ }
104
+ end
105
+ HTTP::Client.new(http_client_options)
106
+ .timeout({
107
+ read: config.read_timeout,
108
+ connect: config.connect_timeout,
109
+ })
110
+ .persistent(uri_s)
111
+ end
112
+
113
+ def self.log_exception(logger, message, exc)
114
+ logger.error { "[LDClient] #{message}: #{exc.inspect}" }
115
+ logger.debug { "[LDClient] Exception trace: #{exc.backtrace}" }
116
+ end
117
+
118
+ def self.http_error_recoverable?(status)
119
+ if status >= 400 && status < 500
120
+ status == 400 || status == 408 || status == 429
121
+ else
122
+ true
123
+ end
124
+ end
125
+
126
+ def self.http_error_message(status, context, recoverable_message)
127
+ desc = (status == 401 || status == 403) ? " (invalid SDK key)" : ""
128
+ message = Util.http_error_recoverable?(status) ? recoverable_message : "giving up permanently"
129
+ "HTTP error #{status}#{desc} for #{context} - #{message}"
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,3 @@
1
+ module LaunchDarkly
2
+ VERSION = "8.8.3" # x-release-please-version
3
+ end
@@ -0,0 +1,27 @@
1
+
2
+ #
3
+ # Namespace for the LaunchDarkly Ruby SDK.
4
+ #
5
+ module LaunchDarkly
6
+ end
7
+
8
+ require "ldclient-rb/version"
9
+ require "ldclient-rb/interfaces"
10
+ require "ldclient-rb/util"
11
+ require "ldclient-rb/flags_state"
12
+ require "ldclient-rb/migrations"
13
+ require "ldclient-rb/ldclient"
14
+ require "ldclient-rb/cache_store"
15
+ require "ldclient-rb/expiring_cache"
16
+ require "ldclient-rb/memoized_value"
17
+ require "ldclient-rb/in_memory_store"
18
+ require "ldclient-rb/config"
19
+ require "ldclient-rb/context"
20
+ require "ldclient-rb/reference"
21
+ require "ldclient-rb/stream"
22
+ require "ldclient-rb/polling"
23
+ require "ldclient-rb/simple_lru_cache"
24
+ require "ldclient-rb/non_blocking_thread_pool"
25
+ require "ldclient-rb/events"
26
+ require "ldclient-rb/requestor"
27
+ require "ldclient-rb/integrations"