lib/faulty/patch.rb
# frozen_string_literal: true
require 'faulty/patch/base'
class Faulty
# Automatic wrappers for common core dependencies like database connections
# or caches
module Patch
class << self
# Create a circuit from a configuration hash
#
# This is intended to be used in contexts where the user passes in
# something like a connection hash to a third-party library. For example
# the Redis patch hooks into the normal Redis connection options to add
# a `:faulty` key, which is a hash of faulty circuit options. This is
# slightly different from the normal Faulty circuit options because
# we also accept an `:instance` key which is a faulty instance.
#
# @example
# # We pass in a faulty instance along with some circuit options
# Patch.circuit_from_hash(
# :mysql,
# { host: 'localhost', faulty: {
# name: 'my_mysql', # A custom circuit name can be included
# instance: Faulty.new,
# sample_threshold: 5
# }
# }
# )
#
# @example
# # instance can be a registered faulty instance referenced by a string
# or symbol
# Faulty.register(:db_faulty, Faulty.new)
# Patch.circuit_from_hash(
# :mysql,
# { host: 'localhost', faulty: { instance: :db_faulty } }
# )
# @example
# # If instance is a hash with the key :constant, the value can be
# # a global constant name containing a Faulty instance
# DB_FAULTY = Faulty.new
# Patch.circuit_from_hash(
# :mysql,
# { host: 'localhost', faulty: { instance: { constant: 'DB_FAULTY' } } }
# )
#
# @example
# # Certain patches may want to enforce certain options like :errors
# # This can be done via hash or the usual block syntax
# Patch.circuit_from_hash(:mysql,
# { host: 'localhost', faulty: {} }
# errors: [Mysql2::Error]
# )
#
# Patch.circuit_from_hash(:mysql,
# { host: 'localhost', faulty: {} }
# ) do |conf|
# conf.errors = [Mysql2::Error]
# end
#
# @param default_name [String] The default name for the circuit
# @param hash [Hash] A hash of user-provided options. Supports any circuit
# option and these additional options
# @option hash [String] :name The circuit name. Defaults to `default_name`
# @option hash [Boolean] :patch_errors By default, circuit errors will be
# subclasses of `options[:patched_error_mapper]`. The user can disable
# this by setting this option to false.
# @option hash [Faulty, String, Symbol, Hash{ constant: String }] :instance
# A reference to a faulty instance. See examples.
# @param options [Hash] Additional override options. Supports any circuit
# option and these additional ones.
# @option options [Module] :patched_error_mapper The namespace module
# for patched errors or a mapping proc. See {Faulty::Circuit::Options}
# `:error_mapper`
# @yield [Circuit::Options] For setting override options in a block
# @return [Circuit, nil] The circuit if one was created
def circuit_from_hash(default_name, hash, **options, &block)
return unless hash
hash = symbolize_keys(hash)
name = hash.delete(:name) || default_name
patch_errors = hash.delete(:patch_errors) != false
error_mapper = options.delete(:patched_error_mapper)
hash[:error_mapper] ||= error_mapper if error_mapper && patch_errors
faulty = resolve_instance(hash.delete(:instance))
faulty.circuit(name, **hash, **options, &block)
end
# Create a full set of {CircuitError}s with a given base error class
#
# For patches that need their errors to be subclasses of a common base.
#
# @param namespace [Module] The module to define the error classes in
# @param base [Class] The base class for the error classes
# @return [void]
def define_circuit_errors(namespace, base)
circuit_error = Class.new(base) { include CircuitErrorBase }
namespace.const_set('CircuitError', circuit_error)
namespace.const_set('OpenCircuitError', Class.new(circuit_error))
namespace.const_set('CircuitFailureError', Class.new(circuit_error))
namespace.const_set('CircuitTrippedError', Class.new(circuit_error))
end
private
# Resolves a constant from a constant name or returns a default
#
# - If value is a string or symbol, gets a registered Faulty instance with that name
# - If value is a Hash with a key `:constant`, resolves the value to a global constant
# - If value is nil, gets Faulty.default
# - Otherwise, return value directly
#
# @param value [String, Symbol, Faulty, nil] The object or constant name to resolve
# @return [Object] The resolved Faulty instance
def resolve_instance(value)
case value
when String, Symbol
result = Faulty[value]
raise NameError, "No Faulty instance for #{value}" unless result
result
when Hash
const_name = value[:constant]
raise ArgumentError 'Missing hash key :constant for Faulty instance' unless const_name
Kernel.const_get(const_name)
when nil
Faulty.default
else
value
end
end
# Some config files may not suport symbol keys, so we convert the hash
# to use symbols so that users can pass in strings
#
# We cannot use transform_keys since we support Ruby < 2.5
#
# @param hash [Hash] A hash to convert
# @return [Hash] The hash with keys as symbols
def symbolize_keys(hash)
result = {}
hash.each do |key, val|
result[key.to_sym] = if val.is_a?(Hash)
symbolize_keys(val)
else
val
end
end
result
end
end
end
end