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