class Typesense::ApiCall

Constants

API_KEY_HEADER_NAME

Attributes

logger[R]

Public Class Methods

new(configuration) click to toggle source
# File lib/typesense/api_call.rb, line 12
def initialize(configuration)
  @configuration = configuration

  @api_key = @configuration.api_key
  @nodes = @configuration.nodes.dup # Make a copy, since we'll be adding additional metadata to the nodes
  @nearest_node = @configuration.nearest_node.dup
  @connection_timeout_seconds = @configuration.connection_timeout_seconds
  @healthcheck_interval_seconds = @configuration.healthcheck_interval_seconds
  @num_retries_per_request = @configuration.num_retries
  @retry_interval_seconds = @configuration.retry_interval_seconds

  @logger = @configuration.logger

  initialize_metadata_for_nodes
  @current_node_index = -1
end

Public Instance Methods

delete(endpoint, query_parameters = {}) click to toggle source
# File lib/typesense/api_call.rb, line 56
def delete(endpoint, query_parameters = {})
  perform_request :delete,
                  endpoint,
                  query_parameters: query_parameters
end
get(endpoint, query_parameters = {}) click to toggle source
# File lib/typesense/api_call.rb, line 50
def get(endpoint, query_parameters = {})
  perform_request :get,
                  endpoint,
                  query_parameters: query_parameters
end
patch(endpoint, body_parameters = {}, query_parameters = {}) click to toggle source
# File lib/typesense/api_call.rb, line 36
def patch(endpoint, body_parameters = {}, query_parameters = {})
  perform_request :patch,
                  endpoint,
                  query_parameters: query_parameters,
                  body_parameters: body_parameters
end
perform_request(method, endpoint, query_parameters: nil, body_parameters: nil, additional_headers: {}) click to toggle source
# File lib/typesense/api_call.rb, line 62
def perform_request(method, endpoint, query_parameters: nil, body_parameters: nil, additional_headers: {})
  @configuration.validate!
  last_exception = nil
  @logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
  (1..(@num_retries_per_request + 1)).each do |num_tries|
    node = next_node

    @logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"

    begin
      request_options = {
        method: method,
        timeout: @connection_timeout_seconds,
        headers: default_headers.merge(additional_headers)
      }
      request_options.merge!(params: query_parameters) unless query_parameters.nil?

      unless body_parameters.nil?
        body = body_parameters
        body = Oj.dump(body_parameters, mode: :compat) if request_options[:headers]['Content-Type'] == 'application/json'
        request_options.merge!(body: body)
      end

      response = Typhoeus::Request.new(uri_for(endpoint, node), request_options).run
      set_node_healthcheck(node, is_healthy: true) if response.code >= 1 && response.code <= 499

      @logger.debug "Request #{method}:#{uri_for(endpoint, node)} to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response.code}."

      parsed_response = if response.headers && (response.headers['content-type'] || '').include?('application/json')
                          Oj.load(response.body)
                        else
                          response.body
                        end

      # If response is 2xx return the object, else raise the response as an exception
      return parsed_response if response.code >= 200 && response.code <= 299

      exception_message = (parsed_response && parsed_response['message']) || 'Error'
      raise custom_exception_klass_for(response), exception_message
    rescue Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
           Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
           Typesense::Error::TimeoutError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
      # Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
      # Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
      #   other languages that might not support the same construct.
      set_node_healthcheck(node, is_healthy: false)
      last_exception = e
      @logger.warn "Request #{method}:#{uri_for(endpoint, node)} to Node #{node[:index]} failed due to \"#{e.class}: #{e.message}\""
      @logger.warn "Sleeping for #{@retry_interval_seconds}s and then retrying request..."
      sleep @retry_interval_seconds
    end
  end
  @logger.debug "No retries left. Raising last error \"#{last_exception.class}: #{last_exception.message}\"..."
  raise last_exception
end
post(endpoint, body_parameters = {}, query_parameters = {}) click to toggle source
# File lib/typesense/api_call.rb, line 29
def post(endpoint, body_parameters = {}, query_parameters = {})
  perform_request :post,
                  endpoint,
                  query_parameters: query_parameters,
                  body_parameters: body_parameters
end
put(endpoint, body_parameters = {}, query_parameters = {}) click to toggle source
# File lib/typesense/api_call.rb, line 43
def put(endpoint, body_parameters = {}, query_parameters = {})
  perform_request :put,
                  endpoint,
                  query_parameters: query_parameters,
                  body_parameters: body_parameters
