Skip to content

Commit

Permalink
Merge pull request #92 from launchdarkly/eb/no-faraday
Browse files Browse the repository at this point in the history
remove Faraday; fix charset handling in poll requests
  • Loading branch information
eli-darkly authored Jan 29, 2019
2 parents 76fa71e + d38973a commit 0405d3d
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 135 deletions.
2 changes: 0 additions & 2 deletions ldclient-rb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "listen", "~> 3.0" # see file_data_source.rb

spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
spec.add_runtime_dependency "semantic", "~> 1.6"
spec.add_runtime_dependency "net-http-persistent", [">= 2.9", "< 4.0"]
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
Expand Down
6 changes: 2 additions & 4 deletions lib/ldclient-rb/cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

module LaunchDarkly
#
# A thread-safe in-memory store suitable for use with the Faraday caching HTTP client. Uses the
# concurrent-ruby gem's Map as the underlying cache.
# A thread-safe in-memory store that uses the same semantics that Faraday would expect, although we
# no longer use Faraday. This is used by Requestor, when we are not in a Rails environment.
#
# @see https://github.com/plataformatec/faraday-http-cache
# @see https://github.com/ruby-concurrency
# @private
#
class ThreadSafeMemoryStore
Expand Down
22 changes: 4 additions & 18 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def initialize(opts = {})
@use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval
@proxy = opts[:proxy] || Config.default_proxy
@all_attributes_private = opts[:all_attributes_private] || false
@private_attribute_names = opts[:private_attribute_names] || []
@send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
Expand Down Expand Up @@ -153,9 +152,10 @@ def offline?
attr_reader :capacity

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

#
# The proxy configuration string.
# @return [String]
#
attr_reader :proxy

#
# True if all user attributes (other than the key) should be considered private. This means
# that the attribute values will not be sent to LaunchDarkly in analytics events and will not
Expand Down Expand Up @@ -336,14 +330,6 @@ def self.default_connect_timeout
2
end

#
# The default value for {#proxy}.
# @return [String] nil
#
def self.default_proxy
nil
end

#
# The default value for {#logger}.
# @return [Logger] the Rails logger if in Rails, or a default Logger at WARN level otherwise
Expand Down
44 changes: 25 additions & 19 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require "concurrent"
require "concurrent/atomics"
require "concurrent/executors"
require "net/http/persistent"
require "thread"
require "time"
require "faraday"

module LaunchDarkly
MAX_FLUSH_WORKERS = 5
Expand Down Expand Up @@ -115,7 +115,12 @@ class EventDispatcher
def initialize(queue, sdk_key, config, client)
@sdk_key = sdk_key
@config = config
@client = client ? client : Faraday.new

@client = client ? client : Net::HTTP::Persistent.new do |c|
c.open_timeout = @config.connect_timeout
c.read_timeout = @config.read_timeout
end

@user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
@formatter = EventOutputFormatter.new(config)
@disabled = Concurrent::AtomicBoolean.new(false)
Expand Down Expand Up @@ -162,7 +167,7 @@ def main_loop(queue, buffer, flush_workers)
def do_shutdown(flush_workers)
flush_workers.shutdown
flush_workers.wait_for_termination
# There seems to be no such thing as "close" in Faraday: https://github.com/lostisland/faraday/issues/241
@client.shutdown
end

def synchronize_for_testing(flush_workers)
Expand Down Expand Up @@ -246,16 +251,17 @@ def trigger_flush(buffer, flush_workers)
end

