Skip to content

Commit 0405d3d

Browse files
authored
Merge pull request #92 from launchdarkly/eb/no-faraday
remove Faraday; fix charset handling in poll requests
2 parents 76fa71e + d38973a commit 0405d3d

File tree

10 files changed

+353
-135
lines changed

10 files changed

+353
-135
lines changed

ldclient-rb.gemspec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ Gem::Specification.new do |spec|
3434
spec.add_development_dependency "listen", "~> 3.0" # see file_data_source.rb
3535

3636
spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
37-
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
38-
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
3937
spec.add_runtime_dependency "semantic", "~> 1.6"
4038
spec.add_runtime_dependency "net-http-persistent", [">= 2.9", "< 4.0"]
4139
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"

lib/ldclient-rb/cache_store.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22

33
module LaunchDarkly
44
#
5-
# A thread-safe in-memory store suitable for use with the Faraday caching HTTP client. Uses the
6-
# concurrent-ruby gem's Map as the underlying cache.
5+
# A thread-safe in-memory store that uses the same semantics that Faraday would expect, although we
6+
# no longer use Faraday. This is used by Requestor, when we are not in a Rails environment.
77
#
8-
# @see https://github.com/plataformatec/faraday-http-cache
9-
# @see https://github.com/ruby-concurrency
108
# @private
119
#
1210
class ThreadSafeMemoryStore

lib/ldclient-rb/config.rb

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ def initialize(opts = {})
5353
@use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
5454
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
5555
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval
56-
@proxy = opts[:proxy] || Config.default_proxy
5756
@all_attributes_private = opts[:all_attributes_private] || false
5857
@private_attribute_names = opts[:private_attribute_names] || []
5958
@send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
@@ -153,9 +152,10 @@ def offline?
153152
attr_reader :capacity
154153

155154
#
156-
# A store for HTTP caching. This must support the semantics used by the
157-
# [`faraday-http-cache`](https://github.com/plataformatec/faraday-http-cache) gem. Defaults
158-
# to the Rails cache in a Rails environment, or a thread-safe in-memory store otherwise.
155+
# A store for HTTP caching (used only in polling mode). This must support the semantics used by
156+
# the [`faraday-http-cache`](https://github.com/plataformatec/faraday-http-cache) gem, although
157+
# the SDK no longer uses Faraday. Defaults to the Rails cache in a Rails environment, or a
158+
# thread-safe in-memory store otherwise.
159159
# @return [Object]
160160
#
161161
attr_reader :cache_store
@@ -184,12 +184,6 @@ def offline?
184184
#
185185
attr_reader :feature_store
186186

187-
#
188-
# The proxy configuration string.
189-
# @return [String]
190-
#
191-
attr_reader :proxy
192-
193187
#
194188
# True if all user attributes (other than the key) should be considered private. This means
195189
# that the attribute values will not be sent to LaunchDarkly in analytics events and will not
@@ -336,14 +330,6 @@ def self.default_connect_timeout
336330
2
337331
end
338332

339-
#
340-
# The default value for {#proxy}.
341-
# @return [String] nil
342-
#
343-
def self.default_proxy
344-
nil
345-
end
346-
347333
#
348334
# The default value for {#logger}.
349335
# @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise

lib/ldclient-rb/events.rb

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
require "concurrent"
22
require "concurrent/atomics"
33
require "concurrent/executors"
4+
require "net/http/persistent"
45
require "thread"
56
require "time"
6-
require "faraday"
77

88
module LaunchDarkly
99
MAX_FLUSH_WORKERS = 5
@@ -115,7 +115,12 @@ class EventDispatcher
115115
def initialize(queue, sdk_key, config, client)
116116
@sdk_key = sdk_key
117117
@config = config
118-
@client = client ? client : Faraday.new
118+
119+
@client = client ? client : Net::HTTP::Persistent.new do |c|
120+
c.open_timeout = @config.connect_timeout
121+
c.read_timeout = @config.read_timeout
122+
end
123+
119124
@user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
120125
@formatter = EventOutputFormatter.new(config)
121126
@disabled = Concurrent::AtomicBoolean.new(false)
@@ -162,7 +167,7 @@ def main_loop(queue, buffer, flush_workers)
162167
def do_shutdown(flush_workers)
163168
flush_workers.shutdown
164169
flush_workers.wait_for_termination
165-
# There seems to be no such thing as "close" in Faraday: https://github.com/lostisland/faraday/issues/241
170+
@client.shutdown
166171
end
167172

168173
def synchronize_for_testing(flush_workers)
@@ -246,16 +251,17 @@ def trigger_flush(buffer, flush_workers)
246251
end
247252

