lib/ably/rest/client.rb in ably-0.6.2 vs lib/ably/rest/client.rb in ably-0.7.0
- old
+ new
@@ -6,38 +6,60 @@
module Ably
module Rest
# Client for the Ably REST API
#
- # @!attribute [r] auth
- # @return {Ably::Auth} authentication object configured for this connection
# @!attribute [r] client_id
# @return [String] A client ID, used for identifying this client for presence purposes
# @!attribute [r] auth_options
# @return [Hash] {Ably::Auth} options configured for this client
- # @!attribute [r] environment
- # @return [String] May contain 'sandbox' when testing the client library against an alternate Ably environment
- # @!attribute [r] log_level
- # @return [Logger::Severity] Log level configured for this {Client}
- # @!attribute [r] channels
- # @return [Aby::Rest::Channels] The collection of {Ably::Rest::Channel}s that have been created
- # @!attribute [r] protocol
- # @return [Symbol] The protocol configured for this client, either binary `:msgpack` or text based `:json`
#
class Client
include Ably::Modules::Conversions
include Ably::Modules::HttpHelpers
extend Forwardable
+ # Default Ably domain for REST
DOMAIN = 'rest.ably.io'
- attr_reader :environment, :protocol, :auth, :channels, :log_level
+ # Configuration for connection retry attempts
+ CONNECTION_RETRY = {
+ single_request_open_timeout: 4,
+ single_request_timeout: 15,
+ cumulative_request_open_timeout: 10,
+ max_retry_attempts: 3
+ }.freeze
+
def_delegators :auth, :client_id, :auth_options
- # @api private
+ # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
+ # @return [String]
+ attr_reader :environment
+
+ # The protocol configured for this client, either binary `:msgpack` or text based `:json`
+ # @return [Symbol]
+ attr_reader :protocol
+
+ # {Ably::Auth} authentication object configured for this connection
+ # @return [Ably::Auth]
+ attr_reader :auth
+
+ # The collection of {Ably::Rest::Channel}s that have been created
+ # @return [Aby::Rest::Channels]
+ attr_reader :channels
+
+ # Log level configured for this {Client}
+ # @return [Logger::Severity]
+ attr_reader :log_level
+
+ # The custom host that is being used if it was provided with the option `:rest_host` when the {Client} was created
+ # @return [String,Nil]
+ attr_reader :custom_host
+
# The registered encoders that are used to encode and decode message payloads
# @return [Array<Ably::Models::MessageEncoder::Base>]
+ # @api private
attr_reader :encoders
# The additional options passed to this Client's #initialize method not available as attributes of this class
# @return [Hash]
# @api private
@@ -45,16 +67,17 @@
# Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
#
# @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
# @option options (see Ably::Auth#authorise)
- # @option options [Boolean] :tls TLS is used by default, providing a value of false disbles TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
+ # @option options [Boolean] :tls TLS is used by default, providing a value of false disables TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
# @option options [String] :api_key API key comprising the key ID and key secret in a single string
+ # @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and TOken auth if set to true
# @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
# @option options [Symbol] :protocol Protocol used to communicate with Ably, :json and :msgpack currently supported. Defaults to :msgpack
# @option options [Boolean] :use_binary_protocol Protocol used to communicate with Ably, defaults to true and uses MessagePack protocol. This option will overide :protocol option
- # @option options [Logger::Severity,Symbol] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::ERROR, can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG)
+ # @option options [Logger::Severity,Symbol] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::ERROR, can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
# @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
#
# @yield (see Ably::Auth#authorise)
# @yieldparam (see Ably::Auth#authorise)
# @yieldreturn (see Ably::Auth#authorise)
@@ -66,11 +89,13 @@
# client = Ably::Rest::Client.new('key.id:secret')
#
# # create a new client and configure a client ID used for presence
# client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
#
- def initialize(options, &auth_block)
+ def initialize(options, &token_request_block)
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
+
options = options.clone
if options.kind_of?(String)
options = { api_key: options }
end
@@ -78,12 +103,17 @@
@environment = options.delete(:environment) # nil is production
@protocol = options.delete(:protocol) || :msgpack
@debug_http = options.delete(:debug_http)
@log_level = options.delete(:log_level) || ::Logger::ERROR
@custom_logger = options.delete(:logger)
+ @custom_host = options.delete(:rest_host)
- @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
+ if @log_level == :none
+ @custom_logger = Ably::Models::NilLogger.new
+ else
+ @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
+ end
options.delete(:use_binary_protocol).tap do |use_binary_protocol|
if use_binary_protocol == true
@protocol = :msgpack
elsif use_binary_protocol == false
@@ -91,11 +121,11 @@
end
end
raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
@options = options.freeze
- @auth = Auth.new(self, options, &auth_block)
+ @auth = Auth.new(self, options, &token_request_block)
@channels = Ably::Rest::Channels.new(self)
@encoders = []
initialize_default_encoders
end
@@ -155,14 +185,11 @@
end
# @!attribute [r] endpoint
# @return [URI::Generic] Default Ably REST endpoint used for all requests
def endpoint
- URI::Generic.build(
- scheme: use_tls? ? "https" : "http",
- host: [@environment, DOMAIN].compact.join('-')
- )
+ endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-'))
end
# @!attribute [r] logger
# @return [Logger] The {Ably::Logger} for this client.
# Configure the log_level with the `:log_level` option, refer to {Client#initialize}
@@ -189,11 +216,13 @@
# @return [void]
#
# @api private
def register_encoder(encoder)
encoder_klass = if encoder.kind_of?(String)
- Object.const_get(encoder)
+ encoder.split('::').inject(Kernel) do |base, klass_name|
+ base.public_send(:const_get, klass_name)
+ end
else
encoder
end
raise "Encoder must inherit from `Ably::Models::MessageEncoders::Base`" unless encoder_klass.ancestors.include?(Ably::Models::MessageEncoders::Base)
@@ -205,41 +234,108 @@
# @return [Boolean] True of the transport #protocol communicates with Ably with a binary protocol
def protocol_binary?
protocol == :msgpack
end
+ # Connection used to make HTTP requests
+ #
+ # @param [Hash] options
+ # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see {Ably::FALLBACK_HOSTS}
+ #
+ # @return [Faraday::Connection]
+ #
+ # @api private
+ def connection(options = {})
+ if options[:use_fallback]
+ fallback_connection
+ else
+ @connection ||= Faraday.new(endpoint.to_s, connection_options)
+ end
+ end
+
+ # Fallback connection used to make HTTP requests.
+ # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback host}
+ #
+ # @return [Faraday::Connection]
+ #
+ # @api private
+ def fallback_connection
+ unless @fallback_connections
+ @fallback_connections = Ably::FALLBACK_HOSTS.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
+ end
+ @fallback_index ||= 0
+
+ @fallback_connections[@fallback_index % @fallback_connections.count].tap do
+ @fallback_index += 1
+ end
+ end
+
private
def request(method, path, params = {}, options = {})
- reauthorise_on_authorisation_failure do
- connection.send(method, path, params) do |request|
+ options = options.clone
+ if options.delete(:disable_automatic_reauthorise) == true
+ send_request(method, path, params, options)
+ else
+ reauthorise_on_authorisation_failure do
+ send_request(method, path, params, options)
+ end
+ end
+ end
+
+ # Sends HTTP request to connection end point
+ # Connection failures will automatically be reattempted until thresholds are met
+ def send_request(method, path, params, options)
+ max_retry_attempts = CONNECTION_RETRY.fetch(:max_retry_attempts)
+ cumulative_timeout = CONNECTION_RETRY.fetch(:cumulative_request_open_timeout)
+ requested_at = Time.now
+ retry_count = 0
+
+ begin
+ use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
+
+ connection(use_fallback: use_fallback).send(method, path, params) do |request|
unless options[:send_auth_header] == false
request.headers[:authorization] = auth.auth_header
end
end
+
+ rescue Faraday::TimeoutError, Faraday::ClientError => error
+ time_passed = Time.now - requested_at
+ if can_fallback_to_alternate_ably_host? && retry_count < max_retry_attempts && time_passed <= cumulative_timeout
+ retry_count += 1
+ retry
+ end
+
+ case error
+ when Faraday::TimeoutError
+ raise Ably::Exceptions::ConnectionTimeoutError.new(error.message, nil, 80014, error)
+ when Faraday::ClientError
+ raise Ably::Exceptions::ConnectionError.new(error.message, nil, 80000, error)
+ end
end
end
def reauthorise_on_authorisation_failure
- attempts = 0
- begin
- yield
- rescue Ably::Exceptions::InvalidRequest => e
- attempts += 1
- if attempts == 1 && e.code == 40140 && auth.token_renewable?
+ yield
+ rescue Ably::Exceptions::InvalidRequest => e
+ if e.code == 40140
+ if auth.token_renewable?
auth.authorise force: true
- retry
+ yield
else
raise Ably::Exceptions::InvalidToken.new(e.message, e.status, e.code)
end
+ else
+ raise e
end
end
- # Return a Faraday::Connection to use to make HTTP requests
- #
- # @return [Faraday::Connection]
- def connection
- @connection ||= Faraday.new(endpoint.to_s, connection_options)
+ def endpoint_for_host(host)
+ URI::Generic.build(
+ scheme: use_tls? ? 'https' : 'http',
+ host: host
+ )
end
# Return a Hash of connection options to initiate the Faraday::Connection with
#
# @return [Hash]
@@ -250,12 +346,12 @@
content_type: mime_type,
accept: mime_type,
user_agent: user_agent
},
request: {
- open_timeout: 5,
- timeout: 10
+ open_timeout: CONNECTION_RETRY.fetch(:single_request_open_timeout),
+ timeout: CONNECTION_RETRY.fetch(:single_request_timeout)
}
}
end
# Return a Faraday middleware stack to initiate the Faraday::Connection with
@@ -271,9 +367,13 @@
setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
# Set Faraday's HTTP adapter
builder.adapter Faraday.default_adapter
end
+ end
+
+ def can_fallback_to_alternate_ably_host?
+ !custom_host && !environment
end
def initialize_default_encoders
Ably::Models::MessageEncoders.register_default_encoders self
end