lib/aws/core/client.rb



# Copyright 2011 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'

module AWS
  module Core
    
    # Base class for all of the Amazon AWS service clients.
    # @private
    class Client
  
      extend Naming
  
      include ClientLogging
  
      CACHEABLE_REQUESTS = Set.new
  
      # 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 = {}
  
        if options[:endpoint]
          options[:"#{self.class.service_ruby_name}_endpoint"] = 
            options.delete(:endpoint)
        end
  
        options_without_config = options.dup
        @config = options_without_config.delete(:config)
        @config ||= AWS.config
        @config = @config.with(options_without_config)
        @signer = @config.signer
        @http_handler = @config.http_handler
        @stubs = {}
  
      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.
      attr_reader :signer
  
      # @return [String] the configured endpoint for this client.
      def endpoint
        config.send(:"#{self.class.service_ruby_name}_endpoint")
      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] 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[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
        req = self.class::REQUEST_CLASS.new
        req.http_method = 'POST'
        req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
        req.add_param 'Timestamp', Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
        req.add_param 'Version', self.class::API_VERSION
        req
      end
  
      protected
      def new_response(*args, &block)
        Response.new(*args, &block)
      end
  
      private
      def make_async_request response
  
        pauses = async_request_with_retries(response, response.http_request)
  
        response
  
      end
  
      private
      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?
              response.rebuild_request
              @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
  
      private
      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
  
      private
      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
          response.rebuild_request
          response = yield
        end
  
        response
  
      end
  
      private
      def sleep_durations response
        factor = scaling_factor(response)
        Array.new(config.max_retries) {|n| (2 ** n) * factor }
      end
  
      private
      def scaling_factor response
        response.throttled? ? (0.5 + Kernel.rand * 0.1) : 0.3
      end
  
      private
      def should_retry? response
        response.timeout? or
          response.throttled? or
          response.error.kind_of?(Errors::ServerError)
      end
  
      private
      def return_or_raise options, &block
        response = yield
        unless options[:async]
          raise response.error if response.error
        end
        response
      end
  
      protected
      def populate_error response
  
        # clear out a previous error
        response.error = nil
        status = response.http_response.status
        code = nil
        code = xml_error_grammar.parse(response.http_response.body).code if
          xml_error_response?(response)
        
  
        case
        when response.timeout?
          response.error = Timeout::Error.new
  
        when code
          response.error =
            service_module::Errors.error_class(code).new(response.http_request,
                                                         response.http_response)
        when status >= 500
          response.error =
            Errors::ServerError.new(response.http_request, response.http_response)
  
        when status >= 300
          response.error =
            Errors::ClientError.new(response.http_request, response.http_response)
        end
  
      end
  
      protected
      def xml_error_response? response
        response.http_response.status >= 300 and
          response.http_response.body and
          xml_error_grammar.parse(response.http_response.body).respond_to?(:code)
      end
  
      protected
      def xml_error_grammar
        if service_module::const_defined?(:Errors) and
            service_module::Errors::const_defined?(:BASE_ERROR_GRAMMAR)
          service_module::Errors::BASE_ERROR_GRAMMAR
        else
          XmlGrammar
        end
      end
  
      protected
      def service_module
        AWS.const_get(self.class.to_s[/(\w+)::Client/, 1])
      end
  
      private
      def client_request name, options, &block
        return_or_raise(options) do
          log_client_request(name, 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 self.class::CACHEABLE_REQUESTS.include?(name) and
                  cache = AWS.response_cache and
                  cached_response = cache.cached(response)
                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
  
      private
      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.host = endpoint
        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
  
        send("configure_#{name}_request", http_request, opts, &block)
        http_request.headers["user-agent"] = user_agent_string
        http_request.add_authorization!(signer)
        http_request
      end
  
      private
      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
  
      private
      def self.add_client_request_method method_name, options = {}, &block
  
        method = ClientRequestMethodBuilder.new(self, method_name, &block)
  
        if xml_grammar = options[:xml_grammar]
  
          method.process_response do |resp|
            xml_grammar.parse(resp.http_response.body, :context => resp)
            super(resp)
          end
  
          method.simulate_response do |resp|
            xml_grammar.simulate(resp)
            super(resp)
          end
  
        end
  
        module_eval <<-END
          def #{method_name}(*args, &block)
            options = args.first ? args.first : {}
            client_request(#{method_name.inspect}, options, &block)
          end
        END
  
      end
  
      protected
      def self.configure_client
  
        module_eval('module Options; end')
        module_eval('module XML; end')
  
      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