def handle_response(res)
if res.status >= 400
message = Util.http_error_message(res.status, "event delivery", "some events were dropped")
status = res.code.to_i
if status >= 400
message = Util.http_error_message(status, "event delivery", "some events were dropped")
@config.logger.error { "[LDClient] #{message}" }
if !Util.http_error_recoverable?(res.status)
if !Util.http_error_recoverable?(status)
@disabled.value = true
end
else
if !res.headers.nil? && res.headers.has_key?("Date")
if !res["date"].nil?
begin
res_time = (Time.httpdate(res.headers["Date"]).to_f * 1000).to_i
res_time = (Time.httpdate(res["date"]).to_f * 1000).to_i
@last_known_past_time.value = res_time
rescue ArgumentError
end
Expand Down Expand Up @@ -317,21 +323,21 @@ def run(sdk_key, config, client, payload, formatter)
end
begin
config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
res = client.post (config.events_uri + "/bulk") do |req|
req.headers["Authorization"] = sdk_key
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
req.headers["Content-Type"] = "application/json"
req.headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
req.body = body
req.options.timeout = config.read_timeout
req.options.open_timeout = config.connect_timeout
end
uri = URI(config.events_uri + "/bulk")
req = Net::HTTP::Post.new(uri)
req.content_type = "application/json"
req.body = body
req["Authorization"] = sdk_key
req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
res = client.request(uri, req)
rescue StandardError => exn
config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
next
end
if res.status < 200 || res.status >= 300
if Util.http_error_recoverable?(res.status)
status = res.code.to_i
if status < 200 || status >= 300
if Util.http_error_recoverable?(status)
next
end
end
Expand Down
3 changes: 1 addition & 2 deletions lib/ldclient-rb/polling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ def create_worker
stop
end
rescue StandardError => exn
@config.logger.error { "[LDClient] Exception while polling: #{exn.inspect}" }
# TODO: log_exception(__method__.to_s, exn)
Util.log_exception(@config.logger, "Exception while polling", exn)
end
delta = @config.poll_interval - (Time.now - started_at)
if delta > 0
Expand Down
76 changes: 55 additions & 21 deletions lib/ldclient-rb/requestor.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
require "concurrent/atomics"
require "json"
require "net/http/persistent"
require "faraday/http_cache"

module LaunchDarkly
# @private
class UnexpectedResponseError < StandardError
def initialize(status)
@status = status
super("HTTP error #{status}")
end

def status
Expand All @@ -16,14 +17,15 @@ def status

# @private
class Requestor
CacheEntry = Struct.new(:etag, :body)

def initialize(sdk_key, config)
@sdk_key = sdk_key
@config = config
@client = Faraday.new do |builder|
builder.use :http_cache, store: @config.cache_store, serializer: Marshal

builder.adapter :net_http_persistent
end
@client = Net::HTTP::Persistent.new
@client.open_timeout = @config.connect_timeout
@client.read_timeout = @config.read_timeout
@cache = @config.cache_store
end

def request_flag(key)
Expand All @@ -38,27 +40,59 @@ def request_all_data()
make_request("/sdk/latest-all")
end

def stop
@client.shutdown
end

private

def make_request(path)
uri = @config.base_uri + path
res = @client.get (uri) do |req|
req.headers["Authorization"] = @sdk_key
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
req.options.timeout = @config.read_timeout
req.options.open_timeout = @config.connect_timeout
if @config.proxy
req.options.proxy = Faraday::ProxyOptions.from @config.proxy
end
uri = URI(@config.base_uri + path)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = @sdk_key
req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
cached = @cache.read(uri)
if !cached.nil?
req["If-None-Match"] = cached.etag
end
res = @client.request(uri, req)
status = res.code.to_i
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{res.to_hash}\n\tbody: #{res.body}" }

@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{res.status}\n\theaders: #{res.headers}\n\tbody: #{res.body}" }

if res.status < 200 || res.status >= 300
raise UnexpectedResponseError.new(res.status)
if status == 304 && !cached.nil?
body = cached.body
else
@cache.delete(uri)
if status < 200 || status >= 300
raise UnexpectedResponseError.new(status)
end
body = fix_encoding(res.body, res["content-type"])
etag = res["etag"]
@cache.write(uri, CacheEntry.new(etag, body)) if !etag.nil?
end
JSON.parse(body, symbolize_names: true)
end

JSON.parse(res.body, symbolize_names: true)
def fix_encoding(body, content_type)
return body if content_type.nil?
media_type, charset = parse_content_type(content_type)
return body if charset.nil?
body.force_encoding(Encoding::find(charset)).encode(Encoding::UTF_8)
end

private :make_request
def parse_content_type(value)
return [nil, nil] if value.nil? || value == ''
parts = value.split(/; */)
return [value, nil] if parts.count < 2
charset = nil
parts.each do |part|
fields = part.split('=')
if fields.count >= 2 && fields[0] == 'charset'
charset = fields[1]
break
end
end
return [parts[0], charset]
end
end
end
1 change: 0 additions & 1 deletion lib/ldclient-rb/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def start
}
opts = {
headers: headers,
proxy: @config.proxy,
read_timeout: READ_TIMEOUT_SECONDS,
logger: @config.logger
}
Expand Down
Loading

0 comments on commit 0405d3d

Please sign in to comment.