lib/faulty/patch/elasticsearch.rb
# frozen_string_literal: true
class Faulty
module Patch
# Patch Elasticsearch to run requests in a circuit
#
# This module is not required by default
#
# Pass a `:faulty` key into your Elasticsearch client options to enable
# circuit protection. See {Patch.circuit_from_hash} for the available
# options.
#
# By default, all circuit errors raised by this patch inherit from
# `::Elasticsearch::Transport::Transport::Error`. One side effect of the way
# this patch wraps errors is that `host_unreachable_exceptions` raised by
# the inner transport adapters are converted into
# `Elasticsearch::Transport::Transport::Error` instead of the transport
# error type such as `Faraday::ConnectionFailed`.
#
# @example
# require 'faulty/patch/elasticsearch'
#
# es = Elasticsearch::Client.new(url: 'http://localhost:9200', faulty: {})
# es.search(q: 'test') # raises Faulty::CircuitError if connection fails
#
# # If the faulty key is not given, no circuit is used
# es = Elasticsearch::Client.new(url: 'http://localhost:9200', faulty: {})
# es.search(q: 'test') # not protected by a circuit
#
# # With Searchkick
# Searchkick.client_options[:faulty] = {}
#
# @see Patch.circuit_from_hash
module Elasticsearch
include Base
module Error; end
module SnifferTimeoutError; end
module ServerError; end
PATCHED_MODULE = if Gem.loaded_specs['opensearch-ruby']
require 'opensearch'
::OpenSearch
else
require 'elasticsearch'
::Elasticsearch
end
# We will freeze this after adding the dynamic error classes
MAPPED_ERRORS = { # rubocop:disable Style/MutableConstant
PATCHED_MODULE::Transport::Transport::Error => Error,
PATCHED_MODULE::Transport::Transport::SnifferTimeoutError => SnifferTimeoutError,
PATCHED_MODULE::Transport::Transport::ServerError => ServerError
}
module Errors
PATCHED_MODULE::Transport::Transport::ERRORS.each do |_code, klass|
MAPPED_ERRORS[klass] = const_set(klass.name.split('::').last, Module.new)
end
end
MAPPED_ERRORS.freeze
MAPPED_ERRORS.each do |klass, mod|
Patch.define_circuit_errors(mod, klass)
end
ERROR_MAPPER = lambda do |error_name, cause, circuit|
MAPPED_ERRORS.fetch(cause&.class, Error).const_get(error_name).new(cause&.message, circuit)
end
private_constant :ERROR_MAPPER, :MAPPED_ERRORS
def initialize(arguments = {}, &block)
super
errors = [PATCHED_MODULE::Transport::Transport::Error]
errors.concat(@transport.host_unreachable_exceptions)
@faulty_circuit = Patch.circuit_from_hash(
'elasticsearch',
arguments[:faulty],
errors: errors,
exclude: PATCHED_MODULE::Transport::Transport::Errors::NotFound,
patched_error_mapper: ERROR_MAPPER
)
end
# Protect all elasticsearch requests
def perform_request(*args)
faulty_run { super }
end
end
end
end
if Gem.loaded_specs['opensearch-ruby']
module OpenSearch
module Transport
class Client
prepend(Faulty::Patch::Elasticsearch)
end
end
end
else
module Elasticsearch
module Transport
class Client
prepend(Faulty::Patch::Elasticsearch)
end
end
end
end