lib/aws/core/client.rb



# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

require 'set'
require 'aws/core/client/query_xml'
require 'aws/core/client/query_json'
require 'json'

module AWS
  module Core
    
    # Base client class for all of the Amazon AWS service clients.
    class Client
  
      extend Naming
  
      # @private
      CACHEABLE_REQUESTS = Set[]
  
      # Creates a new low-level client.
      #
      # == Required Options
      #
      # To create a client you must provide access to AWS credentials.
      # There are two options:
      #
      # * +:signer+ -- An object that responds to +access_key_id+
      #   (to return the AWS Access Key ID) and to
      #   <code>sign(string_to_sign)</code> (to return a signature
      #   for a given string).  An example implementation is
      #   AWS::Core::DefaultSigner.  This option is useful if you want to
      #   more tightly control access to your secret access key (for
      #   example by moving the signature computation into a
      #   different process).
      #
      # * +:access_key_id+ and +:secret_access_key+ -- You can use
      #   these options to provide the AWS Access Key ID and AWS
      #   Secret Access Key directly to the client.
      #
      # == Optional
      #
      # * +:http_handler+ -- Any object that implements a
      #   <code>handle(request, response)</code> method; an example
      #   is BuiltinHttpHandler.  This method is used to perform the
      #   HTTP requests that this client constructs.
      #
      def initialize options = {}

        options = options.dup # so we don't modify the options passed in

        @service_ruby_name = self.class.service_ruby_name
  
        # translate these into service specific configuration options,
        # e.g. :endpoint into :s3_endpoint
        [:endpoint, :region, :port].each do |opt|
          if options[opt]
            options[:"#{service_ruby_name}_#{opt}"] = options.delete(opt)
          end
        end
  
        @config = options.delete(:config)
        @config ||= AWS.config
        @config = @config.with(options)

        @signer = @config.signer
        @http_handler = @config.http_handler
        @endpoint = config.send(:"#{service_ruby_name}_endpoint")
        @port = config.send(:"#{service_ruby_name}_port")

      end
  
      # @return [Configuration] This clients configuration.
      attr_reader :config
  
      # @return [DefaultSigner,Object] Returns the signer for this client.
      #   This is normally a DefaultSigner, but it can be configured to
      #   an other object.
      # @private
      attr_reader :signer

      # @return [String] The snake-cased ruby name for the service
      #   (e.g. 's3', 'iam', 'dynamo_db', etc).
      # @private
      attr_reader :service_ruby_name

      # @return [Integer] What port this client makes requests via.
      # @private
      attr_reader :port

      # @return [String] Returns the service endpoint (hostname) this client
      #   makes requests against.
      # @private
      attr_reader :endpoint

      # @return (see Client.operations)
      def operations
        self.class.operations
      end
  
      # Returns a copy of the client with a different HTTP handler.
      # You can pass an object like BuiltinHttpHandler or you can
      # use a block; for example:
      #
      #   s3_with_logging = s3.with_http_handler do |request, response|
      #     $stderr.puts request.inspect
      #     super
      #   end
      #
      # The block executes in the context of an HttpHandler
      # instance, and +super+ delegates to the HTTP handler used by
      # this client.  This provides an easy way to spy on requests
      # and responses.  See HttpHandler, HttpRequest, and
      # HttpResponse for more details on how to implement a fully
      # functional HTTP handler using a different HTTP library than
      # the one that ships with Ruby.
      # @param handler (nil) A new http handler.  Leave blank and pass a
      #   block to wrap the current handler with the block.
      # @return [Core::Client] Returns a new instance of the client class with
      #   the modified or wrapped http handler.
      def with_http_handler(handler = nil, &blk)
        handler ||= Http::Handler.new(@http_handler, &blk)
        with_options(:http_handler => handler)
      end
  
      # @param [Hash] options
      # @see AWS.config detailed list of accepted options.
      def with_options options
        with_config(config.with(options))
      end
  
      # @param [Configuration] config The configuration object to use.
      # @return [Core::Client] Returns a new client object with the given
      #   configuration.
      def with_config config
        self.class.new(:config => config)
      end
  
      # The stub returned is memoized.
      # @see new_stub_for
      # @private
      def stub_for method_name
        @stubs ||= {}
        @stubs[method_name] ||= new_stub_for(method_name)
      end
  
      # Primarily used for testing, this method returns an empty psuedo 
      # service response without making a request.  Its used primarily for
      # testing the ligher level service interfaces.
      # @private
      def new_stub_for method_name
        response = Response.new(Http::Request.new, Http::Response.new)
        response.request_type = method_name
        response.request_options = {}
        send("simulate_#{method_name}_response", response)
        response.signal_success
        response
      end
  
      protected

      def new_request
        eval(self.class.name.sub(/::Client$/, ''))::Request.new
      end
  
      def new_response(*args, &block)
        Response.new(*args, &block)
      end
  
      def make_async_request response
  
        pauses = async_request_with_retries(response, response.http_request)
  
        response
  
      end
  
      def async_request_with_retries response, http_request, retry_delays = nil
  
        response.http_response = Http::Response.new
  
        handle = Object.new
        handle.extend AsyncHandle
        handle.on_complete do |status|
          case status
          when :failure
            response.error = StandardError.new("failed to contact the service")
            response.signal_failure
          when :success
            populate_error(response)
            retry_delays ||= sleep_durations(response)
            if should_retry?(response) and !retry_delays.empty?
              rebuild_http_request(response)
              @http_handler.sleep_with_callback(retry_delays.shift) do
                async_request_with_retries(response, response.http_request, retry_delays)
              end
            else
              response.error ?
                response.signal_failure :
                response.signal_success
            end
          end
        end
  
        @http_handler.handle_async(http_request, response.http_response, handle)
  
      end
  
      def make_sync_request response
        retry_server_errors do
  
          response.http_response = http_response =
            Http::Response.new
  
          @http_handler.handle(response.http_request, http_response)

          populate_error(response)
          response.signal_success unless response.error
          response
  
        end
      end
  
      def retry_server_errors &block
  
        response = yield
  
        sleeps = sleep_durations(response)
        while should_retry?(response)
          break if sleeps.empty?
          Kernel.sleep(sleeps.shift)
          # rebuild the request to get a fresh signature
          rebuild_http_request(response)
          response = yield
        end
  
        response
  
      end

      def rebuild_http_request response
        response.rebuild_request
        response.retry_count += 1
      end
  
      def sleep_durations response
        factor = scaling_factor(response)
        Array.new(config.max_retries) {|n| (2 ** n) * factor }
      end
  
      def scaling_factor response
        response.throttled? ? (0.5 + Kernel.rand * 0.1) : 0.3
      end
  
      def should_retry? response
        response.timeout? or
          response.throttled? or
          response.error.kind_of?(Errors::ServerError)
      end
  
      def return_or_raise options, &block
        response = yield
        unless options[:async]
          raise response.error if response.error
        end
        response
      end

      # Yields to the given block (which should be making a 
      # request and returning a {Response} object).  The results of the 
      # request/response are logged.
      # 
      # @param [Hash] options
      # @option options [Boolean] :async
      # @return [Response]
      def log_client_request options, &block

        # time the request, retries and all
        start = Time.now
        response = yield
        response.duration = Time.now - start

        if options[:async] 
          response.on_complete { log_response(response) } 
        else
          log_response(response)
        end
  
        response

      end

      # Logs the response to the configured logger.
      # @param [Resposne] response
      # @return [nil]
      def log_response response
        if config.logger
          message = config.log_formatter.format(response)
          config.logger.send(config.log_level, message)
        end
        nil
      end
  
      def populate_error response
        response.error = extract_error(response)
      end

      # If the response contains error, this method will construct
      # and return an error object.  If no error is contained in the 
      # response, then nil is returned.
      # @param [Response] response
      # @return [Errors::Base,nil]
      def extract_error response

        status = response.http_response.status

        error_code, error_message = extract_error_details(response)

        error_args = [
          response.http_request,
          response.http_response,
          error_code,
          error_message
        ]

        case
        when response.timeout? then TimeoutError.new
        when error_code        then error_class(error_code).new(*error_args)
        when status >= 500     then Errors::ServerError.new(*error_args)
        when status >= 300     then Errors::ClientError.new(*error_args)
        else nil # no error
        end

      end

      # Given an error code string, this method will return an error class.
      #
      #   AWS::EC2::Client.new.send(:error_code, 'InvalidInstanceId')
      #   #=> AWS::EC2::Errors::InvalidInstanceId
      #
      # @param [String] error_code The error code string as returned by
      #   the service.  If this class contains periods, they will be 
      #   converted into namespaces (e.g. 'Foo.Bar' becomes Errors::Foo::Bar).
      #
      # @return [Class]
      #
      def error_class error_code
        errors_module.error_class(error_code)
      end

      # Returns the ::Errors module for the current client.
      #
      #   AWS::S3::Client.new.errors_module
      #   #=> AWS::S3::Errors
      #
      # @return [Module]
      #
      def errors_module
        AWS.const_get(self.class.to_s[/(\w+)::Client/, 1])::Errors
      end
  
      def client_request name, options, &block
        return_or_raise(options) do
          log_client_request(options) do
  
            if config.stub_requests?
  
              response = stub_for(name)
              response.http_request = build_request(name, options, &block)
              response.request_options = options
              response
  
            else
  
              client = self
              response = new_response { client.send(:build_request, name, options, &block) }
              response.request_type = name
              response.request_options = options
  
              if 
                cacheable_request?(name, options) and
                cache = AWS.response_cache and
                cached_response = cache.cached(response)
              then
                cached_response.cached = true
                cached_response
              else
                # process the http request
                options[:async] ?
                make_async_request(response) :
                  make_sync_request(response)
  
                # process the http response
                response.on_success do
                  send("process_#{name}_response", response)
                  if cache = AWS.response_cache
                    cache.add(response)
                  end
                end
  
                response
  
              end
  
            end
  
          end
        end
      end

      def cacheable_request? name, options
        self.class::CACHEABLE_REQUESTS.include?(name)
      end
  
      def build_request(name, options, &block)

        # we dont want to pass the async option to the configure block
        opts = options.dup
        opts.delete(:async)
  
        http_request = new_request
  
        # configure the http request
        http_request.service_ruby_name = service_ruby_name
        http_request.host = endpoint
        http_request.port = port
        http_request.region = config.send(:"#{service_ruby_name}_region")
        http_request.proxy_uri = config.proxy_uri
        http_request.use_ssl = config.use_ssl?
        http_request.ssl_verify_peer = config.ssl_verify_peer?
        http_request.ssl_ca_file = config.ssl_ca_file if config.ssl_ca_file
        http_request.ssl_ca_path = config.ssl_ca_path if config.ssl_ca_path
  
        send("configure_#{name}_request", http_request, opts, &block)

        http_request.headers["user-agent"] = user_agent_string
        http_request.add_authorization!(signer)

        http_request

      end
  
      def user_agent_string
        engine = (RUBY_ENGINE rescue nil or "ruby")
        user_agent = "%s aws-sdk-ruby/#{VERSION} %s/%s %s" %
          [config.user_agent_prefix, engine, RUBY_VERSION, RUBY_PLATFORM]
        user_agent.strip!
        if AWS.memoizing?
          user_agent << " memoizing"
        end
        user_agent
      end
  
      # Adds a single method to the current client class.  This method
      # yields a request method builder that allows you to specify how:
      #
      # * the request is built
      # * the response is processed
      # * the response is stubbed for testing
      #
      def self.add_client_request_method method_name, options = {}, &block

        self.operations << method_name
  
        ClientRequestMethodBuilder.new(self, method_name, &block)
  
        module_eval <<-END
          def #{method_name}(*args, &block)
            options = args.first ? args.first : {}
            client_request(#{method_name.inspect}, options, &block)
          end
        END
  
      end

      # Parses the service's API configuration yaml file.  This file has
      # configuration that drives the request and response DSLs.
      # @return [Hash]
      def self.api_config
        config_file = 
          File.dirname(File.dirname(__FILE__)) + 
          "/api_config/#{service_name}-#{self::API_VERSION}.yml"
        YAML.load(File.read(config_file))
      end

      # @return [Array<Symbol>] Returns a list of service operations as
      #   method names supported by this client.
      def self.operations
        @operations ||= []
      end
  
      # @private
      class ClientRequestMethodBuilder
  
        def initialize client_class, method_name, &block
          @client_class = client_class
          @method_name = method_name
          configure_request {|request, options|}
          process_response {|response|}
          simulate_response {|response|}
          instance_eval(&block)
        end
  
        def configure_request options = {}, &block
          name = "configure_#{@method_name}_request"
          MetaUtils.class_extend_method(@client_class, name, &block)
          if block.arity == 3
            m = Module.new
            m.module_eval(<<-END)
              def #{name}(req, options, &block)
                super(req, options, block)
              end
            END
            @client_class.send(:include, m)
          end
        end
  
        def process_response &block
          name = "process_#{@method_name}_response"
          MetaUtils.class_extend_method(@client_class, name, &block)
        end
  
        def simulate_response &block
          name = "simulate_#{@method_name}_response"
          MetaUtils.class_extend_method(@client_class, name, &block)
        end
  
      end
  
    end
  end
end