248253
def handle_response(res)
249-
if res.status >= 400
250-
message = Util.http_error_message(res.status, "event delivery", "some events were dropped")
254+
status = res.code.to_i
255+
if status >= 400
256+
message = Util.http_error_message(status, "event delivery", "some events were dropped")
251257
@config.logger.error { "[LDClient] #{message}" }
252-
if !Util.http_error_recoverable?(res.status)
258+
if !Util.http_error_recoverable?(status)
253259
@disabled.value = true
254260
end
255261
else
256-
if !res.headers.nil? && res.headers.has_key?("Date")
262+
if !res["date"].nil?
257263
begin
258-
res_time = (Time.httpdate(res.headers["Date"]).to_f * 1000).to_i
264+
res_time = (Time.httpdate(res["date"]).to_f * 1000).to_i
259265
@last_known_past_time.value = res_time
260266
rescue ArgumentError
261267
end
@@ -317,21 +323,21 @@ def run(sdk_key, config, client, payload, formatter)
317323
end
318324
begin
319325
config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
320-
res = client.post (config.events_uri + "/bulk") do |req|
321-
req.headers["Authorization"] = sdk_key
322-
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
323-
req.headers["Content-Type"] = "application/json"
324-
req.headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
325-
req.body = body
326-
req.options.timeout = config.read_timeout
327-
req.options.open_timeout = config.connect_timeout
328-
end
326+
uri = URI(config.events_uri + "/bulk")
327+
req = Net::HTTP::Post.new(uri)
328+
req.content_type = "application/json"
329+
req.body = body
330+
req["Authorization"] = sdk_key
331+
req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
332+
req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
333+
res = client.request(uri, req)
329334
rescue StandardError => exn
330335
config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
331336
next
332337
end
333-
if res.status < 200 || res.status >= 300
334-
if Util.http_error_recoverable?(res.status)
338+
status = res.code.to_i
339+
if status < 200 || status >= 300
340+
if Util.http_error_recoverable?(status)
335341
next
336342
end
337343
end

lib/ldclient-rb/polling.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ def create_worker
6363
stop
6464
end
6565
rescue StandardError => exn
66-
@config.logger.error { "[LDClient] Exception while polling: #{exn.inspect}" }
67-
# TODO: log_exception(__method__.to_s, exn)
66+
Util.log_exception(@config.logger, "Exception while polling", exn)
6867
end
6968
delta = @config.poll_interval - (Time.now - started_at)
7069
if delta > 0

lib/ldclient-rb/requestor.rb

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
require "concurrent/atomics"
12
require "json"
23
require "net/http/persistent"
3-
require "faraday/http_cache"
44

55
module LaunchDarkly
66
# @private
77
class UnexpectedResponseError < StandardError
88
def initialize(status)
99
@status = status
10+
super("HTTP error #{status}")
1011
end
1112

1213
def status
@@ -16,14 +17,15 @@ def status
1617

1718
# @private
1819
class Requestor
20+
CacheEntry = Struct.new(:etag, :body)
21+
1922
def initialize(sdk_key, config)
2023
@sdk_key = sdk_key
2124
@config = config
22-
@client = Faraday.new do |builder|
23-
builder.use :http_cache, store: @config.cache_store, serializer: Marshal
24-
25-
builder.adapter :net_http_persistent
26-
end
25+
@client = Net::HTTP::Persistent.new
26+
@client.open_timeout = @config.connect_timeout
27+
@client.read_timeout = @config.read_timeout
28+
@cache = @config.cache_store
2729
end
2830

2931
def request_flag(key)
@@ -38,27 +40,59 @@ def request_all_data()
3840
make_request("/sdk/latest-all")
3941
end
4042

43+
def stop
44+
@client.shutdown
45+
end
46+
47+
private
48+
4149
def make_request(path)
42-
uri = @config.base_uri + path
43-
res = @client.get (uri) do |req|
44-
req.headers["Authorization"] = @sdk_key
45-
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
46-
req.options.timeout = @config.read_timeout
47-
req.options.open_timeout = @config.connect_timeout
48-
if @config.proxy
49-
req.options.proxy = Faraday::ProxyOptions.from @config.proxy
50-
end
50+
uri = URI(@config.base_uri + path)
51+
req = Net::HTTP::Get.new(uri)
52+
req["Authorization"] = @sdk_key
53+
req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
54+
cached = @cache.read(uri)
55+
if !cached.nil?
56+
req["If-None-Match"] = cached.etag
5157
end
58+
res = @client.request(uri, req)
59+
status = res.code.to_i
60+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }
5261

53-
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{res.status}\n\theaders: #{res.headers}\n\tbody: #{res.body}" }
54-
55-
if res.status < 200 || res.status >= 300
56-
raise UnexpectedResponseError.new(res.status)
62+
if status == 304 && !cached.nil?
63+
body = cached.body
64+
else
65+
@cache.delete(uri)
66+
if status < 200 || status >= 300
67+
raise UnexpectedResponseError.new(status)
68+
end
69+
body = fix_encoding(res.body, res["content-type"])
70+
etag = res["etag"]
71+
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
5772
end
73+
JSON.parse(body, symbolize_names: true)
74+
end
5875

59-
JSON.parse(res.body, symbolize_names: true)
76+
def fix_encoding(body, content_type)
77+
return body if content_type.nil?
78+
media_type, charset = parse_content_type(content_type)
79+
return body if charset.nil?
80+
body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
6081
end
6182

62-
private :make_request
83+
def parse_content_type(value)
84+
return [nil, nil] if value.nil? || value == ''
85+
parts = value.split(/; */)
86+
return [value, nil] if parts.count < 2
87+
charset = nil
88+
parts.each do |part|
89+
fields = part.split('=')
90+
if fields.count >= 2 && fields[0] == 'charset'
91+
charset = fields[1]
92+
break
93+
end
94+
end
95+
return [parts[0], charset]
96+
end
6397
end
6498
end

lib/ldclient-rb/stream.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def start
5050
}
5151
opts = {
5252
headers: headers,
53-
proxy: @config.proxy,
5453
read_timeout: READ_TIMEOUT_SECONDS,
5554
logger: @config.logger
5655
}

0 commit comments

Comments
 (0)