end

Private Instance Methods

custom_exception_klass_for(response) click to toggle source
# File lib/typesense/api_call.rb, line 178
def custom_exception_klass_for(response)
  if response.code == 400
    Typesense::Error::RequestMalformed.new(response: response)
  elsif response.code == 401
    Typesense::Error::RequestUnauthorized.new(response: response)
  elsif response.code == 404
    Typesense::Error::ObjectNotFound.new(response: response)
  elsif response.code == 409
    Typesense::Error::ObjectAlreadyExists.new(response: response)
  elsif response.code == 422
    Typesense::Error::ObjectUnprocessable.new(response: response)
  elsif response.code >= 500 && response.code <= 599
    Typesense::Error::ServerError.new(response: response)
  elsif response.timed_out?
    Typesense::Error::TimeoutError.new(response: response)
  elsif response.code.zero?
    Typesense::Error::HTTPStatus0Error.new(response: response)
  else
    Typesense::Error::HTTPError.new(response: response)
  end
end
default_headers() click to toggle source
# File lib/typesense/api_call.rb, line 200
def default_headers
  {
    'Content-Type' => 'application/json',
    API_KEY_HEADER_NAME.to_s => @api_key,
    'User-Agent' => 'Typesense Ruby Client'
  }
end
initialize_metadata_for_nodes() click to toggle source
# File lib/typesense/api_call.rb, line 162
def initialize_metadata_for_nodes
  unless @nearest_node.nil?
    @nearest_node[:index] = 'nearest_node'
    set_node_healthcheck(@nearest_node, is_healthy: true)
  end
  @nodes.each_with_index do |node, index|
    node[:index] = index
    set_node_healthcheck(node, is_healthy: true)
  end
end
next_node() click to toggle source

Attempts to find the next healthy node, looping through the list of nodes once.

But if no healthy nodes are found, it will just return the next node, even if it's unhealthy
  so we can try the request for good measure, in case that node has become healthy since
# File lib/typesense/api_call.rb, line 127
def next_node
  # Check if nearest_node is set and is healthy, if so return it
  unless @nearest_node.nil?
    @logger.debug "Nodes health: Node #{@nearest_node[:index]} is #{@nearest_node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}"
    if @nearest_node[:is_healthy] == true || node_due_for_healthcheck?(@nearest_node)
      @logger.debug "Updated current node to Node #{@nearest_node[:index]}"
      return @nearest_node
    end
    @logger.debug 'Falling back to individual nodes'
  end

  # Fallback to nodes as usual
  @logger.debug "Nodes health: #{@nodes.each_with_index.map { |node, i| "Node #{i} is #{node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}" }.join(' || ')}"
  candidate_node = nil
  (0..@nodes.length).each do |_i|
    @current_node_index = (@current_node_index + 1) % @nodes.length
    candidate_node = @nodes[@current_node_index]
    if candidate_node[:is_healthy] == true || node_due_for_healthcheck?(candidate_node)
      @logger.debug "Updated current node to Node #{candidate_node[:index]}"
      return candidate_node
    end
  end

  # None of the nodes are marked healthy, but some of them could have become healthy since last health check.
  # So we will just return the next node.
  @logger.debug "No healthy nodes were found. Returning the next node, Node #{candidate_node[:index]}"
  candidate_node
end
node_due_for_healthcheck?(node) click to toggle source
# File lib/typesense/api_call.rb, line 156
def node_due_for_healthcheck?(node)
  is_due_for_check = Time.now.to_i - node[:last_access_timestamp] > @healthcheck_interval_seconds
  @logger.debug "Node #{node[:index]} has exceeded healthcheck_interval_seconds of #{@healthcheck_interval_seconds}. Adding it back into rotation." if is_due_for_check
  is_due_for_check
end
set_node_healthcheck(node, is_healthy:) click to toggle source
# File lib/typesense/api_call.rb, line 173
def set_node_healthcheck(node, is_healthy:)
  node[:is_healthy] = is_healthy
  node[:last_access_timestamp] = Time.now.to_i
end
uri_for(endpoint, node) click to toggle source
# File lib/typesense/api_call.rb, line 120
def uri_for(endpoint, node)
  "#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
end